Spaces:
Sleeping
Sleeping
| """ | |
| Layer 6 — Chain Correlator | |
| Cross-domain finding escalation. No other scanner does this. | |
| Combines findings from different domains to create CRITICAL chain findings. | |
| """ | |
| from __future__ import annotations | |
| import uuid | |
| from pathlib import Path | |
| from typing import List, Dict | |
| from .models import Finding, Severity, Confidence, ScanDomain | |
| # Chain rule: (condition_check_fn, new_title, new_description, new_explanation) | |
| def correlate_chains(findings: List[Finding]) -> List[Finding]: | |
| """Detect multi-finding chains and return new CRITICAL chain findings.""" | |
| chain_findings: List[Finding] = [] | |
| # Index findings by domain and check_id for fast lookup | |
| by_domain: Dict[ScanDomain, List[Finding]] = {} | |
| for f in findings: | |
| by_domain.setdefault(f.domain, []).append(f) | |
| secrets = by_domain.get(ScanDomain.SECRETS, []) | |
| iac = by_domain.get(ScanDomain.IAC, []) | |
| sast = by_domain.get(ScanDomain.SAST, []) | |
| config = by_domain.get(ScanDomain.CONFIG, []) | |
| frontend = by_domain.get(ScanDomain.FRONTEND, []) | |
| active_secrets = [ | |
| f for f in secrets | |
| if not f.is_false_positive | |
| and (f.validity == "active" or f.check_id == "COMMITTED_ENV_FILE" or f.confidence == Confidence.HIGH) | |
| ] | |
| all_active = [f for f in findings if not f.is_false_positive] | |
| # Chain 1: Exposed admin route + no auth + hardcoded password | |
| admin_routes = [f for f in sast if "admin" in (f.file_path or "").lower() or "admin" in f.description.lower()] | |
| admin_secrets = [f for f in secrets if "admin" in f.title.lower() or "password" in f.title.lower()] | |
| if admin_routes and admin_secrets: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: Full Admin Takeover — Exposed Admin Route + Hardcoded Credentials", | |
| description="An unauthenticated admin endpoint was found alongside hardcoded admin credentials. An attacker can access full admin functionality with zero authentication.", | |
| contributing=[admin_routes[0], admin_secrets[0]], | |
| exploit="An attacker navigates to the admin endpoint and uses the hardcoded credentials found in source code to gain full admin access.", | |
| )) | |
| # Chain 2: Live DB secret + DB port exposed in IaC | |
| db_secrets = [f for f in active_secrets if "db" in f.title.lower() or "connection" in f.title.lower() or "mongodb" in f.title.lower() or "postgres" in f.title.lower()] | |
| exposed_ports = [f for f in iac if "port" in f.description.lower() or "0.0.0.0" in f.description.lower()] | |
| if db_secrets and exposed_ports: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: Direct Database Access — Exposed Credentials + Open Port", | |
| description="A live database connection string was found in source code AND the database port is exposed to the internet via infrastructure misconfiguration.", | |
| contributing=[db_secrets[0], exposed_ports[0]], | |
| exploit="An attacker uses the leaked connection string with any database client to connect directly to the production database from the internet.", | |
| )) | |
| # Chain 3: Client-exposed secret + .env file committed | |
| client_secrets = [f for f in frontend if "CLIENT_EXPOSED_SECRET" in (f.check_id or "")] | |
| committed_env = [f for f in secrets if "COMMITTED_ENV_FILE" in (f.check_id or "")] | |
| if client_secrets and committed_env: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: Payment/Service API Fully Compromised — Client Bundle + Git History", | |
| description="A secret key is exposed both in the browser bundle AND committed to git history. The key is accessible to any website visitor AND to anyone who can view the repository.", | |
| contributing=[client_secrets[0], committed_env[0]], | |
| exploit="Any user viewing page source OR anyone with repository access has the live API key. For payment providers like Stripe, this enables fraudulent charges.", | |
| )) | |
| # Chain 4: GraphQL introspection + no auth + sensitive data in schema | |
| graphql = [f for f in config if "GRAPHQL" in (f.check_id or "")] | |
| no_auth_sast = [f for f in sast if "auth" in f.title.lower() and "missing" in f.title.lower()] | |
| if graphql and no_auth_sast: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: Full Data Model Exfiltration — GraphQL Introspection + No Auth", | |
| description="GraphQL introspection is enabled on an unauthenticated endpoint. Attackers can enumerate the entire data schema and query any data.", | |
| contributing=[graphql[0], no_auth_sast[0]], | |
| exploit="An attacker runs an introspection query to map all types and fields, then crafts queries to dump user data, orders, or any sensitive information without authenticating.", | |
| )) | |
| # Chain 5: Live verified secret + no rate limiting | |
| live_secrets = [f for f in active_secrets] | |
| no_rate_limit = [f for f in config if "RATE_LIMIT" in (f.check_id or "")] | |
| if live_secrets and no_rate_limit: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: Credential Stuffing Highway — Live API Keys + No Rate Limiting", | |
| description="Verified live API keys are exposed in the codebase and there is no rate limiting on API endpoints. Attackers can enumerate and use the keys at full speed.", | |
| contributing=[live_secrets[0], no_rate_limit[0]], | |
| exploit="Attacker extracts the live API key from the source code/git history and makes unlimited API calls. No rate limiting means no detection or blocking.", | |
| )) | |
| # Chain 6: DAST confirmed + static confirmed = escalate | |
| dast_findings = by_domain.get(ScanDomain.DAST, []) | |
| for dast_f in dast_findings: | |
| # Find matching static finding | |
| for static_f in all_active: | |
| if static_f.domain == ScanDomain.DAST: | |
| continue | |
| if _findings_overlap(dast_f, static_f): | |
| chain_findings.append(_make_chain( | |
| title=f"CHAIN: Runtime + Static Confirmed — {dast_f.title}", | |
| description="This vulnerability was independently confirmed by both static analysis AND runtime DAST scanning. Exploitability is near-certain.", | |
| contributing=[dast_f, static_f], | |
| exploit=f"Statically detected and runtime-confirmed: {dast_f.description}", | |
| )) | |
| break | |
| # Chain 7: Prototype Pollution -> Remote Code Execution / Auth Bypass | |
| proto_pollution = [f for f in sast if "prototype" in f.title.lower() or "pollution" in f.title.lower()] | |
| unsafe_eval = [f for f in sast if "eval" in f.title.lower() or "exec" in f.title.lower() or "template" in f.title.lower()] | |
| if proto_pollution and unsafe_eval: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: Critical Prototype Pollution to Remote Code Execution (RCE)", | |
| description="Prototype pollution combined with dynamic code execution / template generation enables unauthenticated Remote Code Execution on the hosting server.", | |
| contributing=[proto_pollution[0], unsafe_eval[0]], | |
| exploit="An attacker pollutes object prototypes (e.g., Object.prototype) to inject dynamic properties that are executed when the server parses template variables or runs dynamic evaluations.", | |
| )) | |
| # Chain 8: SSRF -> Internal Cloud Metadata / Port Exposure | |
| ssrf = [f for f in sast if "ssrf" in f.title.lower() or "request" in f.title.lower() or "fetch" in f.title.lower()] | |
| ports = [f for f in iac if "port" in f.description.lower() or "internal" in f.description.lower()] | |
| if ssrf and ports: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: SSRF Server Compromise — Internal Network Port Crawling & Cloud Metadata Leaks", | |
| description="Server-Side Request Forgery vulnerability combined with internal exposed infrastructure ports enables crawling private network assets and stealing temporary IAM credentials.", | |
| contributing=[ssrf[0], ports[0]], | |
| exploit="Attacker forces the web server to make requests to internal service endpoints or cloud metadata APIs (e.g. 169.254.169.254) to bypass firewall barriers.", | |
| )) | |
| # Chain 9: IDOR -> Database Taint Leakage | |
| idor = [f for f in sast if "idor" in f.title.lower() or "auth" in f.title.lower() or "permission" in f.title.lower()] | |
| db_leak = [f for f in sast if "sql" in f.title.lower() or "query" in f.title.lower() or "leak" in f.title.lower()] | |
| if idor and db_leak: | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: Full Tenant Database Exfiltration — IDOR Access + Direct DB Query Exposure", | |
| description="Lack of proper session ownership validation (IDOR) combined with direct database queries allows unauthorized users to query and exfiltrate data from other tenants.", | |
| contributing=[idor[0], db_leak[0]], | |
| exploit="An attacker manipulates query indices or resource primary keys in the request parameters, querying direct records of other tenants due to missing permission validations.", | |
| )) | |
| # Chain 10: LLM Prompt Injection → Code/Command Execution (CROSS-DOMAIN) | |
| # This is the most critical chain: attacker injects prompt that causes LLM to output | |
| # code that gets eval'd/exec'd server-side. Findings can be LLM_VULNS domain (from | |
| # layer1_llm_vulns.py) or SAST domain (from semgrep ai-agent rules that semgrep | |
| # tagged as SAST). We match BOTH by domain AND by title/check_id patterns. | |
| LLM_CHECK_ID_KW = {"prompt", "llm", "langchain", "ai-agent", "generative", "ai-chat", "chatbot", "chatcompletion"} | |
| IS_LLM_KW_IN_TITLE = {"prompt", "llm", "generative", "ai-chat", "langchain"} | |
| IS_LLM_KW_IN_CHECK = {"prompt", "llm", "langchain", "ai-agent", "injection", "generative", "output_eval", "tool_execution"} | |
| llm_injection_findings = [ | |
| f for f in findings | |
| if not f.is_false_positive and f.severity not in {None, Severity.LOW} | |
| and ( | |
| f.domain == ScanDomain.LLM_VULNS and "prompt" in f.title.lower() | |
| or any(kw in f.title.lower() for kw in IS_LLM_KW_IN_TITLE) | |
| or any(kw in (f.check_id or "").lower() for kw in IS_LLM_KW_IN_CHECK) | |
| ) | |
| ] | |
| unsafe_llm_output_sinks = [ | |
| f for f in sast | |
| if not f.is_false_positive | |
| and ( | |
| any(kw in f.title.lower() for kw in ["eval", "exec", "template injection", "rce", "command injection"]) | |
| or ("langchain" in (f.check_id or "").lower() and any(kw in f.title.lower() for kw in ["eval", "exec", "template", "command"])) | |
| or ("llm_output" in (f.check_id or "").lower()) | |
| ) | |
| ] | |
| if llm_injection_findings and unsafe_llm_output_sinks: | |
| # Deduplicate by (chain_title, llm_file, sink_file) to avoid N×M duplicates | |
| # when multiple findings from the same file satisfy both criteria. | |
| _seen_chains: set = set() | |
| def _chain_key(title: str, f1: Finding, f2: Finding) -> tuple: | |
| return (title, f1.file_path, f2.file_path) | |
| for llm_f in llm_injection_findings: | |
| for sink_f in unsafe_llm_output_sinks: | |
| if llm_f.file_path == sink_f.file_path: | |
| key = _chain_key("CHAIN: LLM Prompt Injection → Arbitrary Code Execution", llm_f, sink_f) | |
| if key in _seen_chains: | |
| continue | |
| _seen_chains.add(key) | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: LLM Prompt Injection → Arbitrary Code Execution", | |
| description="A prompt injection vulnerability (OWASP LLM01) was found alongside a dynamic code execution sink (eval/exec) in the same file. An attacker injects a malicious prompt that causes the LLM to output code, which is then executed server-side.", | |
| contributing=[llm_f, sink_f], | |
| exploit="Attacker sends a crafted input that is injected into the LLM prompt. The LLM's response contains malicious JavaScript/payload that reaches an eval() or exec() call, resulting in arbitrary server-side code execution (RCE).", | |
| )) | |
| break | |
| # Cross-file chains using taint paths | |
| if llm_f.file_path and llm_f.taint_path: | |
| for sink_f in unsafe_llm_output_sinks: | |
| if sink_f.file_path and llm_f.file_path != sink_f.file_path: | |
| llm_dir = str(Path(llm_f.file_path).parent).replace("\\", "/") | |
| sink_dir = str(Path(sink_f.file_path).parent).replace("\\", "/") | |
| if llm_dir == sink_dir: | |
| key = _chain_key("CHAIN: LLM Prompt Injection → Cross-Module Code Execution", llm_f, sink_f) | |
| if key in _seen_chains: | |
| continue | |
| _seen_chains.add(key) | |
| chain_findings.append(_make_chain( | |
| title="CHAIN: LLM Prompt Injection → Cross-Module Code Execution", | |
| description="A prompt injection in a file using an LLM is chained to a code execution sink in a related module in the same directory. The LLM output can flow to the dangerous sink.", | |
| contributing=[llm_f, sink_f], | |
| exploit="Attacker injects a prompt that manipulates the LLM output, which is then executed via eval/exec calls in a related module in the same service.", | |
| )) | |
| break | |
| return chain_findings | |
| def _findings_overlap(f1: Finding, f2: Finding) -> bool: | |
| """Check if two findings likely refer to the same vulnerability.""" | |
| if f1.file_path and f2.file_path and f1.file_path == f2.file_path: | |
| # Same file: check if line numbers are close (within 10 lines) | |
| if f1.line_number is not None and f2.line_number is not None: | |
| if abs(f1.line_number - f2.line_number) <= 10: | |
| return True | |
| else: | |
| # Fall back to verifying if they share semantic context via titles | |
| title_words_1 = set(f1.title.lower().split()) | |
| title_words_2 = set(f2.title.lower().split()) | |
| overlap = title_words_1 & title_words_2 - {"the", "a", "in", "of", "and", "or", "is", "are", "for", "dast", "sast"} | |
| if len(overlap) >= 2: | |
| return True | |
| title_words_1 = set(f1.title.lower().split()) | |
| title_words_2 = set(f2.title.lower().split()) | |
| overlap = title_words_1 & title_words_2 - {"the", "a", "in", "of", "and", "or", "is", "are", "for", "dast", "sast"} | |
| return len(overlap) >= 3 | |
| def _make_chain(title: str, description: str, contributing: List[Finding], exploit: str) -> Finding: | |
| ids = [f.id for f in contributing] | |
| return Finding( | |
| id=uuid.uuid4().hex, | |
| title=title, | |
| description=description, | |
| severity=Severity.CRITICAL, | |
| confidence=Confidence.HIGH, | |
| domain=ScanDomain.CHAIN, | |
| check_id="CHAIN_CORRELATION", | |
| check_category="chain", | |
| explanation=exploit, | |
| suggested_fix="Address each contributing finding. Start with the secret/credential exposure first, then fix the infrastructure and code issues.", | |
| chained_from=ids, | |
| confirmed_runtime=any(f.confirmed_runtime for f in contributing), | |
| ) | |