Skip to main content

Attack Chains & Toxic Combinations

Sidebar: Intelligence → Attack Map (/attack-map)

A single vulnerability is rarely the whole story. An open SSRF on its own is a finding; an open SSRF on an internet-facing app that reaches cloud metadata, which yields a wildcard IAM policy, which exposes a public bucket is a breach. apPosture's attack-chain engine performs multi-hop attack-path inference over your unified vulnerabilities to surface exactly those escalating paths - and the toxic combinations that turn several medium findings into one critical, internet-reachable kill-chain.

This page documents the engine, its scoring, the API, and how chains surface in the UI and reports. For the interactive graph and choke-point analysis, see the Attack Map.

What an attack chain is

A chain is an ordered sequence of real findings, each classified into a kill-chain stage, that compose into an end-to-end exploit path. The engine walks six stages in escalation order:

A toxic combination is a named, well-known multi-finding pattern (for example SSRF → IMDS → IAM) that fires when its required capabilities co-occur on the same app. Toxic combinations carry a crisp narrative and a small score bonus over a generic ladder chain.

Every hop is grounded in a real open Vulnerability - the engine never invents a step the data does not support.

Inputs

The engine reads the unified Vulnerabilities hub: every open finding (any status in the open set), regardless of source - DAST, SAST, SCA, secrets, cloud/IaC, and Kubernetes posture - is a candidate hop. For each app it considers up to the top 40 open vulnerabilities ordered by priority score. The relevant per-finding fields are CWE, title, severity, effective risk (risk_override or risk_score), reachable, and exploit_confirmed; per-app it reads environment, business_criticality, and project_id.

How the capability graph is built

The engine does capability inference, not full network simulation. It reads the semantics of each finding to decide what an attacker gains, rather than requiring a hand-drawn topology.

  • Nodes / hops - each open Vulnerability that grants an attacker capability. classify() maps a finding to {stage, technique, mitre, label} using CWE first, then title keywords to disambiguate findings that share a CWE (e.g. CWE-732 covers both public-S3 and wildcard-IAM). Findings that move no attacker forward (informational / hygiene) classify to None and are dropped.
  • Capabilities / stages - the attacker primitive each hop grants, bucketed into one of entry · execute · credential · privesc · pivot · impact. Examples: SSRF (CWE-918) → entry; command injection (CWE-78/77) → execute; hardcoded secret (CWE-798) → credential; wildcard IAM (CWE-732 + "iam"/"wildcard") → privesc; privileged/hostPath container → privesc; public bucket → impact. Each hop carries its MITRE ATT&CK technique (T1190, T1059, T1552.001, T1078.004, T1611, T1530, …).
  • Edges - the stage ladder itself. Within an app, the highest-risk capability per populated stage is stitched into an escalating chain that walks the stages in order. Across apps, same-project apps form a trust group: an internet-facing entry/credential/execute capability on one app can pivot to an impact capability on a different crown-jewel app in the same project_id.

Inference pipeline

The pipeline is fully deterministic - there is no AI in the path search or scoring. (An optional AI narrative layer exists separately; see AI narrative.)

Concretely, per application the engine:

  1. Classifies every candidate finding into a capability, discarding the unclassifiable.
  2. Groups capabilities by stage and sorts each stage by effective risk.
  3. Assembles three chain kinds:
    • kill-chain - the primary ladder: one best vuln per populated stage, emitted when at least two stages are present.
    • toxic-combination - any named rule that fires (SSRF→IMDS→IAM, public object storage, container escape, injection→exec→data/cred).
    • cross-service - a two-hop pivot from an internet-facing entry/credential/execute capability to an impact capability on another app in the same project.
  4. Scores, dedups, ranks, and caps: identical hop-sets are collapsed, chains are sorted by score descending, and the top 40 are returned.

There is no unbounded graph traversal - chains are bounded by the six fixed stages (ladder), the named-rule set (toxic combos), or two hops (cross-service), so there is no combinatorial blow-up.

Scoring & what makes a "toxic combo"

Each chain gets a 0-100 score. The score starts from the highest-risk member and is raised by chain depth, internet exposure, proven reachability, and crown-jewel criticality.

The key distinction the engine makes - and never overclaims on - is danger vs theoretical:

  • A chain is danger only when it runs on an internet-facing app (prod environment) and its entry hop is reachable or exploit_confirmed. These are the chains that warrant immediate response; the report builder tints them critical.
  • Otherwise the chain is theoretical: the capability is present but reachability is unproven, and it is labelled as such.

A "toxic combination" is therefore a high-scoring, named, multi-finding chain - and the ones that matter most are the toxic-combination chains flagged danger. Fixing the entry hop of such a chain collapses the whole path; see Priorities for how chain risk feeds the few-that-matter funnel and the Attack Map for choke-point analysis.

API

Both endpoints live under the graph router and require the view_findings permission.

GET /api/v1/graph/attack-chains - build ranked multi-hop chains.

Query parameters:

ParamTypeDefaultEffect
application_idstringnoneScope to one application (else org-wide)
internet_onlyboolfalseOnly consider internet-facing (prod) apps

Response shape (high level):

{
"chains": [
{
"id": "chain:<app>:ladder",
"name": "Initial access → Impact · data / host on api-gateway",
"kind": "kill-chain | toxic-combination | cross-service",
"app": { "id": "...", "name": "...", "environment": "prod",
"criticality": "critical", "internet_facing": true },
"pivot_to": { "id": "...", "name": "...", "criticality": "..." },
"score": 92,
"danger": true,
"hops": [
{ "vuln_id": "...", "title": "...", "severity": "high",
"stage": "entry", "stage_label": "Initial access",
"technique": "SSRF → internal/metadata reach", "mitre": "T1190",
"reachable": true, "confirmed": false, "score": 80, "location": "..." }
],
"mitre": ["T1190", "T1552.005", "T1078.004"]
}
],
"stats": {
"chains": 12, "danger_chains": 3, "toxic_combinations": 4,
"cross_service": 2, "capabilities_classified": 57, "max_chain_risk": 92
}
}

pivot_to is present only on cross-service chains.

AI narrative (optional)

POST /api/v1/graph/attack-chains/explain - best-effort AI narrative for a single chain (STRIDE/CAPEC mapping, attacker story, highest-leverage fix). Send { "hop_vuln_ids": [...], "name": "...", "kind": "..." }. The deterministic structure stays ground truth: hops are re-built from the real Vulnerabilities by id via classify_hop(), client text never reaches the LLM, and the model may only describe - not add or reorder - steps. Up to eight hop ids are honored. Returns { "ok": true, ... } or { "ok": false, "reason": ... } when the model is unavailable.

In the UI

The engine surfaces on the Attack Map page (/attack-map, Intelligence section). Each chain renders as a card with a kind badge (toxic-combination red, cross-service violet, kill-chain slate), a score, a danger indicator, and per-hop rows showing the stage icon, technique, MITRE technique, and the contributing finding. You can filter by application, internet-facing only, severity, and danger-only. A chain can be expanded and replayed hop-by-hop, and the "Explain" action calls the AI-narrative endpoint above. See the Attack Map for the full graph and choke-point view.

In reports

The Attack Chains & Toxic Combinations report builder (reporting/builders/attack_chains.py) renders ranked chains for a report. It scopes to a single application when the report scope resolves to exactly one app, otherwise org-wide. The report contains:

  • A KPI grid: total chains, toxic combinations, danger (internet-facing) chains, and max chain risk.
  • A chain table (top 50 by score) with columns: severity tint, chain name, kind, application, the stage path (e.g. Initial access → Impact · data / host), and score. The severity tint maps danger → critical, then score bands ≥60 high, ≥35 medium, else low.

Limits & honest caveats

  • Capability inference, not simulation. The engine reads finding semantics; it does not model live network reachability between hosts beyond the same-project trust group.
  • Cross-service linkage is lightweight - pivots are limited to apps sharing a project_id, and cross-service chains are two hops.
  • Bounded breadth: up to 40 candidate vulns per app and up to 40 chains returned overall, ranked by score.
  • Theoretical ≠ exploitable. A chain is only flagged danger when it is internet-facing with a reachable/confirmed entry; everything else is explicitly theoretical. Use Threat Models and the Vulnerabilities hub to validate context.