Repository navigation
Add explainable AWS exposure rules - #47
Conversation
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
left a comment
There was a problem hiding this comment.
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>
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:
plugins/aws/src/exposure.rsAnalysisInfo,AnalyzeRequest,FindingRecord,PathRef)khonsu-plugin-protocol, provider-neutralcrates/khonsu-core/src/plugin_analysis.rshopsandpath_graphwereserde(skip), so a direct move to JSON would have lost "Show path on canvas". A path now crosses aspath_refsand is rebuilt from the graph.analysesis optional.crates/khonsu-plugin-protocol/examples/analyzer.rsis 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:
not_met, which cleared the findingunknown, soinsufficient_dataTCP_UDPtarget groupTCP_QUIC,UDP,QUICandGENEVEare handled toocontributingOutput 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:
khonsu plugin analyze aws exposure.khonsu exposureis gone, and since it never shipped there's no alias.aws_fieldis nowsource_field, andinstancesis nowsubjects.@to, rather thanPROTECTED_BY.plugin,analysis,title,verdicts, andpath_refson a finding. A target's group membership adds a record tocontributingon load balancer findings.orderno longer says "There is no risk score". It's written by the host now, which doesn't know that.docs/exposure-rules.mdstill says it.What it does
Each rule is a list of conditions, each
met,not_metorunknown, and each names the exact fields it read. The verdict follows from those alone, with no score:potential_exposureinsufficient_datanot_indicatedNo 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 apotential_exposureorinsufficient_datafinding.A finding's
pathis a derived evidence record with itsrule_id, andwalkholds the arguments to reopen it withkhonsu explore. The host workswalkout, since it's a graph concept and not an AWS one.docs/exposure-rules.mdhas the rules, what they don't read, and a walkthrough.Results on the new fixture
potential_exposurepotential_exposurepotential_exposureinsufficient_datanot_indicatedapp-1 and db-1 also get a
not_indicateddirect finding, for seven findings in total.Choices to review
ec2 describe-route-tables, addingRouteTablenodes andUSES_ROUTE_TABLEandROUTES_VIArelationships, and it stores an instance's IPv6 address. The existingec2:Describe*permission covers this, but a policy that lists actions one by one would now fail the sync.unknown, not a clear result. Graphs synced before this change giveinsufficient_dataon the direct rule until they're synced again.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.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:
0.0.0.0/1plus128.0.0.0/1, aren't recognized as the internet.Testing
cargo test --locked --workspacepasses on Windows, and CI will cover macOS.describe-route-tablescall. Only saved responses were used.Issue status
Refs #55, #35, #33, #36
🤖 Generated with Claude Code