Merge branch 'master' into todo-check-class

This commit is contained in:
OlmeNav
2025-09-12 10:50:16 +02:00
committed by HugoPBrito
39 changed files with 4973 additions and 920 deletions
+151
View File
@@ -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
+88
View File
@@ -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: Fulltext 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 Desktops 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 repositorys main license. See the [LICENSE](../LICENSE) file at the repository root.
+12
View File
@@ -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"]
+20
View File
@@ -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)}
+18
View File
@@ -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
+20
View File
@@ -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
+3 -2
View File
@@ -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)
@@ -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."
@@ -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"
@@ -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."
@@ -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
):
@@ -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."
@@ -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."
@@ -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."
@@ -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(
@@ -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
@@ -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
@@ -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 == []
@@ -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 == []
@@ -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 == []
@@ -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 == []
@@ -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"
+1
View File
@@ -1 +1,2 @@
package-lock=true
save-exact=true
+5
View File
@@ -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
+1 -1
View File
@@ -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":
+15 -16
View File
@@ -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"}
+634
View File
@@ -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"
}
]
-9
View File
@@ -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",
+2587 -618
View File
File diff suppressed because it is too large Load Diff
+85 -89
View File
@@ -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"
},
+95
View File
@@ -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();
+4 -6
View File
@@ -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
View File
@@ -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");
}
+6 -1
View File
@@ -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(),
})