from __future__ import annotations from pydantic import BaseModel, Field from typing import Optional, Literal, List, Dict, Any from enum import Enum class Severity(str, Enum): CRITICAL = "critical" HIGH = "high" MEDIUM = "medium" LOW = "low" INFO = "info" class Confidence(str, Enum): HIGH = "high" MEDIUM = "medium" LOW = "low" class FileRole(str, Enum): PRODUCTION = "production" TEST = "test" FIXTURE = "fixture" CI = "ci" CONFIG = "config" INFRA = "infra" VENDOR = "vendor" class ScanDomain(str, Enum): SECRETS = "secrets" DEPS = "deps" IAC = "iac" SAST = "sast" CONFIG = "config" FRONTEND = "frontend" LLM_VULNS = "llm_vulns" DAST = "dast" CHAIN = "chain" class TaintPath(BaseModel): source_file: str source_line: int source_type: str # req.body | query_param | cookie | header | file intermediate_calls: List[Dict[str, Any]] = Field(default_factory=list) sink_file: str sink_line: int sink_type: str # sql | exec | eval | html_render | redirect | external_request sanitizers_found: List[str] = Field(default_factory=list) class Evidence(BaseModel): tool_call: str result_summary: str file_path: Optional[str] = None line_number: Optional[int] = None class Finding(BaseModel): id: str = Field(default_factory=lambda: __import__("uuid").uuid4().hex) title: str description: str severity: Severity confidence: Confidence = Confidence.MEDIUM domain: ScanDomain check_id: Optional[str] = None check_category: Optional[str] = None policy_reference: Optional[str] = None file_path: Optional[str] = None line_number: Optional[int] = None file_role: Optional[FileRole] = None explanation: str = "" suggested_fix: str = "" evidence: List[Evidence] = Field(default_factory=list) taint_path: Optional[TaintPath] = None is_false_positive: bool = False false_positive_reason: Optional[str] = None reachability_unconfirmed: bool = False dismissed_at: Optional[str] = None confirmed_runtime: bool = False # DAST confirmed chained_from: List[str] = Field(default_factory=list) # IDs of contributing findings category: Optional[str] = None validity: Optional[Literal["active", "revoked", "unknown"]] = None secret_type: Optional[str] = None raw_secret_value: Optional[str] = None secret_value_encrypted: Optional[str] = None exploitable_by: Optional[str] = None # unauthenticated | authenticated | admin taint_confirmed: bool = False verdict: Optional[str] = None validation_reason: Optional[str] = None requires_llm_gate: bool = Field(default=False) @property def file(self) -> Optional[str]: return self.file_path @file.setter def file(self, value: Optional[str]): self.file_path = value class ScanResult(BaseModel): scan_id: str status: Literal["completed", "failed", "partial"] score: int findings: List[Finding] findings_count: int chain_findings: List[Finding] = Field(default_factory=list) scan_metadata: Dict[str, Any] = Field(default_factory=dict) suppressed_findings: List[Finding] = Field(default_factory=list) class ScanRequest(BaseModel): repo_url: Optional[str] = None local_files: Optional[List[Dict[str, str]]] = None # [{path, content}, ...] runtime_url: Optional[str] = None scan_id: Optional[str] = None project_id: Optional[str] = None user_id: Optional[str] = None plan_tier: Literal["free", "pro", "team", "enterprise"] = "free" enable_dast: bool = False github_token: Optional[str] = None