From c2b023ac92e724a9b316d62fa170357234f7d23c Mon Sep 17 00:00:00 2001 From: O M Date: Tue, 29 Sep 2026 16:25:21 -0400 Subject: [PATCH] Document attack-path findings written by a rules run. The findings list, console, and schema pages need to describe the path stored on those rows. Co-authored-by: Cursor --- src/pages/docs/api.astro | 11 ++++-- src/pages/docs/attack-paths.astro | 59 +++++++++++++++++++++++++++- src/pages/docs/cli.astro | 2 +- src/pages/docs/console.astro | 2 +- src/pages/docs/enrichment.astro | 5 ++- src/pages/docs/exports.astro | 4 +- src/pages/docs/getting-started.astro | 7 +++- src/pages/docs/index.astro | 2 +- src/pages/docs/prioritization.astro | 6 +++ src/pages/docs/rules.astro | 24 +++++++---- src/pages/docs/schema.astro | 4 +- 11 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/pages/docs/api.astro b/src/pages/docs/api.astro index ad50768..2fb65d4 100644 --- a/src/pages/docs/api.astro +++ b/src/pages/docs/api.astro @@ -133,10 +133,12 @@ curl -s -H "Authorization: Bearer $OM_API_SECRET" \\ Attack paths for depth and row limits.

- GET /v1/findings returns an array. Each item has finding (the node) + GET /v1/findings returns findings and, when another page remains, + next_cursor. Each item has finding (the node) plus affected_resource_id, affected_resource_name, and - affected_resource_type from the VIOLATES edge. Rows are ordered by - normalized_score descending. Default limit 200. + affected_resource_type from the VIOLATES edge. An attack-path + finding also includes path, the ordered node ids from the internet to the + datastore. Rows are ordered by normalized_score descending. Default limit 200.

POST /v1/rules/run returns matches and findings_created. @@ -163,7 +165,8 @@ curl -s -H "Authorization: Bearer $OM_API_SECRET" \\

Failures are JSON {`{"error":"..."}`} with 400, 401, or 500. A missing or wrong secret on any /v1 route except health is 401 unauthorized. Invalid JSON on ingest is 400 - invalid json body. There is no request id and no pagination cursor. Raise + invalid json body. There is no request id. A list that has another page returns + next_cursor; pass it back as cursor. Raise limit when a list is truncated. There is no server-side maximum above the number you pass, except the fixed caps inside path queries, blast radius, and exports.

diff --git a/src/pages/docs/attack-paths.astro b/src/pages/docs/attack-paths.astro index e0a181a..a12dac8 100644 --- a/src/pages/docs/attack-paths.astro +++ b/src/pages/docs/attack-paths.astro @@ -6,7 +6,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; toxic-s3-public-with-admin-role is an alias and is not listed.

+

Attack-path findings

+

+ Named queries answer a question you ask. om rules run and + POST /v1/rules/run also write the combination into the findings list. Control + rules run first. The attack-path rule then writes one finding for each existing + finding on an internet-reachable workload, paired with a datastore that workload can reach. +

+

+ A workload is internet-reachable when a REACHABLE walk from + internet:global ends on it, with the same depth limit as + internet-to-datastore (recursion stops before depth 8). The source finding is a + VIOLATES edge whose target is that workload. Findings with + finding_type: attack_path are not sources, so the pass does not chain. Two + findings on the same workload produce two attack-path rows for the same datastore. +

+

+ The workload reaches a datastore in either of two hops. A CAN_ACCESS edge from + the workload to the datastore is one. ASSUMES to an identity that itself has + CAN_ACCESS is the other. The demo path uses the second: Internet → sg-web → + web-1 → AdminRole → prod-db. When both hops reach the same datastore, the shorter path is + stored, which is the direct CAN_ACCESS edge. The finding description names the + source finding and the datastore, for example "Internet-exposed workload: web-1 is on a path + to prod-db." +

+

+ path is that ordered list of node ids. The finding id is + {'finding:attack-path:{source-finding-id}:{datastore-id}'}. + finding_type is attack_path. normalized_score and + severity are copied from the source finding’s score, using the same bands as other rules. A + source with no score is stored as 75, severity high. The graph-context bonuses are not added + again. The VIOLATES edge points at the workload, so the findings list names that + workload as the affected resource. datastore_id and + source_finding_id are properties on the finding. +

+

+ A reachable workload with no datastore hop does not get this finding. A hop whose workload + has no other finding does not either. Run om rules run attack-path on a graph + that has the edges and no findings, and the pass writes nothing. + om rules run with no id is different: cspm-internet-workload writes + a finding on every internet-reachable workload first, and the attack-path pass then pairs + that finding with each datastore the workload can reach. sensitivity does not + filter these rows. internet-to-sensitive-datastore remains the query that keeps + only marked datastores. +

+

+ Re-running the rule deletes an attack-path finding whose hop or source finding is gone. + Other rules’ findings stay. om enrich cve does not write attack-path rows. Run + rules again after enrichment so a new CVE is paired with the datastore. + GET /v1/findings returns path on the row. The console prints those + ids on the card. SIEM export includes the same list. +

+

Reading an empty result

An empty path list means the edges are not there, not that the account is safe. The walk diff --git a/src/pages/docs/cli.astro b/src/pages/docs/cli.astro index 9d6db8c..20f379c 100644 --- a/src/pages/docs/cli.astro +++ b/src/pages/docs/cli.astro @@ -69,7 +69,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; om rules run [rule-id] - Evaluate one rule or the full catalog and upsert findings. + Evaluate one rule or the full catalog and upsert findings. A full run writes attack-path findings after the control rules. om paths list diff --git a/src/pages/docs/console.astro b/src/pages/docs/console.astro index 73fd2e5..384d21f 100644 --- a/src/pages/docs/console.astro +++ b/src/pages/docs/console.astro @@ -19,7 +19,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';

What you can do

  • Read node and edge counts in the stats strip. Those numbers come from GET /v1/graph/stats.
  • -
  • Scan the findings list from GET /v1/findings. Each card shows severity, title, and the affected resource. Choosing a card highlights that node when it is in the current snapshot.
  • +
  • Scan the findings list from GET /v1/findings. Each card shows severity, title, and the affected resource. An attack-path card also prints the path node ids. Choosing a card highlights the affected resource when it is in the current snapshot.
  • The dropdown loads every named query. On first paint it selects internet-to-datastore when that query exists, then draws those paths. Choose Full graph snapshot to call GET /v1/graph/snapshot instead (500 nodes and 2000 edges).
  • A named query still uses the snapshot’s edge list to label each hop. If the connecting edge is past that 2000-edge cap, the hop is drawn as type PATH and any reason property is missing. The nodes themselves come from the query, not from the snapshot cap.
  • Select an identity node to load GET /v1/identity/blast-radius. The panel shows the summary and the reachable nodes. See Blast radius. Other node types do not open that panel.
  • diff --git a/src/pages/docs/enrichment.astro b/src/pages/docs/enrichment.astro index b1c38f9..2436be9 100644 --- a/src/pages/docs/enrichment.astro +++ b/src/pages/docs/enrichment.astro @@ -124,7 +124,10 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; VIOLATES edge. The finding id is {'finding:{cve-id-lower}:{workload-id}'}, so a second run overwrites the same row. The edge property relationship is affects. A later run does - not delete a CVE finding whose package has since left the workload. + not delete a CVE finding whose package has since left the workload. Enrichment does not + write attack-path findings. Run om rules run afterward so a new CVE on a + workload that can reach a datastore is paired with that datastore. See + Attack paths.

    • finding_type is cve.
    • diff --git a/src/pages/docs/exports.astro b/src/pages/docs/exports.astro index d2f0814..73d8d0c 100644 --- a/src/pages/docs/exports.astro +++ b/src/pages/docs/exports.astro @@ -49,7 +49,9 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; }`}

      affected_resource_* comes from the VIOLATES edge. A finding with - no such edge still exports. Those three fields are omitted. + no such edge still exports. Those three fields are omitted. An attack-path finding also + includes path, the ordered node ids, both on the record and inside + finding.properties.

      Slack

      diff --git a/src/pages/docs/getting-started.astro b/src/pages/docs/getting-started.astro index bd5165c..79d8c81 100644 --- a/src/pages/docs/getting-started.astro +++ b/src/pages/docs/getting-started.astro @@ -91,7 +91,12 @@ docker compose up -d`}

      Open http://localhost:8080. The console opens on internet-to-datastore, which should draw that path. Stats should be non-zero, - and the findings list should include the public datastore and the internet-exposed workload. + and the findings list should include attack-path rows for web-1 to prod-db, the public datastore, + and the internet-exposed workload. web-1 matches the internet-exposed workload rule, the + public-IP rule, and the IMDSv1 rule, so each of those becomes its own attack-path finding. + The card lists the ids internet:global, aws:sg:sg-web, + aws:ec2:i-web-1, aws:iam:role/AdminRole, and + aws:rds:prod-db. If the page is empty, the CLI and the API are pointed at different databases. See Docker Compose.

      diff --git a/src/pages/docs/index.astro b/src/pages/docs/index.astro index 066b47d..be9ddb3 100644 --- a/src/pages/docs/index.astro +++ b/src/pages/docs/index.astro @@ -50,7 +50,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; diff --git a/src/pages/docs/prioritization.astro b/src/pages/docs/prioritization.astro index 83cabb1..75313ed 100644 --- a/src/pages/docs/prioritization.astro +++ b/src/pages/docs/prioritization.astro @@ -116,4 +116,10 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; and type. The API orders them by normalized_score, highest first, and the console shows that order.

      +

      + The attack-path rule does not use this table. It copies + normalized_score from the finding already on the workload and maps that score + through the bands above. A source with no score is stored as 75 (high). See + Attack paths. +

      diff --git a/src/pages/docs/rules.astro b/src/pages/docs/rules.astro index 2b5f0d4..dc62074 100644 --- a/src/pages/docs/rules.astro +++ b/src/pages/docs/rules.astro @@ -6,12 +6,12 @@ import DocsLayout from '@/layouts/DocsLayout.astro';

      Open-source CSPM rules

      - The rules engine reads the graph and writes Finding nodes. Three checks are - implemented in Go. The rest come from YAML packs embedded in - the binary at compile time. om rules list prints the combined catalog. + The rules engine reads the graph and writes Finding nodes. Three control checks + are implemented in Go. An attack-path pass runs after them. The other checks come from + YAML packs embedded in the binary at compile time. + om rules list prints the combined catalog. attack-path is last.

      {`./om rules list
       ./om rules run
      @@ -64,11 +65,17 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
                 60
                 Datastore with a CAN_ACCESS edge from an identity whose admin_access is true. The match forces admin_can_access in graph context.
               
      +        
      +          attack-path
      +          source score, or 75
      +          Existing finding on an internet-reachable workload that can reach a datastore. Runs after the other rules. Details are on Attack paths.
      +        
             
           
         
         

      - Every match is then scored with the boosts on Prioritization. + Control rules and pack rules are scored with the boosts on Prioritization. + The attack-path rule copies the source finding’s score and does not add those boosts again. A base of 70 plus a path-to-datastore boost of 25 becomes 95, severity critical. The admin-datastore rule forces admin_can_access before scoring, so that match also receives the +5 admin boost. The other two built-in rules do not set that flag. @@ -77,8 +84,9 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; internet-to-datastore and still score as not internet-reachable.

      - Each match upserts {'finding:{rule-id}:{resource-id}'} and a - VIOLATES edge from the finding to the resource. Re-running updates that row. + Each control-rule match upserts {'finding:{rule-id}:{resource-id}'} and a + VIOLATES edge from the finding to the resource. The attack-path id is + {'finding:attack-path:{source-finding-id}:{datastore-id}'}. Re-running updates that row. findings_created counts those upserts, including updates. Pack rules and cspm-public-datastore consider at most 500 nodes of the target type, ordered by name. cspm-internet-workload has no row cap. It uses the full diff --git a/src/pages/docs/schema.astro b/src/pages/docs/schema.astro index bbf14fe..d24b80b 100644 --- a/src/pages/docs/schema.astro +++ b/src/pages/docs/schema.astro @@ -55,7 +55,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; Finding - CSPM or CVE result. finding_type is cspm or a CVE id is set. + CSPM, CVE, exposure, or attack-path result. finding_type is cspm, cve, exposure, or attack_path. Control @@ -128,7 +128,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro'; Finding - {'finding:{rule-id}:{resource-node-id}'} + {'finding:{rule-id}:{resource-node-id}'}. An attack-path finding is {'finding:attack-path:{source-finding-id}:{datastore-id}'}. Edge