mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat(mcp_server): update API base URL environment variable to include complete path (#9542)
This commit is contained in:
committed by
GitHub
parent
6761f0ffd0
commit
e0cf8bffd4
@@ -1,3 +1,3 @@
|
||||
PROWLER_APP_API_KEY="pk_your_api_key_here"
|
||||
PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
API_BASE_URL="https://api.prowler.com/api/v1"
|
||||
PROWLER_MCP_TRANSPORT_MODE="stdio"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
## [0.2.1] (UNRELEASED)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update API base URL environment variable to include complete path [(#9542)](https://github.com/prowler-cloud/prowler/pull/9300)
|
||||
|
||||
## [0.2.0] (Prowler v5.15.0)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -78,7 +78,7 @@ With environment variables:
|
||||
```bash
|
||||
docker run --rm -i \
|
||||
-e PROWLER_APP_API_KEY="pk_your_api_key" \
|
||||
-e PROWLER_API_BASE_URL="https://api.prowler.com" \
|
||||
-e API_BASE_URL="https://api.prowler.com/api/v1" \
|
||||
prowlercloud/prowler-mcp
|
||||
```
|
||||
|
||||
@@ -144,10 +144,10 @@ uv run prowler-mcp --transport http
|
||||
uv run prowler-mcp --transport http --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
For self-deployed MCP remote server, you can use also configure the server to use a custom API base URL with the environment variable `PROWLER_API_BASE_URL`; and the transport mode with the environment variable `PROWLER_MCP_TRANSPORT_MODE`.
|
||||
For self-deployed MCP remote server, you can use also configure the server to use a custom API base URL with the environment variable `API_BASE_URL`; and the transport mode with the environment variable `PROWLER_MCP_TRANSPORT_MODE`.
|
||||
|
||||
```bash
|
||||
export PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
export API_BASE_URL="https://api.prowler.com/api/v1"
|
||||
export PROWLER_MCP_TRANSPORT_MODE="http"
|
||||
```
|
||||
|
||||
@@ -319,7 +319,7 @@ For STDIO mode, authentication is handled via environment variables using an API
|
||||
export PROWLER_APP_API_KEY="pk_your_api_key_here"
|
||||
|
||||
# Optional - for custom API endpoint, in case not provided Prowler Cloud API will be used
|
||||
export PROWLER_API_BASE_URL="https://api.prowler.com"
|
||||
export API_BASE_URL="https://api.prowler.com/api/v1"
|
||||
```
|
||||
|
||||
#### HTTP Mode Authentication
|
||||
@@ -370,7 +370,7 @@ For local execution, configure your MCP client to launch the server directly. Be
|
||||
"args": ["/path/to/prowler/mcp_server/"],
|
||||
"env": {
|
||||
"PROWLER_APP_API_KEY": "pk_your_api_key_here",
|
||||
"PROWLER_API_BASE_URL": "https://api.prowler.com" // Optional, in case not provided Prowler Cloud API will be used
|
||||
"API_BASE_URL": "https://api.prowler.com/api/v1" // Optional, in case not provided Prowler Cloud API will be used
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,7 +387,7 @@ For local execution, configure your MCP client to launch the server directly. Be
|
||||
"args": [
|
||||
"run", "--rm", "-i",
|
||||
"--env", "PROWLER_APP_API_KEY=pk_your_api_key_here",
|
||||
"--env", "PROWLER_API_BASE_URL=https://api.prowler.com", // Optional, in case not provided Prowler Cloud API will be used
|
||||
"--env", "API_BASE_URL=https://api.prowler.com/api/v1", // Optional, in case not provided Prowler Cloud API will be used
|
||||
"prowler-mcp"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class BaseTool(ABC):
|
||||
|
||||
async def search_security_findings(self, severity: list[str] = Field(...)):
|
||||
# Implementation with access to self.api_client
|
||||
response = await self.api_client.get("/api/v1/findings")
|
||||
response = await self.api_client.get("/findings")
|
||||
return response
|
||||
"""
|
||||
|
||||
|
||||
@@ -6,13 +6,14 @@ across all cloud providers.
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.findings import (
|
||||
DetailedFinding,
|
||||
FindingsListResponse,
|
||||
FindingsOverview,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class FindingsTools(BaseTool):
|
||||
@@ -122,11 +123,11 @@ class FindingsTools(BaseTool):
|
||||
|
||||
if date_range is None:
|
||||
# No dates provided - use latest findings endpoint
|
||||
endpoint = "/api/v1/findings/latest"
|
||||
endpoint = "/findings/latest"
|
||||
params = {}
|
||||
else:
|
||||
# Dates provided - use historical findings endpoint
|
||||
endpoint = "/api/v1/findings"
|
||||
endpoint = "/findings"
|
||||
params = {
|
||||
"filter[inserted_at__gte]": date_range[0],
|
||||
"filter[inserted_at__lte]": date_range[1],
|
||||
@@ -228,7 +229,7 @@ class FindingsTools(BaseTool):
|
||||
|
||||
# Get API response and transform to detailed format
|
||||
api_response = await self.api_client.get(
|
||||
f"/api/v1/findings/{finding_id}", params=params
|
||||
f"/findings/{finding_id}", params=params
|
||||
)
|
||||
detailed_finding = DetailedFinding.from_api_response(
|
||||
api_response.get("data", {})
|
||||
@@ -281,7 +282,7 @@ class FindingsTools(BaseTool):
|
||||
|
||||
# Get API response and transform to simplified format
|
||||
api_response = await self.api_client.get(
|
||||
"/api/v1/overviews/findings", params=clean_params
|
||||
"/overviews/findings", params=clean_params
|
||||
)
|
||||
overview = FindingsOverview.from_api_response(api_response)
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ This module provides tools for managing finding muting in Prowler, including:
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.muting import (
|
||||
DetailedMuteRule,
|
||||
MutelistResponse,
|
||||
MuteRulesListResponse,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class MutingTools(BaseTool):
|
||||
@@ -53,9 +54,7 @@ class MutingTools(BaseTool):
|
||||
}
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
api_response = await self.api_client.get(
|
||||
"/api/v1/processors", params=clean_params
|
||||
)
|
||||
api_response = await self.api_client.get("/processors", params=clean_params)
|
||||
|
||||
data = api_response.get("data", [])
|
||||
|
||||
@@ -145,7 +144,7 @@ Structure:
|
||||
}
|
||||
|
||||
api_response = await self.api_client.post(
|
||||
"/api/v1/processors", json_data=create_body
|
||||
"/processors", json_data=create_body
|
||||
)
|
||||
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
|
||||
return mutelist.model_dump()
|
||||
@@ -163,7 +162,7 @@ Structure:
|
||||
}
|
||||
|
||||
api_response = await self.api_client.patch(
|
||||
f"/api/v1/processors/{existing_mutelist['id']}", json_data=update_body
|
||||
f"/processors/{existing_mutelist['id']}", json_data=update_body
|
||||
)
|
||||
mutelist = MutelistResponse.from_api_response(api_response.get("data", {}))
|
||||
return mutelist.model_dump()
|
||||
@@ -194,7 +193,7 @@ Structure:
|
||||
|
||||
# Delete the mutelist
|
||||
mutelist_id = existing_mutelist["id"]
|
||||
await self.api_client.delete(f"/api/v1/processors/{mutelist_id}")
|
||||
await self.api_client.delete(f"/processors/{mutelist_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -276,9 +275,7 @@ Structure:
|
||||
params["filter[search]"] = search
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
api_response = await self.api_client.get(
|
||||
"/api/v1/mute-rules", params=clean_params
|
||||
)
|
||||
api_response = await self.api_client.get("/mute-rules", params=clean_params)
|
||||
|
||||
simplified_response = MuteRulesListResponse.from_api_response(api_response)
|
||||
return simplified_response.model_dump()
|
||||
@@ -311,7 +308,7 @@ Structure:
|
||||
}
|
||||
|
||||
api_response = await self.api_client.get(
|
||||
f"/api/v1/mute-rules/{rule_id}", params=params
|
||||
f"/mute-rules/{rule_id}", params=params
|
||||
)
|
||||
|
||||
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
|
||||
@@ -363,9 +360,7 @@ Structure:
|
||||
}
|
||||
}
|
||||
|
||||
api_response = await self.api_client.post(
|
||||
"/api/v1/mute-rules", json_data=create_body
|
||||
)
|
||||
api_response = await self.api_client.post("/mute-rules", json_data=create_body)
|
||||
|
||||
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
|
||||
return detailed_rule.model_dump()
|
||||
@@ -432,10 +427,9 @@ Structure:
|
||||
}
|
||||
|
||||
api_response = await self.api_client.patch(
|
||||
f"/api/v1/mute-rules/{rule_id}", json_data=update_body
|
||||
f"/mute-rules/{rule_id}", json_data=update_body
|
||||
)
|
||||
|
||||
self.logger.info(f"API response: {api_response}")
|
||||
detailed_rule = DetailedMuteRule.from_api_response(api_response.get("data", {}))
|
||||
return detailed_rule.model_dump()
|
||||
|
||||
@@ -463,7 +457,7 @@ Structure:
|
||||
"""
|
||||
self.logger.info(f"Deleting mute rule {rule_id}...")
|
||||
|
||||
result = await self.api_client.delete(f"/api/v1/mute-rules/{rule_id}")
|
||||
result = await self.api_client.delete(f"/mute-rules/{rule_id}")
|
||||
|
||||
if result.get("success"):
|
||||
return {
|
||||
|
||||
@@ -6,12 +6,13 @@ including searching, connecting, and deleting providers.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.providers import (
|
||||
ProviderConnectionStatus,
|
||||
ProvidersListResponse,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class ProvidersTools(BaseTool):
|
||||
@@ -100,9 +101,7 @@ class ProvidersTools(BaseTool):
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
api_response = await self.api_client.get(
|
||||
"/api/v1/providers", params=clean_params
|
||||
)
|
||||
api_response = await self.api_client.get("/providers", params=clean_params)
|
||||
simplified_response = ProvidersListResponse.from_api_response(api_response)
|
||||
|
||||
# Fetch secret_type for each provider that has a secret
|
||||
@@ -306,9 +305,7 @@ class ProvidersTools(BaseTool):
|
||||
self.logger.info(f"Deleting provider {provider_id}...")
|
||||
try:
|
||||
# Initiate the deletion task
|
||||
task_response = await self.api_client.delete(
|
||||
f"/api/v1/providers/{provider_id}"
|
||||
)
|
||||
task_response = await self.api_client.delete(f"/providers/{provider_id}")
|
||||
task_id = task_response.get("data", {}).get("id")
|
||||
|
||||
# Poll until task completes (with 60 second timeout)
|
||||
@@ -345,7 +342,7 @@ class ProvidersTools(BaseTool):
|
||||
"""
|
||||
self.logger.info(f"Checking if provider {provider_uid} exists...")
|
||||
response = await self.api_client.get(
|
||||
"/api/v1/providers", params={"filter[uid]": provider_uid}
|
||||
"/providers", params={"filter[uid]": provider_uid}
|
||||
)
|
||||
providers = response.get("data", [])
|
||||
|
||||
@@ -391,7 +388,7 @@ class ProvidersTools(BaseTool):
|
||||
if alias:
|
||||
provider_body["data"]["attributes"]["alias"] = alias
|
||||
|
||||
await self.api_client.post("/api/v1/providers", json_data=provider_body)
|
||||
await self.api_client.post("/providers", json_data=provider_body)
|
||||
|
||||
provider_id = await self._check_provider_exists(provider_uid)
|
||||
if provider_id is None:
|
||||
@@ -418,7 +415,7 @@ class ProvidersTools(BaseTool):
|
||||
}
|
||||
}
|
||||
result = await self.api_client.patch(
|
||||
f"/api/v1/providers/{prowler_provider_id}", json_data=update_body
|
||||
f"/providers/{prowler_provider_id}", json_data=update_body
|
||||
)
|
||||
if result.get("data", {}).get("attributes", {}).get("alias") != alias:
|
||||
raise Exception(f"Provider {prowler_provider_id} alias update failed")
|
||||
@@ -450,7 +447,7 @@ class ProvidersTools(BaseTool):
|
||||
"""
|
||||
try:
|
||||
response = await self.api_client.get(
|
||||
"/api/v1/providers/secrets",
|
||||
"/providers/secrets",
|
||||
params={"filter[provider]": prowler_provider_id},
|
||||
)
|
||||
secrets = response.get("data", [])
|
||||
@@ -481,7 +478,7 @@ class ProvidersTools(BaseTool):
|
||||
"""
|
||||
try:
|
||||
response = await self.api_client.get(
|
||||
f"/api/v1/providers/secrets/{secret_id}",
|
||||
f"/providers/secrets/{secret_id}",
|
||||
params={"fields[provider-secrets]": "secret_type"},
|
||||
)
|
||||
secret_type = (
|
||||
@@ -536,7 +533,7 @@ class ProvidersTools(BaseTool):
|
||||
}
|
||||
try:
|
||||
response = await self.api_client.patch(
|
||||
f"/api/v1/providers/secrets/{existing_secret_id}",
|
||||
f"/providers/secrets/{existing_secret_id}",
|
||||
json_data=update_body,
|
||||
)
|
||||
self.logger.info("Credentials updated successfully")
|
||||
@@ -567,7 +564,7 @@ class ProvidersTools(BaseTool):
|
||||
|
||||
try:
|
||||
response = await self.api_client.post(
|
||||
"/api/v1/providers/secrets", json_data=secret_body
|
||||
"/providers/secrets", json_data=secret_body
|
||||
)
|
||||
self.logger.info("Credentials added successfully")
|
||||
return response
|
||||
@@ -588,7 +585,7 @@ class ProvidersTools(BaseTool):
|
||||
try:
|
||||
# Initiate the connection test task
|
||||
task_response = await self.api_client.post(
|
||||
f"/api/v1/providers/{prowler_provider_id}/connection", json_data={}
|
||||
f"/providers/{prowler_provider_id}/connection", json_data={}
|
||||
)
|
||||
task_id = task_response.get("data", {}).get("id")
|
||||
|
||||
@@ -619,5 +616,5 @@ class ProvidersTools(BaseTool):
|
||||
Provider data dictionary
|
||||
"""
|
||||
return await self.api_client.get(
|
||||
f"/api/v1/providers/{prowler_provider_id}",
|
||||
f"/providers/{prowler_provider_id}",
|
||||
)
|
||||
|
||||
@@ -6,13 +6,14 @@ across all providers.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.resources import (
|
||||
DetailedResource,
|
||||
ResourcesListResponse,
|
||||
ResourcesMetadataResponse,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class ResourcesTools(BaseTool):
|
||||
@@ -121,11 +122,11 @@ class ResourcesTools(BaseTool):
|
||||
|
||||
if date_range is None:
|
||||
# No dates provided - use latest resources endpoint
|
||||
endpoint = "/api/v1/resources/latest"
|
||||
endpoint = "/resources/latest"
|
||||
params = {}
|
||||
else:
|
||||
# Dates provided - use historical resources endpoint
|
||||
endpoint = "/api/v1/resources"
|
||||
endpoint = "/resources"
|
||||
params = {
|
||||
"filter[updated_at__gte]": date_range[0],
|
||||
"filter[updated_at__lte]": date_range[1],
|
||||
@@ -206,9 +207,8 @@ class ResourcesTools(BaseTool):
|
||||
|
||||
# Get API response and transform to detailed format
|
||||
api_response = await self.api_client.get(
|
||||
f"/api/v1/resources/{resource_id}", params=params
|
||||
f"/resources/{resource_id}", params=params
|
||||
)
|
||||
self.logger.info(f"API response: {api_response}")
|
||||
detailed_resource = DetailedResource.from_api_response(
|
||||
api_response.get("data", {})
|
||||
)
|
||||
@@ -265,13 +265,13 @@ class ResourcesTools(BaseTool):
|
||||
|
||||
if date_range is None:
|
||||
# No dates provided - use latest metadata endpoint
|
||||
metadata_endpoint = "/api/v1/resources/metadata/latest"
|
||||
list_endpoint = "/api/v1/resources/latest"
|
||||
metadata_endpoint = "/resources/metadata/latest"
|
||||
list_endpoint = "/resources/latest"
|
||||
params = {}
|
||||
else:
|
||||
# Dates provided - use historical endpoints
|
||||
metadata_endpoint = "/api/v1/resources/metadata"
|
||||
list_endpoint = "/api/v1/resources"
|
||||
metadata_endpoint = "/resources/metadata"
|
||||
list_endpoint = "/resources"
|
||||
params = {
|
||||
"filter[updated_at__gte]": date_range[0],
|
||||
"filter[updated_at__lte]": date_range[1],
|
||||
|
||||
@@ -5,6 +5,8 @@ This module provides tools for managing and monitoring Prowler security scans.
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from prowler_mcp_server.prowler_app.models.scans import (
|
||||
DetailedScan,
|
||||
ScanCreationResult,
|
||||
@@ -12,7 +14,6 @@ from prowler_mcp_server.prowler_app.models.scans import (
|
||||
ScheduleCreationResult,
|
||||
)
|
||||
from prowler_mcp_server.prowler_app.tools.base import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class ScansTools(BaseTool):
|
||||
@@ -119,7 +120,7 @@ class ScansTools(BaseTool):
|
||||
|
||||
clean_params = self.api_client.build_filter_params(params)
|
||||
|
||||
api_response = await self.api_client.get("/api/v1/scans", params=clean_params)
|
||||
api_response = await self.api_client.get("/scans", params=clean_params)
|
||||
simplified_response = ScansListResponse.from_api_response(api_response)
|
||||
|
||||
return simplified_response.model_dump()
|
||||
@@ -163,9 +164,7 @@ class ScansTools(BaseTool):
|
||||
"fields[scans]": "name,trigger,state,progress,duration,unique_resource_count,started_at,completed_at,scheduled_at,next_scan_at,inserted_at"
|
||||
}
|
||||
|
||||
api_response = await self.api_client.get(
|
||||
f"/api/v1/scans/{scan_id}", params=params
|
||||
)
|
||||
api_response = await self.api_client.get(f"/scans/{scan_id}", params=params)
|
||||
detailed_scan = DetailedScan.from_api_response(api_response["data"])
|
||||
|
||||
return detailed_scan.model_dump()
|
||||
@@ -213,9 +212,7 @@ class ScansTools(BaseTool):
|
||||
|
||||
# Create scan (returns Task)
|
||||
self.logger.info(f"Creating scan for provider {provider_id}")
|
||||
task_response = await self.api_client.post(
|
||||
"/api/v1/scans", json_data=request_data
|
||||
)
|
||||
task_response = await self.api_client.post("/scans", json_data=request_data)
|
||||
|
||||
scan_id = (
|
||||
task_response.get("data", {})
|
||||
@@ -228,7 +225,7 @@ class ScansTools(BaseTool):
|
||||
raise Exception("No scan_id returned from scan creation")
|
||||
|
||||
self.logger.info(f"Scan created successfully: {scan_id}")
|
||||
scan_response = await self.api_client.get(f"/api/v1/scans/{scan_id}")
|
||||
scan_response = await self.api_client.get(f"/scans/{scan_id}")
|
||||
scan_info = DetailedScan.from_api_response(scan_response["data"])
|
||||
|
||||
return ScanCreationResult(
|
||||
@@ -273,7 +270,7 @@ class ScansTools(BaseTool):
|
||||
"""
|
||||
self.logger.info(f"Creating daily schedule for provider {provider_id}")
|
||||
task_response = await self.api_client.post(
|
||||
"/api/v1/schedules/daily",
|
||||
"/schedules/daily",
|
||||
json_data={
|
||||
"data": {
|
||||
"type": "daily-schedules",
|
||||
@@ -316,7 +313,7 @@ class ScansTools(BaseTool):
|
||||
2. Use this tool with the scan 'id' and new name
|
||||
"""
|
||||
api_response = await self.api_client.patch(
|
||||
f"/api/v1/scans/{scan_id}",
|
||||
f"/scans/{scan_id}",
|
||||
json_data={
|
||||
"data": {
|
||||
"type": "scans",
|
||||
|
||||
@@ -228,7 +228,7 @@ class ProwlerAPIClient(metaclass=SingletonMeta):
|
||||
)
|
||||
|
||||
# Fetch current task state
|
||||
response = await self.get(f"/api/v1/tasks/{task_id}")
|
||||
response = await self.get(f"/tasks/{task_id}")
|
||||
task_data = response.get("data", {})
|
||||
task_attrs = task_data.get("attributes", {})
|
||||
state = task_attrs.get("state")
|
||||
|
||||
@@ -15,7 +15,7 @@ class ProwlerAppAuth:
|
||||
def __init__(
|
||||
self,
|
||||
mode: str = os.getenv("PROWLER_MCP_TRANSPORT_MODE", "stdio"),
|
||||
base_url: str = os.getenv("PROWLER_API_BASE_URL", "https://api.prowler.com"),
|
||||
base_url: str = os.getenv("API_BASE_URL", "https://api.prowler.com/api/v1"),
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
logger.info(f"Using Prowler App API base URL: {self.base_url}")
|
||||
|
||||
Reference in New Issue
Block a user