Most agent demos stop at an answer. Ours had to take an action in production—the kind of action that pages people at 3 a.m. if it is wrong.
Building AegisGraph for Graph Hacks (WeMakeDevs x FalkorDB, Track 01: Agents That Act on Connected Data) taught us that an agent's ability to act safely depends almost entirely on the shape of its context. Flat vector chunks were not enough. A knowledge graph was.
The question an incident actually asks
A WAF alert says forged bearer tokens are being accepted by auth-token-service, and points at jwt-crypto-core. The on-call engineer needs answers to three things at once:
- What customer data can an attacker reach from here?
- What can we cut to stop it without taking checkout down?
- Who should fix the code?
Each answer lives in a different system. Package imports live in the repositories. Service-to-service traffic lives in the mesh. Data access lives in cloud IAM.
The real answer is a multi-hop path across all three:
CVE → Package → Function → Service → Service Mesh → IAM Role → Database
In our demo topology that is five to eight hops, and there are six such paths leading into 3 critical PII databases.
A vector store finds text that sounds like JWT bugs. It cannot tell you that legacy-billing-bridge sits on all six paths.
One graph for code, runtime and identity
We modelled the three layers as one FalkorDB property graph:
-
Code:
(:Vulnerability)-[:AFFECTS]->(:Package)<-[:IMPORTS]-(:Function)<-[:MAINTAINED]-(:Developer), plus(:Function)-[:CALLS]->(:Function) -
Runtime:
(:Service)-[:DEPLOYS_CODE]->(:Function)and(:Service)-[:ROUTES_TO]->(:Service) -
Identity and data:
(:Service)-[:ASSUMES_ROLE]->(:IAMRole)-[:CAN_ACCESS]->(:Database)
The whole blast radius then becomes one OpenCypher query:
cypher
MATCH (v:Vulnerability {id: $cve_id})-[:AFFECTS]->(:Package)<-[:IMPORTS]-(f:Function)<-[:DEPLOYS_CODE]-(origin:Service)
WITH DISTINCT origin
MATCH attack = (origin)-[:ROUTES_TO*0..3]->(pivot:Service)-[:ASSUMES_ROLE]->(role:IAMRole)
-[:CAN_ACCESS]->(db:Database {contains_pii: true})
WHERE ALL(r IN relationships(attack) WHERE coalesce(r.status, 'ACTIVE') = 'ACTIVE')
RETURN pivot.name, role.name, db.name, [n IN nodes(attack) | n.name] AS path
Top comments (0)