Skip to content

Add explainable AWS exposure rules - #47

Merged
JetSquirrel merged 5 commits into
mainfrom
feat/exposure-rules
Nov 3, 2026
Merged

JetSquirrel merged 5 commits into
mainfrom
feat/exposure-rules

Conversation

@JetSquirrel

@JetSquirrel JetSquirrel commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Adds khonsu plugin analyze aws exposure, which says which instances an AWS account's configuration leaves potentially open to the internet, and shows how it decided. The rules live in the AWS plugin, not in KhonsuGraph's core, which is the correction #55 asked for.

Merge this first in the chain. #46 has landed. This targets main. Please merge these with merge commits, not squash or rebase, or each later branch stops containing the one before it (#55).

The boundary (#55)

Plugins can now offer analyses, an optional part of the protocol. A plugin owns what a finding means, and the host owns the graph:

Where it lives
The three rules, AWS field mappings, assumptions, verdict policy, their Chinese text plugins/aws/src/exposure.rs
Protocol types (AnalysisInfo, AnalyzeRequest, FindingRecord, PathRef) khonsu-plugin-protocol, provider-neutral
Reading a bounded part of the graph, checking the answer, rebuilding paths and evidence crates/khonsu-core/src/plugin_analysis.rs
  • The plugin only points at records. It answers in stable references. The host looks field values up itself, and checks that each path connects and that each reference, rule and verdict is one it knows. So a plugin can't misquote a record or claim a connection the graph doesn't have.
  • A bad answer is refused whole, because dropping one finding would read as that resource having none.
  • Paths survive the process boundary. hops and path_graph were serde(skip), so a direct move to JSON would have lost "Show path on canvas". A path now crosses as path_refs and is rebuilt from the graph.
  • Other plugins are unaffected. analyses is optional. crates/khonsu-plugin-protocol/examples/analyzer.rs is a small analyzer with nothing to do with AWS, which the tests install to exercise the interface and its failure cases.

Review findings fixed

All three gave false readings, and each now has a regression test:

Finding Before Now
Missing balancer group data not_met, which cleared the finding unknown, so insufficient_data
UDP through a TCP_UDP target group Treated as TCP only, so a UDP rule didn't count Either protocol counts. TCP_QUIC, UDP, QUIC and GENEVE are handled too
Target security group membership Not in the rule's evidence In the condition's fields and contributing

Output changes

Compared field by field against the golden file from before the move, every finding ID, rule ID, verdict, order and condition text is unchanged. Only these differ:

  • The command is khonsu plugin analyze aws exposure. khonsu exposure is gone, and since it never shipped there's no alias.
  • aws_field is now source_field, and instances is now subjects.
  • A relationship's end reads as the field @to, rather than PROTECTED_BY.
  • Added: plugin, analysis, title, verdicts, and path_refs on a finding. A target's group membership adds a record to contributing on load balancer findings.
  • order no longer says "There is no risk score". It's written by the host now, which doesn't know that. docs/exposure-rules.md still says it.

What it does

Each rule is a list of conditions, each met, not_met or unknown, and each names the exact fields it read. The verdict follows from those alone, with no score:

Verdict When
potential_exposure Every condition is met
insufficient_data None is unmet, and at least one is unknown
not_indicated At least one is unmet

No verdict says "reachable". The three rules:

  • direct-instance: running, public address, open security group rule, and an active default route to an internet gateway.
  • public-load-balancer: an internet-facing balancer with an open group, a healthy target, and a target group that allows the port from the balancer.
  • security-group-lateral: one security group rule behind an instance with a potential_exposure or insufficient_data finding.

A finding's path is a derived evidence record with its rule_id, and walk holds the arguments to reopen it with khonsu explore. The host works walk out, since it's a graph concept and not an AWS one. docs/exposure-rules.md has the rules, what they don't read, and a walkthrough.

Results on the new fixture

Instance Rule Verdict Why
web-1 direct potential_exposure All four conditions are met
app-1 load balancer potential_exposure All five conditions are met
db-1 lateral potential_exposure Its group allows web-1's group
legacy-1 direct insufficient_data No route tables are saved for its VPC
batch-1 direct not_indicated Port 22 is open, but it has no public address or internet route

app-1 and db-1 also get a not_indicated direct finding, for seven findings in total.

Choices to review

  • Plugin: it now also calls ec2 describe-route-tables, adding RouteTable nodes and USES_ROUTE_TABLE and ROUTES_VIA relationships, and it stores an instance's IPv6 address. The existing ec2:Describe* permission covers this, but a policy that lists actions one by one would now fail the sync.
  • Missing data reads as unknown: a saved response that's missing reads as empty ([v0.4] Sync completeness, source epochs and recoverable partial imports #38), so an instance with no groups or a subnet with no route table gives unknown, not a clear result. Graphs synced before this change give insufficient_data on the direct rule until they're synced again.
  • Separate fixture: the cases live in plugins/aws/tests/fixture-exposure. Changing the shop fixture would have invalidated the HawDB 0.24.0 snapshot, and the golden diff here only adds lines.
  • An existing candidate path now reads differently: the shop fixture's load balancer path to web-1 gets not_indicated, because its target group uses port 8080 and web-1's group doesn't allow that port. The path query's answer hasn't changed.

Limits

Each finding lists what its rule didn't read, including network ACLs and load balancer listeners. Two gaps can still produce a wrong answer:

  • If one of an instance's several security groups is missing from the responses, nothing detects it.
  • Rules that together cover the address space, such as 0.0.0.0/1 plus 128.0.0.0/1, aren't recognized as the internet.

Testing

  • cargo test --locked --workspace passes on Windows, and CI will cover macOS.
  • I ran the walkthrough end to end on Windows before the move. After it I checked the same fixture through the tests and by running the analysis in the window. I didn't repeat each walkthrough command by hand.
  • Not tested: a real AWS account, including the new describe-route-tables call. Only saved responses were used.

Issue status

Refs #55, #35, #33, #36

🤖 Generated with Claude Code

JetSquirrel and others added 4 commits November 2, 2026 15:25
The canvas could only add a node's neighbors, one hop and untyped, with no
sign when the fixed limit cut the result. An investigator following a
suspected chain needs direction, types and caps, and needs to know when
the answer is incomplete.

Graph::explore walks breadth-first with one parameterized query per step,
so caps and cancellation are enforced between queries, and says which cap
cut the result. khonsu explore offers it to agents and scripts, and the
inspector gets a panel with the hops to the selected node.

gpui-graph cannot highlight a set of nodes, so showing a path narrows the
canvas to it, and Back returns to earlier views.

Refs #32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A relationship on the canvas said nothing about where it came from. An
investigator needs the source, the time and whether it was reported or
worked out, and needs missing values stated rather than filled in.

Each provenance field is explicitly known or unknown. khonsu evidence and
explore --to give stable IDs, and the inspector shows the same record with
buttons to copy it as JSON. Exports hold no source configuration, and
secret-looking names and values are masked and listed.

A sync does not rewrite an unchanged relationship, so its timestamp is
named record_written_at, separate from the source's last successful sync.

Refs #33

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add evidence records: what supports a node, relationship or path
An open security group rule or a walkable path isn't an actionable finding.
An investigator needs to know why a resource was flagged, which fields say
so, and what was never checked.

khonsu exposure evaluates three documented rules. Each condition is met,
not met or unknown and names the fields it read. The verdict follows from
those alone: potential_exposure, insufficient_data or not_indicated. There
is no score, and no verdict says reachable.

The AWS plugin now reads route tables and IPv6 addresses, because a public
address and an open rule mean little without a route. A record that should
exist and doesn't gives unknown, not a clear result, since a missing saved
response reads as empty (#38).

A finding carries its path as a derived evidence record with its rule ID,
plus the arguments to reopen it in khonsu explore.

The new cases live in their own fixture, so the shop fixture's answers and
the HawDB snapshot stay as they were.

Refs #35, #33, #36

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@JetSquirrel JetSquirrel left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architectural concern: AWS-specific exposure rules should live in the AWS plugin, while the core retains generic path exploration and evidence capabilities.

This PR goes beyond adding an AWS example or data-source integration: crates/khonsu-core/src/exposure.rs hard-codes EC2, security group, load balancer, and route-table semantics, and khonsu exposure exposes that provider-specific analysis as a general CLI capability. AWS collection in plugins/aws, plus AWS examples and fixtures, fits the plugin boundary; embedding these rules in the core makes the general-purpose graph workbench responsible for AWS domain behavior and encourages similar additions for every future provider.

Please move the three AWS exposure rules and their AWS-specific assumptions/field mappings into the AWS plugin. Keep reusable graph traversal, evidence records, and path provenance in khonsu-core, and expose the AWS analysis through an explicitly provider-scoped command or plugin capability. If shared finding/condition types are needed, they can remain provider-neutral without placing AWS evaluation logic in the core. A general rule DSL is not necessary for this change.

I recommend addressing this boundary before merging, so AWS remains an integration built on the workbench's generic capabilities.

KhonsuGraph is a general graph workbench, but its core held AWS semantics:
EC2, security group, load balancer and route table rules, and a general
`khonsu exposure` command for them. Every new provider would have added more.

Plugins can now offer analyses, an optional part of the protocol. The host
reads a bounded part of the graph and sends it with stable references to each
record. The plugin answers with findings that point at those records. The host
checks every reference, looks up field values itself, and rebuilds each path
from the graph, so a plugin can't misquote a record or claim a connection that
isn't there. A bad answer is refused whole, since dropping a finding would read
as that resource having none. Nothing in the host names a provider's labels,
rules or verdicts.

The three rules, their field mappings, assumptions and verdict policy are now in
plugins/aws/src/exposure.rs, run with `khonsu plugin analyze aws exposure`.
Finding IDs, rule IDs, verdicts, order and condition text are unchanged. In the
output, `aws_field` is now `source_field`, `instances` is now `subjects`, and a
relationship's end reads as the field `@to`.

Three false readings found in review are fixed, with regression tests:

- Missing balancer group data read as "the target's rules don't allow it",
  clearing the finding. It's now unknown.
- A TCP_UDP target group was treated as TCP only, so a UDP rule didn't count.
- The target's security group membership wasn't in the rule's evidence.

An example analyzer with nothing to do with AWS shows the interface, and tests
how the host handles an answer that misbehaves.

Refs #55, #35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JetSquirrel
JetSquirrel merged commit b849481 into main Nov 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant