mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
Merge branch 'master' into todo-check-class
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# poetry
|
||||
poetry.lock
|
||||
|
||||
# pdm
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
pdm.lock
|
||||
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
.cursor/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# uv
|
||||
uv.lock
|
||||
@@ -0,0 +1,88 @@
|
||||
# Prowler MCP Server
|
||||
|
||||
Access the entire Prowler ecosystem through the Model Context Protocol (MCP), the supported capabilities right now are:
|
||||
|
||||
- Prowler Hub for checking the current covering in checks, fixers and compliance frameworks in Prowler.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.12+
|
||||
- Network access to `https://hub.prowler.com`
|
||||
|
||||
## Installation
|
||||
|
||||
### From Sources
|
||||
|
||||
It is needed to have [uv](https://docs.astral.sh/uv/) installed.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/prowler-cloud/prowler.git
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
After installation, start the MCP server via the console script:
|
||||
|
||||
```bash
|
||||
cd prowler/mcp_server
|
||||
uv run prowler-mcp
|
||||
```
|
||||
|
||||
Alternatively, you can run from wherever you want using `uvx` command:
|
||||
|
||||
```bash
|
||||
uvx /path/to/prowler/mcp_server/
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Prowler Hub
|
||||
|
||||
All tools are exposed under the `prowler_hub` prefix.
|
||||
|
||||
- prowler_hub_get_check_filters: Return available filter values for checks (providers, services, severities, categories, compliances). Call this before `prowler_hub_get_checks` to build valid queries.
|
||||
- prowler_hub_get_checks: List checks with option of advanced filtering.
|
||||
- prowler_hub_search_checks: Full‑text search across check metadata.
|
||||
- prowler_hub_get_compliance_frameworks: List/filter compliance frameworks.
|
||||
- prowler_hub_search_compliance_frameworks: Full-text search across frameworks.
|
||||
- prowler_hub_list_providers: List Prowler official providers and their services.
|
||||
- prowler_hub_get_artifacts_count: Return total artifact count (checks + frameworks).
|
||||
|
||||
## MCP Client Configuration
|
||||
|
||||
Configure your MCP client to launch the server with the `uvx` command. Below is a generic snippet; consult your client's documentation for exact locations.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"prowler": {
|
||||
"command": "uvx",
|
||||
"args": ["/path/to/prowler/mcp_server/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Desktop (macOS/Windows)
|
||||
|
||||
Add the server to Claude Desktop’s config file, then restart the app.
|
||||
|
||||
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- Windows: `%AppData%\Claude\claude_desktop_config.json` (e.g. `C:\\Users\\<you>\\AppData\\Roaming\\Claude\\claude_desktop_config.json`)
|
||||
|
||||
Example content to append/merge:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"prowler": {
|
||||
"command": "uvx",
|
||||
"args": ["/path/to/prowler/mcp_server/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project follows the repository’s main license. See the [LICENSE](../LICENSE) file at the repository root.
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Prowler MCP - Model Context Protocol server for Prowler ecosystem
|
||||
|
||||
This package provides MCP tools for accessing:
|
||||
- Prowler Hub: All security artifacts (detections, remediations and frameworks) supported by Prowler
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Prowler Team"
|
||||
__email__ = "engineering@prowler.com"
|
||||
|
||||
__all__ = ["__version__", "prowler_mcp_server"]
|
||||
@@ -0,0 +1,20 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from prowler_mcp_server.server import setup_main_server, prowler_mcp_server
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the MCP server."""
|
||||
try:
|
||||
asyncio.run(setup_main_server())
|
||||
prowler_mcp_server.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down Prowler MCP server...")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Prowler Hub module for MCP server."""
|
||||
|
||||
__all__ = ["prowler_hub_mcp"]
|
||||
@@ -0,0 +1,479 @@
|
||||
"""
|
||||
Prowler Hub MCP module
|
||||
|
||||
Provides access to Prowler Hub API for security checks and compliance frameworks.
|
||||
"""
|
||||
|
||||
from typing import Optional, Any
|
||||
import httpx
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Initialize FastMCP for Prowler Hub
|
||||
hub_mcp_server = FastMCP("prowler-hub")
|
||||
|
||||
# API base URL
|
||||
BASE_URL = "https://hub.prowler.com/api"
|
||||
|
||||
# HTTP client configuration
|
||||
client = httpx.Client(
|
||||
base_url=BASE_URL, timeout=30.0, headers={"Accept": "application/json"}
|
||||
)
|
||||
|
||||
# GitHub raw content base URL for Prowler checks
|
||||
GITHUB_RAW_BASE = (
|
||||
"https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/"
|
||||
"prowler/providers"
|
||||
)
|
||||
|
||||
# Separate HTTP client for GitHub raw content
|
||||
github_client = httpx.Client(
|
||||
timeout=30.0,
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "prowler-mcp-server/1.0",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def github_check_path(provider_id: str, check_id: str, suffix: str) -> str:
|
||||
"""Build the GitHub raw URL for a given check artifact suffix using provider
|
||||
and check_id.
|
||||
|
||||
Suffix examples: ".metadata.json", ".py", "_fixer.py"
|
||||
"""
|
||||
try:
|
||||
service_id = check_id.split("_", 1)[0]
|
||||
except IndexError:
|
||||
service_id = check_id
|
||||
return f"{GITHUB_RAW_BASE}/{provider_id}/services/{service_id}/{check_id}/{check_id}{suffix}"
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_filters() -> dict[str, Any]:
|
||||
"""
|
||||
Get available values for filtering for tool `get_checks`. Recommended to use before calling `get_checks` to get the available values for the filters.
|
||||
|
||||
Returns:
|
||||
Available filter options including providers, types, services, severities,
|
||||
categories, and compliance frameworks with their respective counts
|
||||
"""
|
||||
try:
|
||||
response = client.get("/check/filters")
|
||||
response.raise_for_status()
|
||||
filters = response.json()
|
||||
|
||||
return {"filters": filters}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# Security Check Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def get_checks(
|
||||
providers: Optional[str] = None,
|
||||
types: Optional[str] = None,
|
||||
services: Optional[str] = None,
|
||||
severities: Optional[str] = None,
|
||||
categories: Optional[str] = None,
|
||||
compliances: Optional[str] = None,
|
||||
ids: Optional[str] = None,
|
||||
fields: Optional[str] = "id,service,severity,title,description,risk",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
List security Prowler Checks. The list can be filtered by the parameters defined for the tool.
|
||||
It is recommended to use the tool `get_check_filters` to get the available values for the filters.
|
||||
A not filtered request will return more than 1000 checks, so it is recommended to use the filters.
|
||||
|
||||
Args:
|
||||
providers: Filter by Prowler provider IDs. Example: "aws,azure". Use the tool `list_providers` to get the available providers IDs.
|
||||
types: Filter by check types.
|
||||
services: Filter by provider services IDs. Example: "s3,keyvault". Use the tool `list_providers` to get the available services IDs in a provider.
|
||||
severities: Filter by severity levels. Example: "medium,high". Available values are "low", "medium", "high", "critical".
|
||||
categories: Filter by categories. Example: "cluster-security,encryption".
|
||||
compliances: Filter by compliance framework IDs. Example: "cis_4.0_aws,ens_rd2022_azure".
|
||||
ids: Filter by specific check IDs. Example: "s3_bucket_level_public_access_block".
|
||||
fields: Specify which fields from checks metadata to return (id is always included). Example: "id,title,description,risk".
|
||||
Available values are "id", "title", "description", "provider", "type", "service", "subservice", "severity", "risk", "reference", "remediation", "services_required", "aws_arn_template", "notes", "categories", "default_value", "resource_type", "related_url", "depends_on", "related_to", "fixer".
|
||||
The default parameters are "id,title,description".
|
||||
If null, all fields will be returned.
|
||||
|
||||
Returns:
|
||||
List of security checks matching the filters. The structure is as follows:
|
||||
{
|
||||
"count": N,
|
||||
"checks": [
|
||||
{"id": "check_id_1", "title": "check_title_1", "description": "check_description_1", ...},
|
||||
{"id": "check_id_2", "title": "check_title_2", "description": "check_description_2", ...},
|
||||
{"id": "check_id_3", "title": "check_title_3", "description": "check_description_3", ...},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
params: dict[str, str] = {}
|
||||
|
||||
if providers:
|
||||
params["providers"] = providers
|
||||
if types:
|
||||
params["types"] = types
|
||||
if services:
|
||||
params["services"] = services
|
||||
if severities:
|
||||
params["severities"] = severities
|
||||
if categories:
|
||||
params["categories"] = categories
|
||||
if compliances:
|
||||
params["compliances"] = compliances
|
||||
if ids:
|
||||
params["ids"] = ids
|
||||
if fields:
|
||||
params["fields"] = fields
|
||||
|
||||
try:
|
||||
response = client.get("/check", params=params)
|
||||
response.raise_for_status()
|
||||
checks = response.json()
|
||||
|
||||
checks_dict = {}
|
||||
for check in checks:
|
||||
check_data = {}
|
||||
# Always include the id field as it's mandatory for the response structure
|
||||
if "id" in check:
|
||||
check_data["id"] = check["id"]
|
||||
|
||||
# Include other requested fields
|
||||
for field in fields.split(","):
|
||||
if field != "id" and field in check: # Skip id since it's already added
|
||||
check_data[field] = check[field]
|
||||
checks_dict[check["id"]] = check_data
|
||||
|
||||
return {"count": len(checks), "checks": checks_dict}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_raw_metadata(
|
||||
provider_id: str,
|
||||
check_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch the raw check metadata JSON, this is a low level version of the tool `get_checks`.
|
||||
It is recommended to use the tool `get_checks` filtering about the `ids` parameter instead of using this tool.
|
||||
|
||||
Args:
|
||||
provider_id: Prowler provider ID (e.g., "aws", "azure").
|
||||
check_id: Prowler check ID (folder and base filename).
|
||||
|
||||
Returns:
|
||||
Raw metadata JSON as stored in Prowler.
|
||||
"""
|
||||
if provider_id and check_id:
|
||||
url = github_check_path(provider_id, check_id, ".metadata.json")
|
||||
try:
|
||||
resp = github_client.get(url)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
return {
|
||||
"error": f"Check {check_id} not found in Prowler",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": f"Error fetching check {check_id} from Prowler: {str(e)}",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": "Provider ID and check ID are required",
|
||||
}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_code(
|
||||
provider_id: str,
|
||||
check_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch the check implementation Python code from Prowler.
|
||||
|
||||
Args:
|
||||
provider_id: Prowler provider ID (e.g., "aws", "azure").
|
||||
check_id: Prowler check ID (e.g., "opensearch_service_domains_not_publicly_accessible").
|
||||
|
||||
Returns:
|
||||
Dict with the code content as text.
|
||||
"""
|
||||
if provider_id and check_id:
|
||||
url = github_check_path(provider_id, check_id, ".py")
|
||||
try:
|
||||
resp = github_client.get(url)
|
||||
resp.raise_for_status()
|
||||
return {
|
||||
"content": resp.text,
|
||||
}
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
return {
|
||||
"error": f"Check {check_id} not found in Prowler",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": "Provider ID and check ID are required",
|
||||
}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def get_check_fixer(
|
||||
provider_id: str,
|
||||
check_id: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Fetch the check fixer Python code from Prowler, if it exists.
|
||||
|
||||
Args:
|
||||
provider_id: Prowler provider ID (e.g., "aws", "azure").
|
||||
check_id: Prowler check ID (e.g., "opensearch_service_domains_not_publicly_accessible").
|
||||
|
||||
Returns:
|
||||
Dict with fixer content as text if present, existence flag.
|
||||
"""
|
||||
if provider_id and check_id:
|
||||
url = github_check_path(provider_id, check_id, "_fixer.py")
|
||||
try:
|
||||
resp = github_client.get(url)
|
||||
if resp.status_code == 404:
|
||||
return {
|
||||
"error": f"Fixer not found for check {check_id}",
|
||||
}
|
||||
resp.raise_for_status()
|
||||
return {
|
||||
"content": resp.text,
|
||||
}
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 404:
|
||||
return {
|
||||
"error": f"Check {check_id} not found in Prowler",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": "Provider ID and check ID are required",
|
||||
}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def search_checks(term: str) -> dict[str, Any]:
|
||||
"""
|
||||
Search the term across all text properties of check metadata.
|
||||
|
||||
Args:
|
||||
term: Search term to find in check titles, descriptions, and other text fields
|
||||
|
||||
Returns:
|
||||
List of checks matching the search term
|
||||
"""
|
||||
try:
|
||||
response = client.get("/check/search", params={"term": term})
|
||||
response.raise_for_status()
|
||||
checks = response.json()
|
||||
|
||||
return {
|
||||
"count": len(checks),
|
||||
"checks": checks,
|
||||
}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# Compliance Framework Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def get_compliance_frameworks(
|
||||
provider: Optional[str] = None,
|
||||
fields: Optional[
|
||||
str
|
||||
] = "id,framework,provider,description,total_checks,total_requirements",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
List and filter compliance frameworks. The list can be filtered by the parameters defined for the tool.
|
||||
|
||||
Args:
|
||||
provider: Filter by one Prowler provider ID. Example: "aws". Use the tool `list_providers` to get the available providers IDs.
|
||||
fields: Specify which fields to return (id is always included). Example: "id,provider,description,version".
|
||||
It is recommended to run with the default parameters because the full response is too large.
|
||||
Available values are "id", "framework", "provider", "description", "total_checks", "total_requirements", "created_at", "updated_at".
|
||||
The default parameters are "id,framework,provider,description,total_checks,total_requirements".
|
||||
If null, all fields will be returned.
|
||||
|
||||
Returns:
|
||||
List of compliance frameworks. The structure is as follows:
|
||||
{
|
||||
"count": N,
|
||||
"frameworks": {
|
||||
"framework_id": {
|
||||
"id": "framework_id",
|
||||
"provider": "provider_id",
|
||||
"description": "framework_description",
|
||||
"version": "framework_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
params = {}
|
||||
|
||||
if provider:
|
||||
params["provider"] = provider
|
||||
if fields:
|
||||
params["fields"] = fields
|
||||
|
||||
try:
|
||||
response = client.get("/compliance", params=params)
|
||||
response.raise_for_status()
|
||||
frameworks = response.json()
|
||||
|
||||
frameworks_dict = {}
|
||||
for framework in frameworks:
|
||||
framework_data = {}
|
||||
# Always include the id field as it's mandatory for the response structure
|
||||
if "id" in framework:
|
||||
framework_data["id"] = framework["id"]
|
||||
|
||||
# Include other requested fields
|
||||
for field in fields.split(","):
|
||||
if (
|
||||
field != "id" and field in framework
|
||||
): # Skip id since it's already added
|
||||
framework_data[field] = framework[field]
|
||||
frameworks_dict[framework["id"]] = framework_data
|
||||
|
||||
return {"count": len(frameworks), "frameworks": frameworks_dict}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@hub_mcp_server.tool()
|
||||
async def search_compliance_frameworks(term: str) -> dict[str, Any]:
|
||||
"""
|
||||
Search compliance frameworks by term.
|
||||
|
||||
Args:
|
||||
term: Search term to find in framework names and descriptions
|
||||
|
||||
Returns:
|
||||
List of compliance frameworks matching the search term
|
||||
"""
|
||||
try:
|
||||
response = client.get("/compliance/search", params={"term": term})
|
||||
response.raise_for_status()
|
||||
frameworks = response.json()
|
||||
|
||||
return {
|
||||
"count": len(frameworks),
|
||||
"search_term": term,
|
||||
"frameworks": frameworks,
|
||||
}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# Provider Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def list_providers() -> dict[str, Any]:
|
||||
"""
|
||||
Get all available Prowler providers and their associated services.
|
||||
|
||||
Returns:
|
||||
List of Prowler providers with their associated services. The structure is as follows:
|
||||
{
|
||||
"count": N,
|
||||
"providers": {
|
||||
"provider_id": {
|
||||
"name": "provider_name",
|
||||
"services": ["service_id_1", "service_id_2", "service_id_3", ...]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
response = client.get("/providers")
|
||||
response.raise_for_status()
|
||||
providers = response.json()
|
||||
|
||||
providers_dict = {}
|
||||
for provider in providers:
|
||||
providers_dict[provider["id"]] = {
|
||||
"name": provider.get("name", ""),
|
||||
"services": provider.get("services", []),
|
||||
}
|
||||
|
||||
return {"count": len(providers), "providers": providers_dict}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
# Analytics Tools
|
||||
@hub_mcp_server.tool()
|
||||
async def get_artifacts_count() -> dict[str, Any]:
|
||||
"""
|
||||
Get total count of security artifacts (checks + compliance frameworks).
|
||||
|
||||
Returns:
|
||||
Total number of artifacts in the Prowler Hub.
|
||||
"""
|
||||
try:
|
||||
response = client.get("/n_artifacts")
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
return {
|
||||
"total_artifacts": data.get("n", 0),
|
||||
"details": "Total count includes both security checks and compliance frameworks",
|
||||
}
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"error": f"HTTP error {e.response.status_code}: {e.response.text}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
@@ -0,0 +1,18 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
||||
# Initialize main Prowler MCP server
|
||||
prowler_mcp_server = FastMCP("prowler-mcp-server")
|
||||
|
||||
|
||||
async def setup_main_server():
|
||||
"""Set up the main Prowler MCP server with all available integrations."""
|
||||
|
||||
# Import Prowler Hub tools with prowler_hub_ prefix
|
||||
try:
|
||||
from prowler_mcp_server.prowler_hub.server import hub_mcp_server
|
||||
|
||||
await prowler_mcp_server.import_server(hub_mcp_server, prefix="prowler_hub")
|
||||
except Exception:
|
||||
# TODO: Add error logging
|
||||
pass
|
||||
@@ -0,0 +1,20 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "prowler-mcp"
|
||||
version = "0.1.0"
|
||||
description = "MCP server for Prowler ecosystem"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastmcp>=2.11.3",
|
||||
"httpx>=0.27.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
prowler-mcp = "prowler_mcp_server.main:main"
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
@@ -11,15 +11,16 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
### Changed
|
||||
- Update AWS Neptune service metadata to new format [(#8494)](https://github.com/prowler-cloud/prowler/pull/8494)
|
||||
|
||||
### Changed
|
||||
- Update AWS Config service metadata to new format [(#8641)](https://github.com/prowler-cloud/prowler/pull/8641)
|
||||
|
||||
### Fixed
|
||||
|
||||
## [v5.12.1] (Prowler v5.12.1)
|
||||
|
||||
### Fixed
|
||||
- Replaced old check id with new ones for compliance files [(#8682)](https://github.com/prowler-cloud/prowler/pull/8682)
|
||||
- `firehose_stream_encrypted_at_rest` check false positives and new api call in kafka service [(#8599)](https://github.com/prowler-cloud/prowler/pull/8599)
|
||||
- Replace defender rules policies key to use old name [(#8702)](https://github.com/prowler-cloud/prowler/pull/8702)
|
||||
|
||||
## [v5.12.0] (Prowler v5.12.0)
|
||||
|
||||
|
||||
+23
-1
@@ -3,6 +3,7 @@ from typing import List
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.providers.aws.services.firehose.firehose_client import firehose_client
|
||||
from prowler.providers.aws.services.firehose.firehose_service import EncryptionStatus
|
||||
from prowler.providers.aws.services.kafka.kafka_client import kafka_client
|
||||
from prowler.providers.aws.services.kinesis.kinesis_client import kinesis_client
|
||||
from prowler.providers.aws.services.kinesis.kinesis_service import EncryptionType
|
||||
|
||||
@@ -37,7 +38,28 @@ class firehose_stream_encrypted_at_rest(Check):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Firehose Stream {stream.name} does not have at rest encryption enabled but the source stream {source_stream.name} has at rest encryption enabled."
|
||||
|
||||
# Check if the stream has encryption enabled directly
|
||||
# MSK source - check if the MSK cluster has encryption at rest with CMK
|
||||
elif stream.delivery_stream_type == "MSKAsSource":
|
||||
msk_cluster_arn = stream.source.msk.msk_cluster_arn
|
||||
if msk_cluster_arn:
|
||||
msk_cluster = None
|
||||
for cluster in kafka_client.clusters.values():
|
||||
if cluster.arn == msk_cluster_arn:
|
||||
msk_cluster = cluster
|
||||
break
|
||||
|
||||
if msk_cluster:
|
||||
# All MSK clusters (both provisioned and serverless) always have encryption at rest enabled by AWS
|
||||
# AWS MSK always encrypts data at rest - either with AWS managed keys or CMK
|
||||
report.status = "PASS"
|
||||
if msk_cluster.kafka_version == "SERVERLESS":
|
||||
report.status_extended = f"Firehose Stream {stream.name} uses MSK serverless source which always has encryption at rest enabled by default."
|
||||
else:
|
||||
report.status_extended = f"Firehose Stream {stream.name} uses MSK provisioned source which always has encryption at rest enabled by AWS (either with AWS managed keys or CMK)."
|
||||
else:
|
||||
report.status_extended = f"Firehose Stream {stream.name} uses MSK source which always has encryption at rest enabled by AWS."
|
||||
|
||||
# Check if the stream has encryption enabled directly (DirectPut or DatabaseAsSource cases)
|
||||
elif stream.kms_encryption == EncryptionStatus.ENABLED:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Firehose Stream {stream.name} does have at rest encryption enabled."
|
||||
|
||||
+6
-1
@@ -12,7 +12,12 @@ class kafka_cluster_encryption_at_rest_uses_cmk(Check):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' does not have encryption at rest enabled with a CMK."
|
||||
|
||||
if any(
|
||||
# Serverless clusters always have encryption at rest enabled by default
|
||||
if cluster.kafka_version == "SERVERLESS":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' is serverless and always has encryption at rest enabled by default."
|
||||
# For provisioned clusters, check if they use a customer managed KMS key
|
||||
elif any(
|
||||
(
|
||||
cluster.data_volume_kms_key_id == key.arn
|
||||
and getattr(key, "manager", "") == "CUSTOMER"
|
||||
|
||||
+6
-1
@@ -13,7 +13,12 @@ class kafka_cluster_enhanced_monitoring_enabled(Check):
|
||||
f"Kafka cluster '{cluster.name}' has enhanced monitoring enabled."
|
||||
)
|
||||
|
||||
if cluster.enhanced_monitoring == "DEFAULT":
|
||||
# Serverless clusters always have enhanced monitoring enabled by default
|
||||
if cluster.kafka_version == "SERVERLESS":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' is serverless and always has enhanced monitoring enabled by default."
|
||||
# For provisioned clusters, check the enhanced monitoring configuration
|
||||
elif cluster.enhanced_monitoring == "DEFAULT":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' does not have enhanced monitoring enabled."
|
||||
|
||||
|
||||
+6
-1
@@ -11,7 +11,12 @@ class kafka_cluster_in_transit_encryption_enabled(Check):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' does not have encryption in transit enabled."
|
||||
|
||||
if (
|
||||
# Serverless clusters always have encryption in transit enabled by default
|
||||
if cluster.kafka_version == "SERVERLESS":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' is serverless and always has encryption in transit enabled by default."
|
||||
# For provisioned clusters, check the encryption configuration
|
||||
elif (
|
||||
cluster.encryption_in_transit.client_broker == "TLS"
|
||||
and cluster.encryption_in_transit.in_cluster
|
||||
):
|
||||
|
||||
+6
-1
@@ -13,7 +13,12 @@ class kafka_cluster_is_public(Check):
|
||||
f"Kafka cluster {cluster.name} is publicly accessible."
|
||||
)
|
||||
|
||||
if not cluster.public_access:
|
||||
# Serverless clusters are always private by default
|
||||
if cluster.kafka_version == "SERVERLESS":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster {cluster.name} is serverless and always private by default."
|
||||
# For provisioned clusters, check the public access configuration
|
||||
elif not cluster.public_access:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Kafka cluster {cluster.name} is not publicly accessible."
|
||||
|
||||
+6
-1
@@ -11,7 +11,12 @@ class kafka_cluster_mutual_tls_authentication_enabled(Check):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' does not have mutual TLS authentication enabled."
|
||||
|
||||
if cluster.tls_authentication:
|
||||
# Serverless clusters always have TLS authentication enabled by default
|
||||
if cluster.kafka_version == "SERVERLESS":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' is serverless and always has TLS authentication enabled by default."
|
||||
# For provisioned clusters, check the TLS configuration
|
||||
elif cluster.tls_authentication:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' has mutual TLS authentication enabled."
|
||||
|
||||
|
||||
+6
-1
@@ -13,7 +13,12 @@ class kafka_cluster_unrestricted_access_disabled(Check):
|
||||
f"Kafka cluster '{cluster.name}' has unrestricted access enabled."
|
||||
)
|
||||
|
||||
if not cluster.unauthentication_access:
|
||||
# Serverless clusters always require authentication by default
|
||||
if cluster.kafka_version == "SERVERLESS":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' is serverless and always requires authentication by default."
|
||||
# For provisioned clusters, check the unauthenticated access configuration
|
||||
elif not cluster.unauthentication_access:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' does not have unrestricted access enabled."
|
||||
|
||||
|
||||
+6
-1
@@ -13,7 +13,12 @@ class kafka_cluster_uses_latest_version(Check):
|
||||
f"Kafka cluster '{cluster.name}' is using the latest version."
|
||||
)
|
||||
|
||||
if cluster.kafka_version != kafka_client.kafka_versions[-1].version:
|
||||
# Serverless clusters don't have specific Kafka versions - AWS manages them automatically
|
||||
if cluster.kafka_version == "SERVERLESS":
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Kafka cluster '{cluster.name}' is serverless and AWS automatically manages the Kafka version."
|
||||
# For provisioned clusters, check if they're using the latest version
|
||||
elif cluster.kafka_version != kafka_client.kafka_versions[-1].version:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Kafka cluster '{cluster.name}' is not using the latest version."
|
||||
|
||||
@@ -15,61 +15,133 @@ class Kafka(AWSService):
|
||||
self.__threading_call__(self._list_kafka_versions)
|
||||
|
||||
def _list_clusters(self, regional_client):
|
||||
logger.info(f"Kafka - Listing clusters in region {regional_client.region}...")
|
||||
try:
|
||||
cluster_paginator = regional_client.get_paginator("list_clusters")
|
||||
# Use list_clusters_v2 to support both provisioned and serverless clusters
|
||||
cluster_paginator = regional_client.get_paginator("list_clusters_v2")
|
||||
logger.info(
|
||||
f"Kafka - Paginator created for region {regional_client.region}"
|
||||
)
|
||||
|
||||
for page in cluster_paginator.paginate():
|
||||
logger.info(
|
||||
f"Kafka - Processing page with {len(page.get('ClusterInfoList', []))} clusters in region {regional_client.region}"
|
||||
)
|
||||
for cluster in page["ClusterInfoList"]:
|
||||
logger.info(
|
||||
f"Kafka - Found cluster: {cluster.get('ClusterName', 'Unknown')} in region {regional_client.region}"
|
||||
)
|
||||
arn = cluster.get(
|
||||
"ClusterArn",
|
||||
f"{self.account_arn_template}/{cluster.get('ClusterName', '')}",
|
||||
)
|
||||
cluster_type = cluster.get("ClusterType", "UNKNOWN")
|
||||
|
||||
if not self.audit_resources or is_resource_filtered(
|
||||
arn, self.audit_resources
|
||||
):
|
||||
self.clusters[cluster.get("ClusterArn", "")] = Cluster(
|
||||
id=arn.split(":")[-1].split("/")[-1],
|
||||
name=cluster.get("ClusterName", ""),
|
||||
arn=arn,
|
||||
region=regional_client.region,
|
||||
tags=list(cluster.get("Tags", {})),
|
||||
state=cluster.get("State", ""),
|
||||
kafka_version=cluster.get(
|
||||
"CurrentBrokerSoftwareInfo", {}
|
||||
).get("KafkaVersion", ""),
|
||||
data_volume_kms_key_id=cluster.get("EncryptionInfo", {})
|
||||
.get("EncryptionAtRest", {})
|
||||
.get("DataVolumeKMSKeyId", ""),
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker=cluster.get("EncryptionInfo", {})
|
||||
.get("EncryptionInTransit", {})
|
||||
.get("ClientBroker", "PLAINTEXT"),
|
||||
in_cluster=cluster.get("EncryptionInfo", {})
|
||||
.get("EncryptionInTransit", {})
|
||||
.get("InCluster", False),
|
||||
),
|
||||
tls_authentication=cluster.get("ClientAuthentication", {})
|
||||
.get("Tls", {})
|
||||
.get("Enabled", False),
|
||||
public_access=cluster.get("BrokerNodeGroupInfo", {})
|
||||
.get("ConnectivityInfo", {})
|
||||
.get("PublicAccess", {})
|
||||
.get("Type", "SERVICE_PROVIDED_EIPS")
|
||||
!= "DISABLED",
|
||||
unauthentication_access=cluster.get(
|
||||
"ClientAuthentication", {}
|
||||
# Handle provisioned clusters
|
||||
if cluster_type == "PROVISIONED" and "Provisioned" in cluster:
|
||||
provisioned = cluster["Provisioned"]
|
||||
self.clusters[cluster.get("ClusterArn", "")] = Cluster(
|
||||
id=arn.split(":")[-1].split("/")[-1],
|
||||
name=cluster.get("ClusterName", ""),
|
||||
arn=arn,
|
||||
region=regional_client.region,
|
||||
tags=(
|
||||
list(cluster.get("Tags", {}).values())
|
||||
if cluster.get("Tags")
|
||||
else []
|
||||
),
|
||||
state=cluster.get("State", ""),
|
||||
kafka_version=provisioned.get(
|
||||
"CurrentBrokerSoftwareInfo", {}
|
||||
).get("KafkaVersion", ""),
|
||||
data_volume_kms_key_id=provisioned.get(
|
||||
"EncryptionInfo", {}
|
||||
)
|
||||
.get("EncryptionAtRest", {})
|
||||
.get("DataVolumeKMSKeyId", ""),
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker=provisioned.get("EncryptionInfo", {})
|
||||
.get("EncryptionInTransit", {})
|
||||
.get("ClientBroker", "PLAINTEXT"),
|
||||
in_cluster=provisioned.get("EncryptionInfo", {})
|
||||
.get("EncryptionInTransit", {})
|
||||
.get("InCluster", False),
|
||||
),
|
||||
tls_authentication=provisioned.get(
|
||||
"ClientAuthentication", {}
|
||||
)
|
||||
.get("Tls", {})
|
||||
.get("Enabled", False),
|
||||
public_access=provisioned.get("BrokerNodeGroupInfo", {})
|
||||
.get("ConnectivityInfo", {})
|
||||
.get("PublicAccess", {})
|
||||
.get("Type", "SERVICE_PROVIDED_EIPS")
|
||||
!= "DISABLED",
|
||||
unauthentication_access=provisioned.get(
|
||||
"ClientAuthentication", {}
|
||||
)
|
||||
.get("Unauthenticated", {})
|
||||
.get("Enabled", False),
|
||||
enhanced_monitoring=provisioned.get(
|
||||
"EnhancedMonitoring", "DEFAULT"
|
||||
),
|
||||
)
|
||||
.get("Unauthenticated", {})
|
||||
.get("Enabled", False),
|
||||
enhanced_monitoring=cluster.get(
|
||||
"EnhancedMonitoring", "DEFAULT"
|
||||
),
|
||||
logger.info(
|
||||
f"Kafka - Added provisioned cluster {cluster.get('ClusterName', 'Unknown')} to clusters dict"
|
||||
)
|
||||
|
||||
# Handle serverless clusters
|
||||
elif cluster_type == "SERVERLESS" and "Serverless" in cluster:
|
||||
# For serverless clusters, encryption is always enabled by default
|
||||
# We'll create a Cluster object with default encryption values
|
||||
self.clusters[cluster.get("ClusterArn", "")] = Cluster(
|
||||
id=arn.split(":")[-1].split("/")[-1],
|
||||
name=cluster.get("ClusterName", ""),
|
||||
arn=arn,
|
||||
region=regional_client.region,
|
||||
tags=(
|
||||
list(cluster.get("Tags", {}).values())
|
||||
if cluster.get("Tags")
|
||||
else []
|
||||
),
|
||||
state=cluster.get("State", ""),
|
||||
kafka_version="SERVERLESS", # Serverless doesn't have specific Kafka version
|
||||
data_volume_kms_key_id="AWS_MANAGED", # Serverless uses AWS managed keys
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS", # Serverless always has TLS enabled
|
||||
in_cluster=True, # Serverless always has in-cluster encryption
|
||||
),
|
||||
tls_authentication=True, # Serverless always has TLS authentication
|
||||
public_access=False, # Serverless clusters are always private
|
||||
unauthentication_access=False, # Serverless requires authentication
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
logger.info(
|
||||
f"Kafka - Added serverless cluster {cluster.get('ClusterName', 'Unknown')} to clusters dict"
|
||||
)
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
f"Kafka - Unknown cluster type {cluster_type} for cluster {cluster.get('ClusterName', 'Unknown')}"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Kafka - Cluster {cluster.get('ClusterName', 'Unknown')} filtered out by audit_resources"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Kafka - Total clusters found in region {regional_client.region}: {len(self.clusters)}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
logger.error(
|
||||
f"Kafka - Error details in region {regional_client.region}: {str(error)}"
|
||||
)
|
||||
|
||||
def _list_kafka_versions(self, regional_client):
|
||||
try:
|
||||
|
||||
@@ -91,7 +91,7 @@ class Defender(M365Service):
|
||||
malware_rule = [malware_rule]
|
||||
for rule in malware_rule:
|
||||
if rule:
|
||||
malware_rules[rule.get("Name", "")] = MalwareRule(
|
||||
malware_rules[rule.get("MalwareFilterPolicy", "")] = MalwareRule(
|
||||
state=rule.get("State", ""),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
@@ -152,12 +152,14 @@ class Defender(M365Service):
|
||||
antiphishing_rule = [antiphishing_rule]
|
||||
for rule in antiphishing_rule:
|
||||
if rule:
|
||||
antiphishing_rules[rule.get("Name", "")] = AntiphishingRule(
|
||||
state=rule.get("State", ""),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
antiphishing_rules[rule.get("AntiPhishPolicy", "")] = (
|
||||
AntiphishingRule(
|
||||
state=rule.get("State", ""),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
@@ -250,7 +252,9 @@ class Defender(M365Service):
|
||||
outbound_spam_rule = [outbound_spam_rule]
|
||||
for rule in outbound_spam_rule:
|
||||
if rule:
|
||||
outbound_spam_rules[rule.get("Name", "")] = OutboundSpamRule(
|
||||
outbound_spam_rules[
|
||||
rule.get("HostedOutboundSpamFilterPolicy", "")
|
||||
] = OutboundSpamRule(
|
||||
state=rule.get("State", "Disabled"),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("From", None),
|
||||
@@ -330,12 +334,14 @@ class Defender(M365Service):
|
||||
inbound_spam_rule = [inbound_spam_rule]
|
||||
for rule in inbound_spam_rule:
|
||||
if rule:
|
||||
inbound_spam_rules[rule.get("Name", "")] = InboundSpamRule(
|
||||
state=rule.get("State", "Disabled"),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
inbound_spam_rules[rule.get("HostedContentFilterPolicy", "")] = (
|
||||
InboundSpamRule(
|
||||
state=rule.get("State", "Disabled"),
|
||||
priority=rule.get("Priority", 0),
|
||||
users=rule.get("SentTo", None),
|
||||
groups=rule.get("SentToMemberOf", None),
|
||||
domains=rule.get("RecipientDomainIs", None),
|
||||
)
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
|
||||
+61
@@ -162,3 +162,64 @@ class Test_kafka_cluster_encryption_at_rest_uses_cmk:
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
def test_kafka_cluster_serverless_encryption_at_rest(self):
|
||||
kafka_client = MagicMock
|
||||
kafka_client.clusters = {
|
||||
"arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6": Cluster(
|
||||
id="6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
name="serverless-cluster-1",
|
||||
arn="arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
tags=[],
|
||||
state="ACTIVE",
|
||||
kafka_version="SERVERLESS",
|
||||
data_volume_kms_key_id="AWS_MANAGED",
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS",
|
||||
in_cluster=True,
|
||||
),
|
||||
tls_authentication=True,
|
||||
public_access=False,
|
||||
unauthentication_access=False,
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
}
|
||||
|
||||
kms_client = MagicMock
|
||||
kms_client.keys = []
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_cluster_encryption_at_rest_uses_cmk.kafka_cluster_encryption_at_rest_uses_cmk.kms_client",
|
||||
new=kms_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.kafka.kafka_cluster_encryption_at_rest_uses_cmk.kafka_cluster_encryption_at_rest_uses_cmk import (
|
||||
kafka_cluster_encryption_at_rest_uses_cmk,
|
||||
)
|
||||
|
||||
check = kafka_cluster_encryption_at_rest_uses_cmk()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Kafka cluster 'serverless-cluster-1' is serverless and always has encryption at rest enabled by default."
|
||||
)
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
+64
-10
@@ -4,7 +4,7 @@ from prowler.providers.aws.services.kafka.kafka_service import (
|
||||
Cluster,
|
||||
EncryptionInTransit,
|
||||
)
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1
|
||||
|
||||
|
||||
class Test_kafka_cluster_enhanced_monitoring_enabled:
|
||||
@@ -14,11 +14,11 @@ class Test_kafka_cluster_enhanced_monitoring_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -56,11 +56,11 @@ class Test_kafka_cluster_enhanced_monitoring_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -110,11 +110,11 @@ class Test_kafka_cluster_enhanced_monitoring_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -138,3 +138,57 @@ class Test_kafka_cluster_enhanced_monitoring_enabled:
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
def test_kafka_cluster_serverless_enhanced_monitoring(self):
|
||||
kafka_client = MagicMock
|
||||
kafka_client.clusters = {
|
||||
"arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6": Cluster(
|
||||
id="6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
name="serverless-cluster-1",
|
||||
arn="arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
tags=[],
|
||||
state="ACTIVE",
|
||||
kafka_version="SERVERLESS",
|
||||
data_volume_kms_key_id="AWS_MANAGED",
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS",
|
||||
in_cluster=True,
|
||||
),
|
||||
tls_authentication=True,
|
||||
public_access=False,
|
||||
unauthentication_access=False,
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.kafka.kafka_cluster_enhanced_monitoring_enabled.kafka_cluster_enhanced_monitoring_enabled import (
|
||||
kafka_cluster_enhanced_monitoring_enabled,
|
||||
)
|
||||
|
||||
check = kafka_cluster_enhanced_monitoring_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Kafka cluster 'serverless-cluster-1' is serverless and always has enhanced monitoring enabled by default."
|
||||
)
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
+67
-13
@@ -4,7 +4,7 @@ from prowler.providers.aws.services.kafka.kafka_service import (
|
||||
Cluster,
|
||||
EncryptionInTransit,
|
||||
)
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1
|
||||
|
||||
|
||||
class Test_kafka_cluster_in_transit_encryption_enabled:
|
||||
@@ -14,11 +14,11 @@ class Test_kafka_cluster_in_transit_encryption_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -56,11 +56,11 @@ class Test_kafka_cluster_in_transit_encryption_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -110,11 +110,11 @@ class Test_kafka_cluster_in_transit_encryption_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -164,11 +164,11 @@ class Test_kafka_cluster_in_transit_encryption_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -191,3 +191,57 @@ class Test_kafka_cluster_in_transit_encryption_enabled:
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/demo-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
def test_kafka_cluster_serverless_in_transit_encryption(self):
|
||||
kafka_client = MagicMock
|
||||
kafka_client.clusters = {
|
||||
"arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6": Cluster(
|
||||
id="6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
name="serverless-cluster-1",
|
||||
arn="arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
tags=[],
|
||||
state="ACTIVE",
|
||||
kafka_version="SERVERLESS",
|
||||
data_volume_kms_key_id="AWS_MANAGED",
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS",
|
||||
in_cluster=True,
|
||||
),
|
||||
tls_authentication=True,
|
||||
public_access=False,
|
||||
unauthentication_access=False,
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.kafka.kafka_cluster_in_transit_encryption_enabled.kafka_cluster_in_transit_encryption_enabled import (
|
||||
kafka_cluster_in_transit_encryption_enabled,
|
||||
)
|
||||
|
||||
check = kafka_cluster_in_transit_encryption_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Kafka cluster 'serverless-cluster-1' is serverless and always has encryption in transit enabled by default."
|
||||
)
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
+64
-10
@@ -4,7 +4,7 @@ from prowler.providers.aws.services.kafka.kafka_service import (
|
||||
Cluster,
|
||||
EncryptionInTransit,
|
||||
)
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1
|
||||
|
||||
|
||||
class Test_kafka_cluster_is_public:
|
||||
@@ -14,11 +14,11 @@ class Test_kafka_cluster_is_public:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -56,11 +56,11 @@ class Test_kafka_cluster_is_public:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -110,11 +110,11 @@ class Test_kafka_cluster_is_public:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -138,3 +138,57 @@ class Test_kafka_cluster_is_public:
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5"
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_kafka_cluster_serverless_public(self):
|
||||
kafka_client = MagicMock
|
||||
kafka_client.clusters = {
|
||||
"arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6": Cluster(
|
||||
id="6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
name="serverless-cluster-1",
|
||||
arn="arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
tags=[],
|
||||
state="ACTIVE",
|
||||
kafka_version="SERVERLESS",
|
||||
data_volume_kms_key_id="AWS_MANAGED",
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS",
|
||||
in_cluster=True,
|
||||
),
|
||||
tls_authentication=True,
|
||||
public_access=False,
|
||||
unauthentication_access=False,
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.kafka.kafka_cluster_is_public.kafka_cluster_is_public import (
|
||||
kafka_cluster_is_public,
|
||||
)
|
||||
|
||||
check = kafka_cluster_is_public()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Kafka cluster serverless-cluster-1 is serverless and always private by default."
|
||||
)
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
)
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
+64
-10
@@ -4,7 +4,7 @@ from prowler.providers.aws.services.kafka.kafka_service import (
|
||||
Cluster,
|
||||
EncryptionInTransit,
|
||||
)
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1
|
||||
|
||||
|
||||
class Test_kafka_cluster_mutual_tls_authentication_enabled:
|
||||
@@ -14,11 +14,11 @@ class Test_kafka_cluster_mutual_tls_authentication_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -56,11 +56,11 @@ class Test_kafka_cluster_mutual_tls_authentication_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -110,11 +110,11 @@ class Test_kafka_cluster_mutual_tls_authentication_enabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -138,3 +138,57 @@ class Test_kafka_cluster_mutual_tls_authentication_enabled:
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_kafka_cluster_serverless_mutual_tls_authentication(self):
|
||||
kafka_client = MagicMock
|
||||
kafka_client.clusters = {
|
||||
"arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6": Cluster(
|
||||
id="6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
name="serverless-cluster-1",
|
||||
arn="arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
tags=[],
|
||||
state="ACTIVE",
|
||||
kafka_version="SERVERLESS",
|
||||
data_volume_kms_key_id="AWS_MANAGED",
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS",
|
||||
in_cluster=True,
|
||||
),
|
||||
tls_authentication=True,
|
||||
public_access=False,
|
||||
unauthentication_access=False,
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.kafka.kafka_cluster_mutual_tls_authentication_enabled.kafka_cluster_mutual_tls_authentication_enabled import (
|
||||
kafka_cluster_mutual_tls_authentication_enabled,
|
||||
)
|
||||
|
||||
check = kafka_cluster_mutual_tls_authentication_enabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Kafka cluster 'serverless-cluster-1' is serverless and always has TLS authentication enabled by default."
|
||||
)
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
+64
-10
@@ -4,7 +4,7 @@ from prowler.providers.aws.services.kafka.kafka_service import (
|
||||
Cluster,
|
||||
EncryptionInTransit,
|
||||
)
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1
|
||||
|
||||
|
||||
class Test_kafka_cluster_unrestricted_access_disabled:
|
||||
@@ -14,11 +14,11 @@ class Test_kafka_cluster_unrestricted_access_disabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -56,11 +56,11 @@ class Test_kafka_cluster_unrestricted_access_disabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -110,11 +110,11 @@ class Test_kafka_cluster_unrestricted_access_disabled:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -138,3 +138,57 @@ class Test_kafka_cluster_unrestricted_access_disabled:
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
def test_kafka_cluster_serverless_unrestricted_access_disabled(self):
|
||||
kafka_client = MagicMock
|
||||
kafka_client.clusters = {
|
||||
"arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6": Cluster(
|
||||
id="6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
name="serverless-cluster-1",
|
||||
arn="arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
tags=[],
|
||||
state="ACTIVE",
|
||||
kafka_version="SERVERLESS",
|
||||
data_volume_kms_key_id="AWS_MANAGED",
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS",
|
||||
in_cluster=True,
|
||||
),
|
||||
tls_authentication=True,
|
||||
public_access=False,
|
||||
unauthentication_access=False,
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.kafka.kafka_cluster_unrestricted_access_disabled.kafka_cluster_unrestricted_access_disabled import (
|
||||
kafka_cluster_unrestricted_access_disabled,
|
||||
)
|
||||
|
||||
check = kafka_cluster_unrestricted_access_disabled()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Kafka cluster 'serverless-cluster-1' is serverless and always requires authentication by default."
|
||||
)
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_tags == []
|
||||
|
||||
+69
-10
@@ -5,7 +5,7 @@ from prowler.providers.aws.services.kafka.kafka_service import (
|
||||
EncryptionInTransit,
|
||||
KafkaVersion,
|
||||
)
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
|
||||
from tests.providers.aws.utils import AWS_REGION_US_EAST_1
|
||||
|
||||
|
||||
class Test_kafka_cluster_latest_version:
|
||||
@@ -15,11 +15,11 @@ class Test_kafka_cluster_latest_version:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -62,11 +62,11 @@ class Test_kafka_cluster_latest_version:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -121,11 +121,11 @@ class Test_kafka_cluster_latest_version:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
@@ -149,3 +149,62 @@ class Test_kafka_cluster_latest_version:
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
def test_kafka_cluster_serverless_uses_latest_version(self):
|
||||
kafka_client = MagicMock
|
||||
kafka_client.clusters = {
|
||||
"arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6": Cluster(
|
||||
id="6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
name="serverless-cluster-1",
|
||||
arn="arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
tags=[],
|
||||
state="ACTIVE",
|
||||
kafka_version="SERVERLESS",
|
||||
data_volume_kms_key_id="AWS_MANAGED",
|
||||
encryption_in_transit=EncryptionInTransit(
|
||||
client_broker="TLS",
|
||||
in_cluster=True,
|
||||
),
|
||||
tls_authentication=True,
|
||||
public_access=False,
|
||||
unauthentication_access=False,
|
||||
enhanced_monitoring="DEFAULT",
|
||||
)
|
||||
}
|
||||
|
||||
kafka_client.kafka_versions = [
|
||||
KafkaVersion(version="1.0.0", status="DEPRECATED"),
|
||||
KafkaVersion(version="2.8.0", status="ACTIVE"),
|
||||
]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_service.Kafka",
|
||||
new=kafka_client,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.aws.services.kafka.kafka_client.kafka_client",
|
||||
new=kafka_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.kafka.kafka_cluster_uses_latest_version.kafka_cluster_uses_latest_version import (
|
||||
kafka_cluster_uses_latest_version,
|
||||
)
|
||||
|
||||
check = kafka_cluster_uses_latest_version()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Kafka cluster 'serverless-cluster-1' is serverless and AWS automatically manages the Kafka version."
|
||||
)
|
||||
assert result[0].resource_id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert (
|
||||
result[0].resource_arn
|
||||
== "arn:aws:kafka:us-east-1:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
)
|
||||
assert result[0].resource_tags == []
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
|
||||
@@ -13,47 +13,67 @@ make_api_call = botocore.client.BaseClient._make_api_call
|
||||
|
||||
|
||||
def mock_make_api_call(self, operation_name, kwarg):
|
||||
if operation_name == "ListClusters":
|
||||
if operation_name == "ListClustersV2":
|
||||
return {
|
||||
"ClusterInfoList": [
|
||||
{
|
||||
"BrokerNodeGroupInfo": {
|
||||
"BrokerAZDistribution": "DEFAULT",
|
||||
"ClientSubnets": ["subnet-cbfff283", "subnet-6746046b"],
|
||||
"InstanceType": "kafka.m5.large",
|
||||
"SecurityGroups": ["sg-f839b688"],
|
||||
"StorageInfo": {"EbsStorageInfo": {"VolumeSize": 100}},
|
||||
},
|
||||
"ClusterType": "PROVISIONED",
|
||||
"ClusterArn": f"arn:aws:kafka:{AWS_REGION_US_EAST_1}:123456789012:cluster/demo-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5",
|
||||
"ClusterName": "demo-cluster-1",
|
||||
"CreationTime": "2020-07-09T02:31:36.223000+00:00",
|
||||
"CurrentBrokerSoftwareInfo": {"KafkaVersion": "2.2.1"},
|
||||
"CurrentVersion": "K3AEGXETSR30VB",
|
||||
"EncryptionInfo": {
|
||||
"EncryptionAtRest": {
|
||||
"DataVolumeKMSKeyId": f"arn:aws:kms:{AWS_REGION_US_EAST_1}:123456789012:key/a7ca56d5-0768-4b64-a670-339a9fbef81c"
|
||||
},
|
||||
"EncryptionInTransit": {
|
||||
"ClientBroker": "TLS_PLAINTEXT",
|
||||
"InCluster": True,
|
||||
},
|
||||
},
|
||||
"ClientAuthentication": {
|
||||
"Tls": {"CertificateAuthorityArnList": [], "Enabled": True},
|
||||
"Unauthenticated": {"Enabled": False},
|
||||
},
|
||||
"EnhancedMonitoring": "DEFAULT",
|
||||
"OpenMonitoring": {
|
||||
"Prometheus": {
|
||||
"JmxExporter": {"EnabledInBroker": False},
|
||||
"NodeExporter": {"EnabledInBroker": False},
|
||||
}
|
||||
},
|
||||
"NumberOfBrokerNodes": 2,
|
||||
"State": "ACTIVE",
|
||||
"Tags": {},
|
||||
"ZookeeperConnectString": f"z-2.demo-cluster-1.xuy0sb.c5.kafka.{AWS_REGION_US_EAST_1}.amazonaws.com:2181,z-1.demo-cluster-1.xuy0sb.c5.kafka.{AWS_REGION_US_EAST_1}.amazonaws.com:2181,z-3.demo-cluster-1.xuy0sb.c5.kafka.{AWS_REGION_US_EAST_1}.amazonaws.com:2181",
|
||||
}
|
||||
"Provisioned": {
|
||||
"BrokerNodeGroupInfo": {
|
||||
"BrokerAZDistribution": "DEFAULT",
|
||||
"ClientSubnets": ["subnet-cbfff283", "subnet-6746046b"],
|
||||
"InstanceType": "kafka.m5.large",
|
||||
"SecurityGroups": ["sg-f839b688"],
|
||||
"StorageInfo": {"EbsStorageInfo": {"VolumeSize": 100}},
|
||||
"ConnectivityInfo": {
|
||||
"PublicAccess": {"Type": "SERVICE_PROVIDED_EIPS"}
|
||||
},
|
||||
},
|
||||
"CurrentBrokerSoftwareInfo": {"KafkaVersion": "2.2.1"},
|
||||
"CurrentVersion": "K3AEGXETSR30VB",
|
||||
"EncryptionInfo": {
|
||||
"EncryptionAtRest": {
|
||||
"DataVolumeKMSKeyId": f"arn:aws:kms:{AWS_REGION_US_EAST_1}:123456789012:key/a7ca56d5-0768-4b64-a670-339a9fbef81c"
|
||||
},
|
||||
"EncryptionInTransit": {
|
||||
"ClientBroker": "TLS_PLAINTEXT",
|
||||
"InCluster": True,
|
||||
},
|
||||
},
|
||||
"ClientAuthentication": {
|
||||
"Tls": {"CertificateAuthorityArnList": [], "Enabled": True},
|
||||
"Unauthenticated": {"Enabled": False},
|
||||
},
|
||||
"EnhancedMonitoring": "DEFAULT",
|
||||
"OpenMonitoring": {
|
||||
"Prometheus": {
|
||||
"JmxExporter": {"EnabledInBroker": False},
|
||||
"NodeExporter": {"EnabledInBroker": False},
|
||||
}
|
||||
},
|
||||
"NumberOfBrokerNodes": 2,
|
||||
"ZookeeperConnectString": f"z-2.demo-cluster-1.xuy0sb.c5.kafka.{AWS_REGION_US_EAST_1}.amazonaws.com:2181,z-1.demo-cluster-1.xuy0sb.c5.kafka.{AWS_REGION_US_EAST_1}.amazonaws.com:2181,z-3.demo-cluster-1.xuy0sb.c5.kafka.{AWS_REGION_US_EAST_1}.amazonaws.com:2181",
|
||||
},
|
||||
},
|
||||
{
|
||||
"ClusterType": "SERVERLESS",
|
||||
"ClusterArn": f"arn:aws:kafka:{AWS_REGION_US_EAST_1}:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6",
|
||||
"ClusterName": "serverless-cluster-1",
|
||||
"State": "ACTIVE",
|
||||
"Tags": {},
|
||||
"Serverless": {
|
||||
"VpcConfigs": [
|
||||
{
|
||||
"SubnetIds": ["subnet-cbfff283", "subnet-6746046b"],
|
||||
"SecurityGroups": ["sg-f839b688"],
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
elif operation_name == "ListKafkaVersions":
|
||||
@@ -86,32 +106,53 @@ class TestKafkaService:
|
||||
assert kafka.__class__.__name__ == "Kafka"
|
||||
assert kafka.session.__class__.__name__ == "Session"
|
||||
assert kafka.audited_account == AWS_ACCOUNT_NUMBER
|
||||
# Clusters assertions
|
||||
assert len(kafka.clusters) == 1
|
||||
cluster_arn = f"arn:aws:kafka:{AWS_REGION_US_EAST_1}:123456789012:cluster/demo-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5"
|
||||
assert cluster_arn in kafka.clusters
|
||||
|
||||
# Clusters assertions - should now include both provisioned and serverless
|
||||
assert len(kafka.clusters) == 2
|
||||
|
||||
# Check provisioned cluster
|
||||
provisioned_cluster_arn = f"arn:aws:kafka:{AWS_REGION_US_EAST_1}:123456789012:cluster/demo-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5"
|
||||
assert provisioned_cluster_arn in kafka.clusters
|
||||
provisioned_cluster = kafka.clusters[provisioned_cluster_arn]
|
||||
assert provisioned_cluster.id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5"
|
||||
assert provisioned_cluster.arn == provisioned_cluster_arn
|
||||
assert provisioned_cluster.name == "demo-cluster-1"
|
||||
assert provisioned_cluster.region == AWS_REGION_US_EAST_1
|
||||
assert provisioned_cluster.tags == []
|
||||
assert provisioned_cluster.state == "ACTIVE"
|
||||
assert provisioned_cluster.kafka_version == "2.2.1"
|
||||
assert (
|
||||
kafka.clusters[cluster_arn].id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-5"
|
||||
)
|
||||
assert kafka.clusters[cluster_arn].arn == cluster_arn
|
||||
assert kafka.clusters[cluster_arn].name == "demo-cluster-1"
|
||||
assert kafka.clusters[cluster_arn].region == AWS_REGION_US_EAST_1
|
||||
assert kafka.clusters[cluster_arn].tags == []
|
||||
assert kafka.clusters[cluster_arn].state == "ACTIVE"
|
||||
assert kafka.clusters[cluster_arn].kafka_version == "2.2.1"
|
||||
assert (
|
||||
kafka.clusters[cluster_arn].data_volume_kms_key_id
|
||||
provisioned_cluster.data_volume_kms_key_id
|
||||
== f"arn:aws:kms:{AWS_REGION_US_EAST_1}:123456789012:key/a7ca56d5-0768-4b64-a670-339a9fbef81c"
|
||||
)
|
||||
assert (
|
||||
kafka.clusters[cluster_arn].encryption_in_transit.client_broker
|
||||
== "TLS_PLAINTEXT"
|
||||
provisioned_cluster.encryption_in_transit.client_broker == "TLS_PLAINTEXT"
|
||||
)
|
||||
assert kafka.clusters[cluster_arn].encryption_in_transit.in_cluster
|
||||
assert kafka.clusters[cluster_arn].enhanced_monitoring == "DEFAULT"
|
||||
assert kafka.clusters[cluster_arn].tls_authentication
|
||||
assert kafka.clusters[cluster_arn].public_access
|
||||
assert not kafka.clusters[cluster_arn].unauthentication_access
|
||||
assert provisioned_cluster.encryption_in_transit.in_cluster
|
||||
assert provisioned_cluster.enhanced_monitoring == "DEFAULT"
|
||||
assert provisioned_cluster.tls_authentication
|
||||
assert provisioned_cluster.public_access
|
||||
assert not provisioned_cluster.unauthentication_access
|
||||
|
||||
# Check serverless cluster
|
||||
serverless_cluster_arn = f"arn:aws:kafka:{AWS_REGION_US_EAST_1}:123456789012:cluster/serverless-cluster-1/6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert serverless_cluster_arn in kafka.clusters
|
||||
serverless_cluster = kafka.clusters[serverless_cluster_arn]
|
||||
assert serverless_cluster.id == "6357e0b2-0e6a-4b86-a0b4-70df934c2e31-6"
|
||||
assert serverless_cluster.arn == serverless_cluster_arn
|
||||
assert serverless_cluster.name == "serverless-cluster-1"
|
||||
assert serverless_cluster.region == AWS_REGION_US_EAST_1
|
||||
assert serverless_cluster.tags == []
|
||||
assert serverless_cluster.state == "ACTIVE"
|
||||
assert serverless_cluster.kafka_version == "SERVERLESS"
|
||||
assert serverless_cluster.data_volume_kms_key_id == "AWS_MANAGED"
|
||||
assert serverless_cluster.encryption_in_transit.client_broker == "TLS"
|
||||
assert serverless_cluster.encryption_in_transit.in_cluster
|
||||
assert serverless_cluster.enhanced_monitoring == "DEFAULT"
|
||||
assert serverless_cluster.tls_authentication
|
||||
assert not serverless_cluster.public_access
|
||||
assert not serverless_cluster.unauthentication_access
|
||||
|
||||
# Kafka versions assertions
|
||||
assert len(kafka.kafka_versions) == 2
|
||||
assert kafka.kafka_versions[0].version == "1.0.0"
|
||||
|
||||
@@ -9,6 +9,11 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- `Prowler Hub` menu item with tooltip [(#8692)] (https://github.com/prowler-cloud/prowler/pull/8692)
|
||||
- Copy link button to finding detail page [(#8685)] (https://github.com/prowler-cloud/prowler/pull/8685)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Field-level email validation message [(#8698)] (https://github.com/prowler-cloud/prowler/pull/8698)
|
||||
- POST method on auth form [(#8699)] (https://github.com/prowler-cloud/prowler/pull/8699)
|
||||
|
||||
## [1.12.0] (Prowler v5.12.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function authenticate(
|
||||
message: "Credentials error",
|
||||
errors: {
|
||||
...defaultValues,
|
||||
credentials: "Incorrect email or password",
|
||||
credentials: "Invalid email or password",
|
||||
},
|
||||
};
|
||||
case "CallbackRouteError":
|
||||
|
||||
@@ -12,7 +12,7 @@ import { authenticate, createNewUser } from "@/actions/auth";
|
||||
import { initiateSamlAuth } from "@/actions/integrations/saml";
|
||||
import { PasswordRequirementsMessage } from "@/components/auth/oss/password-validator";
|
||||
import { SocialButtons } from "@/components/auth/oss/social-buttons";
|
||||
import { NotificationIcon, ProwlerExtended } from "@/components/icons";
|
||||
import { ProwlerExtended } from "@/components/icons";
|
||||
import { ThemeSwitch } from "@/components/ThemeSwitch";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
@@ -65,6 +65,8 @@ export const AuthForm = ({
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onSubmit",
|
||||
reValidateMode: "onSubmit",
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
@@ -111,10 +113,11 @@ export const AuthForm = ({
|
||||
if (result?.message === "Success") {
|
||||
router.push("/");
|
||||
} else if (result?.errors && "credentials" in result.errors) {
|
||||
form.setError("email", {
|
||||
type: "server",
|
||||
message: result.errors.credentials ?? "Incorrect email or password",
|
||||
});
|
||||
const message =
|
||||
result.errors.credentials ?? "Invalid email or password";
|
||||
|
||||
form.setError("email", { type: "server", message });
|
||||
form.setError("password", { type: "server", message });
|
||||
} else if (result?.message === "User email is not verified") {
|
||||
router.push("/email-verification");
|
||||
} else {
|
||||
@@ -206,6 +209,8 @@ export const AuthForm = ({
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
noValidate
|
||||
method="post"
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
@@ -237,7 +242,8 @@ export const AuthForm = ({
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
isInvalid={!!form.formState.errors.email}
|
||||
showFormMessage={type !== "sign-in"}
|
||||
// Always show field validation message, including on sign-in
|
||||
showFormMessage
|
||||
/>
|
||||
{!isSamlMode && (
|
||||
<>
|
||||
@@ -245,10 +251,8 @@ export const AuthForm = ({
|
||||
control={form.control}
|
||||
name="password"
|
||||
password
|
||||
isInvalid={
|
||||
!!form.formState.errors.password ||
|
||||
!!form.formState.errors.email
|
||||
}
|
||||
// Only mark invalid when the password field has an error
|
||||
isInvalid={!!form.formState.errors.password}
|
||||
/>
|
||||
{type === "sign-up" && (
|
||||
<PasswordRequirementsMessage
|
||||
@@ -319,12 +323,7 @@ export const AuthForm = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{type === "sign-in" && form.formState.errors?.email && (
|
||||
<div className="flex flex-row items-center text-system-error">
|
||||
<NotificationIcon size={16} />
|
||||
<p className="text-small">Invalid email or password</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={type === "sign-in" ? "Log in" : "Sign up"}
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
[
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@hookform/resolvers",
|
||||
"from": "3.10.0",
|
||||
"to": "3.10.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/langgraph-supervisor",
|
||||
"from": "0.0.12",
|
||||
"to": "0.0.12",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/openai",
|
||||
"from": "0.6.9",
|
||||
"to": "0.6.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@next/third-parties",
|
||||
"from": "15.3.5",
|
||||
"to": "15.3.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@nextui-org/react",
|
||||
"from": "2.4.8",
|
||||
"to": "2.4.8",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@nextui-org/system",
|
||||
"from": "2.2.1",
|
||||
"to": "2.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@nextui-org/theme",
|
||||
"from": "2.2.5",
|
||||
"to": "2.2.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-alert-dialog",
|
||||
"from": "1.1.14",
|
||||
"to": "1.1.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-dialog",
|
||||
"from": "1.1.14",
|
||||
"to": "1.1.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-dropdown-menu",
|
||||
"from": "2.1.15",
|
||||
"to": "2.1.15",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-icons",
|
||||
"from": "1.3.2",
|
||||
"to": "1.3.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-label",
|
||||
"from": "2.1.7",
|
||||
"to": "2.1.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-select",
|
||||
"from": "2.2.5",
|
||||
"to": "2.2.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-slot",
|
||||
"from": "1.2.3",
|
||||
"to": "1.2.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@radix-ui/react-toast",
|
||||
"from": "1.2.14",
|
||||
"to": "1.2.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@react-aria/ssr",
|
||||
"from": "3.9.4",
|
||||
"to": "3.9.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@react-aria/visually-hidden",
|
||||
"from": "3.8.12",
|
||||
"to": "3.8.12",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@tailwindcss/typography",
|
||||
"from": "0.5.16",
|
||||
"to": "0.5.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@tanstack/react-table",
|
||||
"from": "8.21.3",
|
||||
"to": "8.21.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@types/js-yaml",
|
||||
"from": "4.0.9",
|
||||
"to": "4.0.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "ai",
|
||||
"from": "4.3.16",
|
||||
"to": "4.3.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "alert",
|
||||
"from": "6.0.2",
|
||||
"to": "6.0.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "class-variance-authority",
|
||||
"from": "0.7.1",
|
||||
"to": "0.7.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "clsx",
|
||||
"from": "2.1.1",
|
||||
"to": "2.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "date-fns",
|
||||
"from": "4.1.0",
|
||||
"to": "4.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "framer-motion",
|
||||
"from": "11.18.2",
|
||||
"to": "11.18.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "intl-messageformat",
|
||||
"from": "10.7.16",
|
||||
"to": "10.7.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "jose",
|
||||
"from": "5.10.0",
|
||||
"to": "5.10.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "js-yaml",
|
||||
"from": "4.1.0",
|
||||
"to": "4.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "jwt-decode",
|
||||
"from": "4.0.0",
|
||||
"to": "4.0.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "lucide-react",
|
||||
"from": "0.543.0",
|
||||
"to": "0.543.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "marked",
|
||||
"from": "15.0.12",
|
||||
"to": "15.0.12",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "next",
|
||||
"from": "14.2.32",
|
||||
"to": "14.2.32",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "next-auth",
|
||||
"from": "5.0.0-beta.29",
|
||||
"to": "5.0.0-beta.29",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "next-themes",
|
||||
"from": "0.2.1",
|
||||
"to": "0.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "radix-ui",
|
||||
"from": "1.4.2",
|
||||
"to": "1.4.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react",
|
||||
"from": "18.3.1",
|
||||
"to": "18.3.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react-dom",
|
||||
"from": "18.3.1",
|
||||
"to": "18.3.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react-hook-form",
|
||||
"from": "7.62.0",
|
||||
"to": "7.62.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react-markdown",
|
||||
"from": "10.1.0",
|
||||
"to": "10.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "recharts",
|
||||
"from": "2.15.4",
|
||||
"to": "2.15.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "rss-parser",
|
||||
"from": "3.13.0",
|
||||
"to": "3.13.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "server-only",
|
||||
"from": "0.0.1",
|
||||
"to": "0.0.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "shadcn",
|
||||
"from": "3.2.1",
|
||||
"to": "3.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "sharp",
|
||||
"from": "0.33.5",
|
||||
"to": "0.33.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "tailwind-merge",
|
||||
"from": "3.3.1",
|
||||
"to": "3.3.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "tailwindcss-animate",
|
||||
"from": "1.0.7",
|
||||
"to": "1.0.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "uuid",
|
||||
"from": "11.1.0",
|
||||
"to": "11.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "zod",
|
||||
"from": "3.25.73",
|
||||
"to": "3.25.73",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "zustand",
|
||||
"from": "4.5.7",
|
||||
"to": "4.5.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@iconify/react",
|
||||
"from": "5.2.1",
|
||||
"to": "5.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@playwright/test",
|
||||
"from": "1.53.2",
|
||||
"to": "1.53.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/node",
|
||||
"from": "20.5.7",
|
||||
"to": "20.5.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/react",
|
||||
"from": "18.3.3",
|
||||
"to": "18.3.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/react-dom",
|
||||
"from": "18.3.0",
|
||||
"to": "18.3.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/uuid",
|
||||
"from": "10.0.0",
|
||||
"to": "10.0.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@typescript-eslint/eslint-plugin",
|
||||
"from": "7.18.0",
|
||||
"to": "7.18.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@typescript-eslint/parser",
|
||||
"from": "7.18.0",
|
||||
"to": "7.18.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "autoprefixer",
|
||||
"from": "10.4.19",
|
||||
"to": "10.4.19",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint",
|
||||
"from": "8.57.1",
|
||||
"to": "8.57.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-config-next",
|
||||
"from": "14.2.32",
|
||||
"to": "14.2.32",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-config-prettier",
|
||||
"from": "10.1.5",
|
||||
"to": "10.1.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-import",
|
||||
"from": "2.32.0",
|
||||
"to": "2.32.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-jsx-a11y",
|
||||
"from": "6.10.2",
|
||||
"to": "6.10.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-node",
|
||||
"from": "11.1.0",
|
||||
"to": "11.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-prettier",
|
||||
"from": "5.5.1",
|
||||
"to": "5.5.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-react",
|
||||
"from": "7.37.5",
|
||||
"to": "7.37.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-react-hooks",
|
||||
"from": "4.6.2",
|
||||
"to": "4.6.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-security",
|
||||
"from": "3.0.1",
|
||||
"to": "3.0.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-simple-import-sort",
|
||||
"from": "12.1.1",
|
||||
"to": "12.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-plugin-unused-imports",
|
||||
"from": "3.2.0",
|
||||
"to": "3.2.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "husky",
|
||||
"from": "9.1.7",
|
||||
"to": "9.1.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "lint-staged",
|
||||
"from": "15.5.2",
|
||||
"to": "15.5.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "postcss",
|
||||
"from": "8.4.38",
|
||||
"to": "8.4.38",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "prettier",
|
||||
"from": "3.6.2",
|
||||
"to": "3.6.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "prettier-plugin-tailwindcss",
|
||||
"from": "0.6.13",
|
||||
"to": "0.6.13",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "tailwind-variants",
|
||||
"from": "0.1.20",
|
||||
"to": "0.1.20",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "tailwindcss",
|
||||
"from": "3.4.3",
|
||||
"to": "3.4.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "typescript",
|
||||
"from": "5.5.4",
|
||||
"to": "5.5.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
}
|
||||
]
|
||||
@@ -1,4 +1,3 @@
|
||||
import { produce } from "immer";
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
|
||||
@@ -11,7 +10,6 @@ type SidebarStore = {
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
setIsHover: (isHover: boolean) => void;
|
||||
getOpenState: () => boolean;
|
||||
setSettings: (settings: Partial<SidebarSettings>) => void;
|
||||
};
|
||||
|
||||
export const useSidebar = create(
|
||||
@@ -33,13 +31,6 @@ export const useSidebar = create(
|
||||
const state = get();
|
||||
return state.isOpen || (state.settings.isHoverOpen && state.isHover);
|
||||
},
|
||||
setSettings: (settings: Partial<SidebarSettings>) => {
|
||||
set(
|
||||
produce((state: SidebarStore) => {
|
||||
state.settings = { ...state.settings, ...settings };
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "sidebar",
|
||||
|
||||
Generated
+2587
-618
File diff suppressed because it is too large
Load Diff
+85
-89
@@ -1,93 +1,4 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@langchain/langgraph-supervisor": "^0.0.12",
|
||||
"@langchain/openai": "^0.6.9",
|
||||
"@next/third-parties": "^15.3.3",
|
||||
"@nextui-org/react": "2.4.8",
|
||||
"@nextui-org/system": "2.2.1",
|
||||
"@nextui-org/theme": "2.2.5",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@react-aria/ssr": "3.9.4",
|
||||
"@react-aria/visually-hidden": "3.8.12",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"add": "^2.0.6",
|
||||
"ai": "^4.3.16",
|
||||
"alert": "^6.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.16.0",
|
||||
"immer": "^10.1.1",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"jose": "^5.9.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.471.0",
|
||||
"marked": "^15.0.12",
|
||||
"next": "^14.2.32",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"radix-ui": "^1.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^2.15.2",
|
||||
"rss-parser": "^3.13.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn-ui": "^0.2.3",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^5.2.0",
|
||||
"@playwright/test": "^1.53.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.10.0",
|
||||
"@typescript-eslint/parser": "^7.10.0",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.2.32",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.7",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||
"tailwind-variants": "0.1.20",
|
||||
"tailwindcss": "3.4.3",
|
||||
"typescript": "5.5.4",
|
||||
"usehooks-ts": "^3.1.0"
|
||||
},
|
||||
"name": "prowler-next-app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -95,6 +6,8 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start:standalone": "node .next/standalone/server.js",
|
||||
"deps:log": "node scripts/update-dependency-log.js",
|
||||
"postinstall": "node -e \"const fs=require('fs'); if(fs.existsSync('scripts/update-dependency-log.js')) require('./scripts/update-dependency-log.js'); else console.log('skip deps:log (script missing)');\"",
|
||||
"typecheck": "tsc",
|
||||
"healthcheck": "npm run typecheck && npm run lint:check",
|
||||
"lint:check": "eslint . --ext .ts,.tsx -c .eslintrc.cjs",
|
||||
@@ -109,6 +22,89 @@
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"test:e2e:install": "playwright install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "3.10.0",
|
||||
"@langchain/langgraph-supervisor": "0.0.12",
|
||||
"@langchain/openai": "0.6.9",
|
||||
"@next/third-parties": "15.3.5",
|
||||
"@nextui-org/react": "2.4.8",
|
||||
"@nextui-org/system": "2.2.1",
|
||||
"@nextui-org/theme": "2.2.5",
|
||||
"@radix-ui/react-alert-dialog": "1.1.14",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.15",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-label": "2.1.7",
|
||||
"@radix-ui/react-select": "2.2.5",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-toast": "1.2.14",
|
||||
"@react-aria/ssr": "3.9.4",
|
||||
"@react-aria/visually-hidden": "3.8.12",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"ai": "4.3.16",
|
||||
"alert": "6.0.2",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"framer-motion": "11.18.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
"jose": "5.10.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"lucide-react": "0.543.0",
|
||||
"marked": "15.0.12",
|
||||
"next": "14.2.32",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"next-themes": "0.2.1",
|
||||
"radix-ui": "1.4.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "7.62.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"recharts": "2.15.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"server-only": "0.0.1",
|
||||
"shadcn": "3.2.1",
|
||||
"sharp": "0.33.5",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"uuid": "11.1.0",
|
||||
"zod": "3.25.73",
|
||||
"zustand": "4.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "5.2.1",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-next": "14.2.32",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "5.5.1",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-security": "3.0.1",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"eslint-plugin-unused-imports": "3.2.0",
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "15.5.2",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-tailwindcss": "0.6.13",
|
||||
"tailwind-variants": "0.1.20",
|
||||
"tailwindcss": "3.4.3",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"overrides": {
|
||||
"@react-types/shared": "3.26.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function readJSON(p) {
|
||||
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
}
|
||||
|
||||
function getInstalledVersion(pkgName) {
|
||||
try {
|
||||
const parts = pkgName.split('/');
|
||||
const pkgPath = path.join('node_modules', ...parts, 'package.json');
|
||||
const meta = readJSON(pkgPath);
|
||||
return meta.version;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function collect(sectionName, obj) {
|
||||
if (!obj) return [];
|
||||
return Object.entries(obj).map(([name, declared]) => {
|
||||
const installed = getInstalledVersion(name);
|
||||
return {
|
||||
section: sectionName,
|
||||
name,
|
||||
from: declared,
|
||||
to: installed || null,
|
||||
strategy: 'installed',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
// If node_modules is missing, skip to avoid generating noisy diffs
|
||||
if (!fs.existsSync('node_modules')) {
|
||||
console.log('Skip: node_modules not found. Run npm install first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pkg = readJSON('package.json');
|
||||
const entries = [
|
||||
...collect('dependencies', pkg.dependencies),
|
||||
...collect('devDependencies', pkg.devDependencies),
|
||||
];
|
||||
|
||||
// Stable sort by section then name
|
||||
entries.sort((a, b) =>
|
||||
a.section === b.section ? a.name.localeCompare(b.name) : a.section.localeCompare(b.section)
|
||||
);
|
||||
|
||||
const outPath = path.join(process.cwd(), 'dependency-log.json');
|
||||
// Merge with previous to preserve generatedAt when unchanged
|
||||
let prevMap = new Map();
|
||||
if (fs.existsSync(outPath)) {
|
||||
try {
|
||||
const prev = JSON.parse(fs.readFileSync(outPath, 'utf8'));
|
||||
for (const e of prev) {
|
||||
prevMap.set(`${e.section}::${e.name}`, e);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const merged = entries.map((e) => {
|
||||
const key = `${e.section}::${e.name}`;
|
||||
const prev = prevMap.get(key);
|
||||
if (!prev) {
|
||||
// New entry: keep declared as from
|
||||
return { ...e, generatedAt: now };
|
||||
}
|
||||
|
||||
// If installed version changed, set from to previous installed version
|
||||
if (prev.to !== e.to) {
|
||||
return { ...e, from: prev.to, generatedAt: now };
|
||||
}
|
||||
|
||||
// Otherwise preserve previous 'from' and timestamp
|
||||
return { ...e, from: prev.from, generatedAt: prev.generatedAt || now };
|
||||
});
|
||||
|
||||
const nextContent = JSON.stringify(merged, null, 2) + '\n';
|
||||
if (fs.existsSync(outPath)) {
|
||||
try {
|
||||
const prevContent = fs.readFileSync(outPath, 'utf8');
|
||||
if (prevContent === nextContent) {
|
||||
console.log(`No changes for ${outPath} (entries: ${entries.length}).`);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
fs.writeFileSync(outPath, nextContent);
|
||||
console.log(`Updated ${outPath} with ${entries.length} entries.`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -48,22 +48,20 @@ test.describe("Login Flow", () => {
|
||||
test("should handle empty form submission", async ({ page }) => {
|
||||
// Submit empty form
|
||||
await submitLoginForm(page);
|
||||
await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS);
|
||||
await verifyLoginError(page, ERROR_MESSAGES.INVALID_EMAIL);
|
||||
|
||||
// Verify we're still on login page
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
});
|
||||
|
||||
/*
|
||||
TODO: This test is failing, need UI work before.
|
||||
test("should validate email format", async ({ page }) => {
|
||||
// Attempt login with invalid email format
|
||||
await login(page, TEST_CREDENTIALS.INVALID_EMAIL_FORMAT);
|
||||
// Verify error message (application shows generic error for invalid email format too)
|
||||
await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS);
|
||||
// Verify field-level email validation message
|
||||
await verifyLoginError(page, ERROR_MESSAGES.INVALID_EMAIL);
|
||||
// Verify we're still on login page
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
});
|
||||
*/
|
||||
|
||||
test("should toggle SAML SSO mode", async ({ page }) => {
|
||||
// Toggle to SAML mode
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ import { Page, expect } from "@playwright/test";
|
||||
|
||||
export const ERROR_MESSAGES = {
|
||||
INVALID_CREDENTIALS: "Invalid email or password",
|
||||
INVALID_EMAIL: "Please enter a valid email address.",
|
||||
} as const;
|
||||
|
||||
export const URLS = {
|
||||
@@ -69,7 +70,8 @@ export async function verifyLoginError(
|
||||
page: Page,
|
||||
errorMessage = "Invalid email or password",
|
||||
) {
|
||||
await expect(page.getByText(errorMessage)).toBeVisible();
|
||||
// There may be multiple field-level errors with the same text; assert at least one is visible
|
||||
await expect(page.getByText(errorMessage).first()).toBeVisible();
|
||||
await expect(page).toHaveURL("/sign-in");
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,12 @@ export const authFormSchema = (type: string) =>
|
||||
}),
|
||||
|
||||
// Fields for Sign In and Sign Up
|
||||
email: z.string().email(),
|
||||
// Trim and normalize email, and provide consistent message
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.email({ message: "Please enter a valid email address." }),
|
||||
password: type === "sign-in" ? z.string() : validatePassword(),
|
||||
isSamlMode: z.boolean().optional(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user