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
11 changes: 7 additions & 4 deletions src/pages/docs/api.astro
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,12 @@ curl -s -H "Authorization: Bearer $OM_API_SECRET" \\
<a href="/docs/attack-paths/">Attack paths</a> for depth and row limits.
</p>
<p>
<code>GET /v1/findings</code> returns an array. Each item has <code>finding</code> (the node)
<code>GET /v1/findings</code> returns <code>findings</code> and, when another page remains,
<code>next_cursor</code>. Each item has <code>finding</code> (the node)
plus <code>affected_resource_id</code>, <code>affected_resource_name</code>, and
<code>affected_resource_type</code> from the <code>VIOLATES</code> edge. Rows are ordered by
<code>normalized_score</code> descending. Default limit 200.
<code>affected_resource_type</code> from the <code>VIOLATES</code> edge. An attack-path
finding also includes <code>path</code>, the ordered node ids from the internet to the
datastore. Rows are ordered by <code>normalized_score</code> descending. Default limit 200.
</p>
<p>
<code>POST /v1/rules/run</code> returns <code>matches</code> and <code>findings_created</code>.
Expand All @@ -163,7 +165,8 @@ curl -s -H "Authorization: Bearer $OM_API_SECRET" \\
<p>
Failures are JSON <code>{`{"error":"..."}`}</code> with 400, 401, or 500. A missing or wrong
secret on any <code>/v1</code> route except health is 401 <code>unauthorized</code>. Invalid JSON on ingest is 400
<code>invalid json body</code>. There is no request id and no pagination cursor. Raise
<code>invalid json body</code>. There is no request id. A list that has another page returns
<code>next_cursor</code>; pass it back as <code>cursor</code>. Raise
<code>limit</code> 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.
</p>
Expand Down
59 changes: 58 additions & 1 deletion src/pages/docs/attack-paths.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';

<DocsLayout
title="OpenSourceOM attack path queries"
description="The six named queries om paths run executes, from the internet through workloads to datastores, including datastores marked with sensitivity."
description="The six named attack-path queries, and the attack-path finding a rules run writes for a workload that can reach a datastore."
faq={[
{
question: 'Which attack path queries does OpenSourceOM run?',
Expand All @@ -18,6 +18,11 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
answer:
'After a scan, run om paths run internet-to-datastore. The same named queries are available from GET /v1/graph/query and from the console dropdown.',
},
{
question: 'When does a rules run write an attack-path finding?',
answer:
'After the control rules, om rules run writes one finding per existing finding on an internet-reachable workload that can reach a datastore. The finding stores the path as ordered node ids. GET /v1/findings returns that path on the row.',
},
]}
related={[
{ href: '/blog/attack-path-analysis-cloud-security/', label: 'Attack path analysis' },
Expand Down Expand Up @@ -116,6 +121,58 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<code>toxic-s3-public-with-admin-role</code> is an alias and is not listed.
</p>

<h2>Attack-path findings</h2>
<p>
Named queries answer a question you ask. <code>om rules run</code> and
<code>POST /v1/rules/run</code> also write the combination into the findings list. Control
rules run first. The <code>attack-path</code> rule then writes one finding for each existing
finding on an internet-reachable workload, paired with a datastore that workload can reach.
</p>
<p>
A workload is internet-reachable when a <code>REACHABLE</code> walk from
<code>internet:global</code> ends on it, with the same depth limit as
<code>internet-to-datastore</code> (recursion stops before depth 8). The source finding is a
<code>VIOLATES</code> edge whose target is that workload. Findings with
<code>finding_type: attack_path</code> are not sources, so the pass does not chain. Two
findings on the same workload produce two attack-path rows for the same datastore.
</p>
<p>
The workload reaches a datastore in either of two hops. A <code>CAN_ACCESS</code> edge from
the workload to the datastore is one. <code>ASSUMES</code> to an identity that itself has
<code>CAN_ACCESS</code> 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 <code>CAN_ACCESS</code> 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."
</p>
<p>
<code>path</code> is that ordered list of node ids. The finding id is
<code>{'finding:attack-path:{source-finding-id}:{datastore-id}'}</code>.
<code>finding_type</code> is <code>attack_path</code>. <code>normalized_score</code> 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 <code>VIOLATES</code> edge points at the workload, so the findings list names that
workload as the affected resource. <code>datastore_id</code> and
<code>source_finding_id</code> are properties on the finding.
</p>
<p>
A reachable workload with no datastore hop does not get this finding. A hop whose workload
has no other finding does not either. Run <code>om rules run attack-path</code> on a graph
that has the edges and no findings, and the pass writes nothing.
<code>om rules run</code> with no id is different: <code>cspm-internet-workload</code> 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. <code>sensitivity</code> does not
filter these rows. <code>internet-to-sensitive-datastore</code> remains the query that keeps
only marked datastores.
</p>
<p>
Re-running the rule deletes an attack-path finding whose hop or source finding is gone.
Other rules’ findings stay. <code>om enrich cve</code> does not write attack-path rows. Run
rules again after enrichment so a new CVE is paired with the datastore.
<code>GET /v1/findings</code> returns <code>path</code> on the row. The console prints those
ids on the card. SIEM export includes the same list.
</p>

<h2>Reading an empty result</h2>
<p>
An empty path list means the edges are not there, not that the account is safe. The walk
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/cli.astro
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
</tr>
<tr>
<td><code>om rules run [rule-id]</code></td>
<td>Evaluate one rule or the full catalog and upsert findings.</td>
<td>Evaluate one rule or the full catalog and upsert findings. A full run writes attack-path findings after the control rules.</td>
</tr>
<tr>
<td><code>om paths list</code></td>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/console.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<h2>What you can do</h2>
<ul>
<li>Read node and edge counts in the stats strip. Those numbers come from <code>GET /v1/graph/stats</code>.</li>
<li>Scan the findings list from <code>GET /v1/findings</code>. Each card shows severity, title, and the affected resource. Choosing a card highlights that node when it is in the current snapshot.</li>
<li>Scan the findings list from <code>GET /v1/findings</code>. Each card shows severity, title, and the affected resource. An attack-path card also prints the <code>path</code> node ids. Choosing a card highlights the affected resource when it is in the current snapshot.</li>
<li>The dropdown loads every named query. On first paint it selects <code>internet-to-datastore</code> when that query exists, then draws those paths. Choose <strong>Full graph snapshot</strong> to call <code>GET /v1/graph/snapshot</code> instead (500 nodes and 2000 edges).</li>
<li>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 <code>PATH</code> and any <code>reason</code> property is missing. The nodes themselves come from the query, not from the snapshot cap.</li>
<li>Select an identity node to load <code>GET /v1/identity/blast-radius</code>. The panel shows the summary and the reachable nodes. See <a href="/docs/blast-radius/">Blast radius</a>. Other node types do not open that panel.</li>
Expand Down
5 changes: 4 additions & 1 deletion src/pages/docs/enrichment.astro
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<code>VIOLATES</code> edge. The finding id is
<code>{'finding:{cve-id-lower}:{workload-id}'}</code>, so a second run overwrites the same
row. The edge property <code>relationship</code> is <code>affects</code>. 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 <code>om rules run</code> afterward so a new CVE on a
workload that can reach a datastore is paired with that datastore. See
<a href="/docs/attack-paths/">Attack paths</a>.
</p>
<ul>
<li><code>finding_type</code> is <code>cve</code>.</li>
Expand Down
4 changes: 3 additions & 1 deletion src/pages/docs/exports.astro
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
}`}</code></pre>
<p>
<code>affected_resource_*</code> comes from the <code>VIOLATES</code> 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 <code>path</code>, the ordered node ids, both on the record and inside
<code>finding.properties</code>.
</p>

<h2>Slack</h2>
Expand Down
7 changes: 6 additions & 1 deletion src/pages/docs/getting-started.astro
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ docker compose up -d`}</code></pre>
<p>
Open <a href="http://localhost:8080">http://localhost:8080</a>. The console opens on
<code>internet-to-datastore</code>, 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 <code>internet:global</code>, <code>aws:sg:sg-web</code>,
<code>aws:ec2:i-web-1</code>, <code>aws:iam:role/AdminRole</code>, and
<code>aws:rds:prod-db</code>.
If the page is empty, the CLI and the API are pointed at different databases. See
<a href="/docs/docker/">Docker Compose</a>.
</p>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<ul>
<li><a href="/docs/the-graph/">The graph</a> — nodes, edges, and how findings attach</li>
<li><a href="/docs/schema/">Schema</a> — node types, edge types, and ID format</li>
<li><a href="/docs/attack-paths/">Attack paths</a> — the six named queries</li>
<li><a href="/docs/attack-paths/">Attack paths</a> — the six named queries and attack-path findings</li>
<li><a href="/docs/blast-radius/">Blast radius</a> — what an identity can reach</li>
<li><a href="/docs/prioritization/">Prioritization</a> — how graph context changes a score</li>
</ul>
Expand Down
6 changes: 6 additions & 0 deletions src/pages/docs/prioritization.astro
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,10 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
and type. The API orders them by <code>normalized_score</code>, highest first, and the console
shows that order.
</p>
<p>
The <code>attack-path</code> rule does not use this table. It copies
<code>normalized_score</code> 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
<a href="/docs/attack-paths/">Attack paths</a>.
</p>
</DocsLayout>
24 changes: 16 additions & 8 deletions src/pages/docs/rules.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import DocsLayout from '@/layouts/DocsLayout.astro';

<DocsLayout
title="Open-source CSPM rules"
description="Built-in OpenSourceOM CSPM rules for public datastores, internet-exposed workloads, and admin access to data."
description="Built-in OpenSourceOM CSPM rules for public datastores, internet-exposed workloads, admin access to data, and attack-path findings."
faq={[
{
question: 'What CSPM checks does OpenSourceOM include?',
answer:
'Three built-in rules flag public datastores, workloads reachable from the internet, and admin identities that can access a datastore. A YAML pack adds CIS AWS-inspired property checks.',
'Three built-in rules flag public datastores, workloads reachable from the internet, and admin identities that can access a datastore. An attack-path rule then writes one finding per workload finding that can reach a datastore. A YAML pack adds CIS AWS-inspired property checks.',
},
{
question: 'When should I run CSPM rules?',
Expand All @@ -26,9 +26,10 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
>
<h1>Open-source CSPM rules</h1>
<p>
The rules engine reads the graph and writes <code>Finding</code> nodes. Three checks are
implemented in Go. The rest come from <a href="/docs/rule-packs/">YAML packs</a> embedded in
the binary at compile time. <code>om rules list</code> prints the combined catalog.
The rules engine reads the graph and writes <code>Finding</code> nodes. Three control checks
are implemented in Go. An attack-path pass runs after them. The other checks come from
<a href="/docs/rule-packs/">YAML packs</a> embedded in the binary at compile time.
<code>om rules list</code> prints the combined catalog. <code>attack-path</code> is last.
</p>
<pre><code>{`./om rules list
./om rules run
Expand Down Expand Up @@ -64,11 +65,17 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<td>60</td>
<td>Datastore with a <code>CAN_ACCESS</code> edge from an identity whose <code>admin_access</code> is true. The match forces <code>admin_can_access</code> in graph context.</td>
</tr>
<tr>
<td><code>attack-path</code></td>
<td>source score, or 75</td>
<td>Existing finding on an internet-reachable workload that can reach a datastore. Runs after the other rules. Details are on <a href="/docs/attack-paths/">Attack paths</a>.</td>
</tr>
</tbody>
</table>
</div>
<p>
Every match is then scored with the boosts on <a href="/docs/prioritization/">Prioritization</a>.
Control rules and pack rules are scored with the boosts on <a href="/docs/prioritization/">Prioritization</a>.
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 <code>admin_can_access</code> before scoring, so that match also
receives the +5 admin boost. The other two built-in rules do not set that flag.
Expand All @@ -77,8 +84,9 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<code>internet-to-datastore</code> and still score as not internet-reachable.
</p>
<p>
Each match upserts <code>{'finding:{rule-id}:{resource-id}'}</code> and a
<code>VIOLATES</code> edge from the finding to the resource. Re-running updates that row.
Each control-rule match upserts <code>{'finding:{rule-id}:{resource-id}'}</code> and a
<code>VIOLATES</code> edge from the finding to the resource. The attack-path id is
<code>{'finding:attack-path:{source-finding-id}:{datastore-id}'}</code>. Re-running updates that row.
<code>findings_created</code> counts those upserts, including updates. Pack rules and
<code>cspm-public-datastore</code> consider at most 500 nodes of the target type, ordered by
name. <code>cspm-internet-workload</code> has no row cap. It uses the full
Expand Down
4 changes: 2 additions & 2 deletions src/pages/docs/schema.astro
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
</tr>
<tr>
<td><code>Finding</code></td>
<td>CSPM or CVE result. <code>finding_type</code> is <code>cspm</code> or a CVE id is set.</td>
<td>CSPM, CVE, exposure, or attack-path result. <code>finding_type</code> is <code>cspm</code>, <code>cve</code>, <code>exposure</code>, or <code>attack_path</code>.</td>
</tr>
<tr>
<td><code>Control</code></td>
Expand Down Expand Up @@ -128,7 +128,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
</tr>
<tr>
<td>Finding</td>
<td><code>{'finding:{rule-id}:{resource-node-id}'}</code></td>
<td><code>{'finding:{rule-id}:{resource-node-id}'}</code>. An attack-path finding is <code>{'finding:attack-path:{source-finding-id}:{datastore-id}'}</code>.</td>
</tr>
<tr>
<td>Edge</td>
Expand Down
Loading