Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/app/api/v1/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
description="Collect MFA enrollment and identity management evidence.",
collector_types=["okta_mfa_enrollment"],
),
ProviderInfo(
provider="prowler",
name="Prowler Security Scanner",
description="Run comprehensive security assessments against AWS, Azure, and GCP. Maps findings to CIS, SOC 2, HIPAA, PCI DSS, and more.",
collector_types=["prowler_aws_full_scan", "prowler_aws_service_scan", "prowler_aws_compliance_scan"],
),
]


Expand Down
84 changes: 84 additions & 0 deletions backend/app/api/v1/prowler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Prowler security scanner API endpoints."""
from uuid import UUID

from fastapi import APIRouter, Query

from app.core.audit_middleware import log_audit
from app.core.dependencies import DB, ComplianceUser, AnyInternalUser, VerifiedOrgId
from app.schemas.prowler import (
ProwlerScanTrigger,
ProwlerScanResultResponse,
ProwlerCompliancePosture,
ProwlerFindingSummary,
)
from app.services import prowler_service

router = APIRouter(
prefix="/organizations/{org_id}/prowler",
tags=["prowler"],
)


@router.post("/scan", response_model=dict, status_code=201)
async def trigger_scan(
org_id: VerifiedOrgId, data: ProwlerScanTrigger, db: DB, current_user: ComplianceUser
):
"""Trigger a Prowler security scan."""
job = await prowler_service.trigger_scan(db, org_id, data)
await log_audit(db, current_user, "trigger_prowler_scan", "prowler", str(job.id), org_id)
return {
"job_id": str(job.id),
"status": job.status,
"collector_type": job.collector_type,
}


@router.get("/results", response_model=dict)
async def list_scan_results(
org_id: VerifiedOrgId, db: DB, current_user: AnyInternalUser,
severity: str | None = Query(None),
status: str | None = Query(None),
service: str | None = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
):
"""List Prowler scan results with optional filters."""
items, total = await prowler_service.list_scan_results(
db, org_id, severity=severity, status=status, service=service,
page=page, page_size=page_size,
)
return {
"items": [item.model_dump() for item in items],
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size,
}


@router.get("/results/{job_id}", response_model=ProwlerScanResultResponse)
async def get_scan_detail(
org_id: VerifiedOrgId, job_id: UUID, db: DB, current_user: AnyInternalUser
):
"""Get detailed results for a specific Prowler scan."""
result = await prowler_service.get_scan_detail(db, org_id, job_id)
if not result:
from app.core.exceptions import NotFoundError
raise NotFoundError(f"Prowler scan {job_id} not found")
return result


@router.get("/compliance-posture", response_model=ProwlerCompliancePosture)
async def get_compliance_posture(
org_id: VerifiedOrgId, db: DB, current_user: AnyInternalUser
):
"""Get aggregate compliance posture from Prowler scans."""
return await prowler_service.get_compliance_posture(db, org_id)


@router.get("/findings-summary", response_model=ProwlerFindingSummary)
async def get_findings_summary(
org_id: VerifiedOrgId, db: DB, current_user: AnyInternalUser
):
"""Get summary of findings from the most recent Prowler scan."""
return await prowler_service.get_findings_summary(db, org_id)
2 changes: 2 additions & 0 deletions backend/app/api/v1/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
gap_analysis,
tenants,
embeddings,
prowler,
)

api_router = APIRouter()
Expand Down Expand Up @@ -69,3 +70,4 @@
api_router.include_router(gap_analysis.router)
api_router.include_router(tenants.router)
api_router.include_router(embeddings.router)
api_router.include_router(prowler.router)
1 change: 1 addition & 0 deletions backend/app/collectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
import app.collectors.aws_collectors # noqa: F401
import app.collectors.github_collectors # noqa: F401
import app.collectors.okta_collectors # noqa: F401
import app.collectors.prowler_collectors # noqa: F401

__all__ = ["COLLECTOR_REGISTRY", "BaseCollector"]
293 changes: 293 additions & 0 deletions backend/app/collectors/prowler_collectors.py

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class Settings(BaseSettings):
LITELLM_MODEL: str = "gpt-4o-mini"
OPENAI_API_KEY: str = ""

# Prowler
PROWLER_OUTPUT_DIR: str = "/tmp/prowler-output"
PROWLER_TIMEOUT_SECONDS: int = 3600

# SMTP email
SMTP_HOST: str = ""
SMTP_PORT: int = 587
Expand Down
73 changes: 73 additions & 0 deletions backend/app/schemas/prowler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Pydantic schemas for Prowler security scanner integration."""
from pydantic import BaseModel, Field


class ProwlerScanTrigger(BaseModel):
integration_id: str
scan_type: str = Field(default="full", description="full | service | compliance")
services: list[str] | None = Field(default=None, description="Services to scan (for service scan type)")
compliance_framework: str | None = Field(default=None, description="Framework ID (for compliance scan type)")


class ProwlerFinding(BaseModel):
check_id: str = ""
check_title: str = ""
status: str = ""
severity: str = ""
service: str = ""
region: str = ""
resource_id: str = ""
resource_arn: str = ""
status_extended: str = ""
risk: str = ""
remediation: str = ""
compliance: dict = Field(default_factory=dict)


class ProwlerScanResultResponse(BaseModel):
job_id: str
status: str
scan_type: str | None = None
cloud_provider: str | None = None
total_findings: int = 0
passed: int = 0
failed: int = 0
pass_rate: float = 0.0
created_at: str | None = None
findings: list[ProwlerFinding] = Field(default_factory=list)


class ComplianceFrameworkPosture(BaseModel):
framework: str
total_checks: int = 0
passed: int = 0
failed: int = 0
pass_rate: float = 0.0


class ServicePosture(BaseModel):
service: str
total_checks: int = 0
passed: int = 0
failed: int = 0
pass_rate: float = 0.0


class ProwlerCompliancePosture(BaseModel):
frameworks: list[ComplianceFrameworkPosture] = Field(default_factory=list)
services: list[ServicePosture] = Field(default_factory=list)
overall_pass_rate: float = 0.0
total_scans: int = 0
last_scan_at: str | None = None


class ProwlerFindingSummary(BaseModel):
total: int = 0
passed: int = 0
failed: int = 0
pass_rate: float = 0.0
by_severity: dict[str, int] = Field(default_factory=dict)
by_service: dict[str, int] = Field(default_factory=dict)
critical_count: int = 0
high_count: int = 0
last_scan_at: str | None = None
49 changes: 49 additions & 0 deletions backend/app/services/monitoring_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,55 @@ async def _execute_checks(
rule.last_result = "fail"
else:
rule.last_result = "pass"
elif rule.check_type == "prowler_scan":
# Check for FAIL findings at or above severity threshold from latest Prowler scan
config = rule.config or {}
severity_threshold = config.get("severity_threshold", "medium")
severity_order = {"critical": 4, "high": 3, "medium": 2, "low": 1}
min_severity = severity_order.get(severity_threshold, 2)

from app.models.collection_job import CollectionJob
scan_result = await db.execute(
select(CollectionJob).where(
CollectionJob.org_id == org_id,
CollectionJob.collector_type.like("prowler_%"),
CollectionJob.status == "completed",
).order_by(CollectionJob.created_at.desc()).limit(1)
)
latest_scan = scan_result.scalar_one_or_none()

if latest_scan and latest_scan.result_data:
data = latest_scan.result_data.get("data", {})
findings = data.get("findings", [])
critical_findings = [
f for f in findings
if f.get("status", "").upper() == "FAIL"
and severity_order.get(f.get("severity", "").lower(), 0) >= min_severity
]
if critical_findings:
alert = MonitorAlert(
org_id=org_id,
rule_id=rule.id,
severity="high",
title=f"Prowler scan: {len(critical_findings)} findings at {severity_threshold}+ severity",
details={
"finding_count": len(critical_findings),
"scan_job_id": str(latest_scan.id),
"top_findings": [
{"check_id": f.get("check_id"), "severity": f.get("severity"), "service": f.get("service")}
for f in critical_findings[:5]
],
},
triggered_at=now,
)
db.add(alert)
alerts_created.append(alert)
rule.last_result = "fail"
else:
rule.last_result = "pass"
else:
rule.last_result = "pass"

else:
rule.last_result = "pass"

Expand Down
Loading
Loading