#Attack Graph
Nyx Agent stores an attack graph as a run-scoped index over the artifacts
the scanner and agent pipeline already persist. It does not replace the
source tables; nyx_signals, pentest_candidates,
verification_attempts, verified_vulnerabilities, chains,
route_models, business_logic_template_runs, and
exploration_memory remain
authoritative. The graph gives those records a
common set of nodes and edges so later UI and report surfaces can answer
provenance and blast-radius questions without hand-joining every table.
Source: crates/nyx-agent-core/src/store/attack_graph.rs,
crates/nyx-agent-core/migrations/0001_v1.sql,
crates/nyx-agent-types/src/attack_graph.rs.
#Schema
Two tables back the graph:
| Table | Purpose |
|---|---|
attack_graph_nodes |
Run-scoped nodes with kind, stable_key, optional ref_id, display label, and JSON properties. |
attack_graph_edges |
Directed relationships between nodes, with an edge kind, optional evidence_ref, and JSON properties. |
Node ids and edge ids are deterministic BLAKE3-derived ids over their run, kind, and stable identity. Replaying the same artifact write for a run converges on the same graph row.
ref_id points back to the owning artifact when there is one, such as a
nyx_signals.id, pentest_candidates.id, verification_attempts.id,
verified_vulnerabilities.id, or chains.id. Route, endpoint,
parameter, role, and object nodes usually rely on stable_key instead.
#Node Kinds
The shipped graph writers can represent these node kinds:
| Kind | Meaning |
|---|---|
route |
A discovered application path, shared across frontend, backend, and API-client evidence for the same run. |
endpoint |
A method-specific backend route, API client call, or concrete request target. |
form |
A discovered HTML/JSX form, including action, method, and field metadata when available. |
parameter |
Path, query, body, tenant, or owner parameter discovered from route models. |
role |
Auth role or role-like check such as authenticated. |
object |
Application resource, service, model, or source object, including route resources and file locations. |
signal |
Static or scanner signal, including placeholder source nodes for external scanner leads. |
candidate |
Pentest candidate from Nyx signals, optional scanners, or AI candidate finding discovery. |
business_logic_template |
Registered template provenance for template-generated candidates. |
verification_attempt |
Live verification attempt that exercised a candidate or chain. |
verified_vulnerability |
Confirmed vulnerability row. |
chain |
Chain reasoning row and its members. |
exploration_memory |
Durable lesson from a prior exploration or verifier result. |
#Edge Kinds
The graph keeps edge labels intentionally small:
| Kind | Typical direction |
|---|---|
discovered_from |
Reserved for future importer provenance. |
targets |
Candidate, attempt, vulnerability, signal, or endpoint points at a route, endpoint, or parameter. |
uses_role |
Route, endpoint, candidate, or vulnerability depends on a role. |
touches_object |
Route, endpoint, signal, candidate, or vulnerability touches a resource or file object. |
derived_candidate |
Signal/source node produced a candidate. |
verified_as |
Candidate or verification attempt led to a verified vulnerability. |
chained_with |
Chain relates to member signals or vulnerabilities. |
learned_from |
Exploration memory points back to the candidate or verification attempt that produced the lesson. |
#Population
Graph rows are dual-written by the existing store accessors:
RouteModelStore::upsertrecords route, endpoint, form, parameter, role, and object nodes, then links route-source locations to existing Nyx signal nodes when line information overlaps. Semantic App Model v2 fields are mirrored into endpoint properties and graph links: framework, handler, middleware/auth checks, role checks, path/query/body fields, tenant and owner fields, service calls, model names, resource names, response hints, and side-effect classifications.NyxSignalStore::insertrecords signal nodes and file object links.PentestCandidateStore::insertandCandidateFindingStore::insertrecord candidate nodes, source edges, and target/object/role links. Business-logic candidates additionally link their template node to the candidate and expose route, role, and object touch points from structured template metadata.VerificationAttemptStore::insertrecords attempt nodes and request targets.VerifiedVulnerabilityStore::upsertrecords verified vulnerability nodes and links source candidates, source signals, verification attempts, chains, and affected components.ChainStore::insertrecords chain nodes andchained_withedges to member nodes when possible.ExplorationMemoryStore::upsertrecords memory nodes, links them to candidate and verification-attempt provenance when available, and mirrors endpoint, role, and object context as graph links. These rows are durable across runs and are consumed by future AI exploration prompts and relevance ranking.
Because the graph is derivative, reports remain compatible with older
consumers. Existing report.json, run cards, vulnerabilities, findings,
and chains keep their current shapes.
#Queries
Store::attack_graph() exposes graph queries for vulnerability evidence,
blast-radius lookup, and chain planning:
evidence_for_vulnerability(vulnerability_id)walks inbound graph edges from a verified vulnerability and includes directly connected target, role, object, and chain context. This answers "what evidence led to this vuln?"vulnerabilities_touching(run_id, kind, stable_key)starts from a route, object, role, or other graph node and walks connected graph edges to verified vulnerabilities. This answers "what vulns touch this route/object/role?"candidate_to_route(run_id, candidate_id)returns the candidate's graph-backed route, endpoint, parameter, role, object, source-signal, and verification context.route_to_role_object(run_id, route_stable_key)returns the route's endpoint/form context plus role and object edges. This is the compact "what does this route require and touch?" query.vuln_to_object_role(run_id, vulnerability_id)returns a confirmed vulnerability's target, role, object, candidate, verification, and chain context.cross_repo_service_edges(run_id)returns service-like target/object edges that cross repository boundaries when both sides carry repo metadata.chain_planning_input(run_id, max_chains)builds the compact ChainReasoning input from graph nodes and edges. It includes candidates, signals, routes, endpoints, forms, parameters, roles, objects, verification attempts, verified vulnerabilities, and business-logic template provenance.
#Chain Planning
ChainReasoning is graph-native and runs after individual candidate/live
verification has had a chance to add verification_attempt and
verified_vulnerability nodes. The planner consumes the attack graph
neighborhood instead of only static finding-flow summaries. Graph nodes
are passed with stable graph ids, artifact ref_ids when present, route,
role, object, and evidence-ref context. Graph edges are passed with edge
ids, labels, evidence refs, source tags, and cross-repo flags.
When the selected runtime supports agent loops (Claude Code or Codex), ChainReasoning runs in source-aware mode: the task receives repository workspace roots and can use read/search/shell inspection tools to look around the code before producing chain JSON. One-shot-only runtimes fall back to the compact graph-only prompt. Both modes pass through the same validation gate: every returned member id must exist in the graph and every adjacent pair must be backed by a graph edge.
The model contract ranks chains with:
| Field | Meaning |
|---|---|
member_ids |
Ordered graph node ids. Every adjacent pair must be connected by an input graph edge. |
rationale |
Human-readable exploitability rationale. |
prerequisites |
Required attacker state, roles, tenant/object state, or route reachability. |
evidence |
Specific graph-backed facts supporting the chain. |
blast_radius |
Affected routes, roles, objects, services, repos, or tenant boundaries. |
confidence |
Integer confidence from 0 to 100. |
missing_verification_steps |
Proof still needed before the chain is confirmed. |
edge_provenance |
Edge ids or evidence refs supporting the member-to-member links. |
The AI task validates that every member id exists and that every adjacent member pair is backed by an input edge. This prevents weak leads from being promoted into serious chains unless the graph contains route, object, role, signal, candidate, verification, or service evidence for the link.
Persisted chains.member_ids remains the ordered member id list for
compatibility. The structured graph proof is persisted in
chains.evidence_blob with schema_version = 1, including member
metadata, edge provenance, prerequisites, evidence, blast radius,
confidence, terminal live-proof state, and missing verification steps.
Chains that terminate in a live verification attempt or verified
vulnerability, with no model-declared proof gaps, are stored as
Verified and materialized into verified_vulnerabilities with
chain_id set. Chains without terminal live proof are stored as
NeedsChainVerification and keep the missing proof steps in
evidence_blob. ChainStore::insert still dual-writes the chain node
and chained_with member edges into the graph.
The chain UI reads the structured evidence_blob to show graph-backed
paths, edge evidence, confidence, blast radius, and missing proof gaps.
Older chains without this blob still render their rationale and member
ids.