mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
334 lines
11 KiB
Python
334 lines
11 KiB
Python
"""Pydantic models for simplified security findings responses."""
|
|
|
|
from typing import Literal
|
|
|
|
from prowler_mcp_server.prowler_app.models.base import MinimalSerializerMixin
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class CheckRemediation(MinimalSerializerMixin, BaseModel):
|
|
"""Remediation information for a security check."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
cli: str | None = Field(
|
|
default=None,
|
|
description="Command-line interface commands for remediation",
|
|
)
|
|
terraform: str | None = Field(
|
|
default=None,
|
|
description="Terraform code snippet with best practices for remediation",
|
|
)
|
|
recommendation_text: str | None = Field(
|
|
default=None, description="Text description with best practices"
|
|
)
|
|
recommendation_url: str | None = Field(
|
|
default=None,
|
|
description="URL to external remediation documentation",
|
|
)
|
|
|
|
|
|
class CheckMetadata(MinimalSerializerMixin, BaseModel):
|
|
"""Essential metadata for a security check."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
check_id: str = Field(
|
|
description="Unique provider identifier for the security check (e.g., 's3_bucket_public_access')",
|
|
)
|
|
title: str = Field(
|
|
description="Human-readable title of the security check",
|
|
)
|
|
description: str = Field(
|
|
description="Detailed description of what the check validates",
|
|
)
|
|
provider: str = Field(
|
|
description="Prowler provider this check belongs to (e.g., 'aws', 'azure', 'gcp')",
|
|
)
|
|
service: str = Field(
|
|
description="Prowler service being checked (e.g., 's3', 'ec2', 'keyvault')",
|
|
)
|
|
resource_type: str = Field(
|
|
description="Type of resource being evaluated (e.g., 'AwsS3Bucket')",
|
|
)
|
|
risk: str | None = Field(
|
|
default=None,
|
|
description="Risk description if the check fails",
|
|
)
|
|
remediation: CheckRemediation | None = Field(
|
|
default=None,
|
|
description="Remediation guidance including CLI commands and recommendations",
|
|
)
|
|
related_url: str | None = Field(
|
|
default=None,
|
|
description="URL to additional documentation or references",
|
|
)
|
|
categories: list[str] = Field(
|
|
default_factory=list,
|
|
description="Categories this check belongs to (e.g., ['encryption', 'logging'])",
|
|
)
|
|
|
|
@classmethod
|
|
def from_api_response(cls, data: dict) -> "CheckMetadata":
|
|
"""Transform API check_metadata to simplified format."""
|
|
remediation_data = data.get("remediation")
|
|
|
|
remediation = None
|
|
if remediation_data:
|
|
code = remediation_data.get("code", {})
|
|
recommendation = remediation_data.get("recommendation", {})
|
|
|
|
remediation = CheckRemediation(
|
|
cli=code.get("cli"),
|
|
terraform=code.get("terraform"),
|
|
recommendation_text=recommendation.get("text"),
|
|
recommendation_url=recommendation.get("url"),
|
|
)
|
|
|
|
return cls(
|
|
check_id=data["checkid"],
|
|
title=data["checktitle"],
|
|
description=data["description"],
|
|
provider=data["provider"],
|
|
risk=data.get("risk"),
|
|
service=data["servicename"],
|
|
resource_type=data["resourcetype"],
|
|
remediation=remediation,
|
|
related_url=data.get("relatedurl"),
|
|
categories=data.get("categories", []),
|
|
)
|
|
|
|
|
|
class SimplifiedFinding(MinimalSerializerMixin, BaseModel):
|
|
"""Simplified security finding with only LLM-relevant information."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
id: str = Field(
|
|
description="Unique UUIDv4 identifier for this finding in Prowler database"
|
|
)
|
|
uid: str = Field(
|
|
description="Human-readable unique identifier assigned by Prowler. Format: prowler-{provider}-{check_id}-{account_uid}-{region}-{resource_name}",
|
|
)
|
|
status: Literal["FAIL", "PASS", "MANUAL"] = Field(
|
|
description="Result status: FAIL (security issue found), PASS (no issue), MANUAL (requires manual verification)",
|
|
)
|
|
severity: Literal["critical", "high", "medium", "low", "informational"] = Field(
|
|
description="Severity level of the finding",
|
|
)
|
|
check_metadata: CheckMetadata = Field(
|
|
description="Metadata about the security check that generated this finding",
|
|
)
|
|
status_extended: str = Field(
|
|
description="Extended status information providing additional context",
|
|
)
|
|
delta: Literal["new", "changed"] = Field(
|
|
description="Change status: 'new' (not seen before), 'changed' (modified since last scan), or None (unchanged)",
|
|
)
|
|
muted: bool = Field(
|
|
description="Whether this finding has been muted/suppressed by the user",
|
|
)
|
|
muted_reason: str = Field(
|
|
default=None,
|
|
description="Reason provided when muting this finding (3-500 chars if muted)",
|
|
)
|
|
|
|
@classmethod
|
|
def from_api_response(cls, data: dict) -> "SimplifiedFinding":
|
|
"""Transform JSON:API finding response to simplified format."""
|
|
attributes = data["attributes"]
|
|
check_metadata = attributes["check_metadata"]
|
|
|
|
return cls(
|
|
id=data["id"],
|
|
uid=attributes["uid"],
|
|
status=attributes["status"],
|
|
severity=attributes["severity"],
|
|
check_metadata=CheckMetadata.from_api_response(check_metadata),
|
|
status_extended=attributes["status_extended"],
|
|
delta=attributes["delta"],
|
|
muted=attributes["muted"],
|
|
muted_reason=attributes["muted_reason"],
|
|
)
|
|
|
|
|
|
class DetailedFinding(SimplifiedFinding):
|
|
"""Detailed security finding with comprehensive information for deep analysis.
|
|
|
|
Extends SimplifiedFinding with temporal metadata and relationships to scans and resources.
|
|
Use this when you need complete context about a specific finding.
|
|
"""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
inserted_at: str = Field(
|
|
description="ISO 8601 timestamp when this finding was first inserted into the database",
|
|
)
|
|
updated_at: str = Field(
|
|
description="ISO 8601 timestamp when this finding was last updated",
|
|
)
|
|
first_seen_at: str | None = Field(
|
|
default=None,
|
|
description="ISO 8601 timestamp when this finding was first detected across all scans",
|
|
)
|
|
scan_id: str | None = Field(
|
|
default=None,
|
|
description="UUID of the scan that generated this finding",
|
|
)
|
|
resource_ids: list[str] = Field(
|
|
default_factory=list,
|
|
description="List of UUIDs for cloud resources associated with this finding",
|
|
)
|
|
|
|
@classmethod
|
|
def from_api_response(cls, data: dict) -> "DetailedFinding":
|
|
"""Transform JSON:API finding response to detailed format."""
|
|
attributes = data["attributes"]
|
|
check_metadata = attributes["check_metadata"]
|
|
relationships = data.get("relationships", {})
|
|
|
|
# Parse scan relationship
|
|
scan_id = None
|
|
scan_data = relationships.get("scan", {}).get("data")
|
|
if scan_data:
|
|
scan_id = scan_data["id"]
|
|
|
|
# Parse resources relationship
|
|
resource_ids = []
|
|
resources_data = relationships.get("resources", {}).get("data", [])
|
|
if resources_data:
|
|
resource_ids = [r["id"] for r in resources_data]
|
|
|
|
return cls(
|
|
id=data["id"],
|
|
uid=attributes["uid"],
|
|
status=attributes["status"],
|
|
severity=attributes["severity"],
|
|
check_metadata=CheckMetadata.from_api_response(check_metadata),
|
|
status_extended=attributes.get("status_extended"),
|
|
delta=attributes.get("delta"),
|
|
muted=attributes["muted"],
|
|
muted_reason=attributes.get("muted_reason"),
|
|
inserted_at=attributes["inserted_at"],
|
|
updated_at=attributes["updated_at"],
|
|
first_seen_at=attributes.get("first_seen_at"),
|
|
scan_id=scan_id,
|
|
resource_ids=resource_ids,
|
|
)
|
|
|
|
|
|
class FindingsListResponse(BaseModel):
|
|
"""Simplified response for findings list queries."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
findings: list[SimplifiedFinding] = Field(
|
|
description="List of security findings matching the query",
|
|
)
|
|
total_num_finding: int = Field(
|
|
description="Total number of findings matching the query across all pages",
|
|
ge=0,
|
|
)
|
|
total_num_pages: int = Field(
|
|
description="Total number of pages available",
|
|
ge=0,
|
|
)
|
|
current_page: int = Field(
|
|
description="Current page number (1-indexed)",
|
|
ge=1,
|
|
)
|
|
|
|
@classmethod
|
|
def from_api_response(cls, response: dict) -> "FindingsListResponse":
|
|
"""Transform JSON:API response to simplified format."""
|
|
data = response["data"]
|
|
meta = response["meta"]
|
|
pagination = meta["pagination"]
|
|
|
|
findings = [SimplifiedFinding.from_api_response(item) for item in data]
|
|
|
|
return cls(
|
|
findings=findings,
|
|
total_num_finding=pagination["count"],
|
|
total_num_pages=pagination["pages"],
|
|
current_page=pagination["page"],
|
|
)
|
|
|
|
|
|
class FindingsOverview(BaseModel):
|
|
"""Simplified findings overview with aggregate statistics."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
total: int = Field(
|
|
description="Total number of findings",
|
|
ge=0,
|
|
)
|
|
fail: int = Field(
|
|
description="Total number of failed security checks",
|
|
ge=0,
|
|
)
|
|
passed: int = ( # Using 'passed' instead of 'pass' since 'pass' is a Python keyword
|
|
Field(
|
|
description="Total number of passed security checks",
|
|
ge=0,
|
|
)
|
|
)
|
|
muted: int = Field(
|
|
description="Total number of muted findings",
|
|
ge=0,
|
|
)
|
|
new: int = Field(
|
|
description="Total number of new findings (not seen in previous scans)",
|
|
ge=0,
|
|
)
|
|
changed: int = Field(
|
|
description="Total number of changed findings (modified since last scan)",
|
|
ge=0,
|
|
)
|
|
fail_new: int = Field(
|
|
description="Number of new findings with FAIL status",
|
|
ge=0,
|
|
)
|
|
fail_changed: int = Field(
|
|
description="Number of changed findings with FAIL status",
|
|
ge=0,
|
|
)
|
|
pass_new: int = Field(
|
|
description="Number of new findings with PASS status",
|
|
ge=0,
|
|
)
|
|
pass_changed: int = Field(
|
|
description="Number of changed findings with PASS status",
|
|
ge=0,
|
|
)
|
|
muted_new: int = Field(
|
|
description="Number of new muted findings",
|
|
ge=0,
|
|
)
|
|
muted_changed: int = Field(
|
|
description="Number of changed muted findings",
|
|
ge=0,
|
|
)
|
|
|
|
@classmethod
|
|
def from_api_response(cls, response: dict) -> "FindingsOverview":
|
|
"""Transform JSON:API overview response to simplified format."""
|
|
data = response["data"]
|
|
attributes = data["attributes"]
|
|
|
|
return cls(
|
|
total=attributes["total"],
|
|
fail=attributes["fail"],
|
|
passed=attributes["pass"],
|
|
muted=attributes["muted"],
|
|
new=attributes["new"],
|
|
changed=attributes["changed"],
|
|
fail_new=attributes["fail_new"],
|
|
fail_changed=attributes["fail_changed"],
|
|
pass_new=attributes["pass_new"],
|
|
pass_changed=attributes["pass_changed"],
|
|
muted_new=attributes["muted_new"],
|
|
muted_changed=attributes["muted_changed"],
|
|
)
|