When to Use
- Adding or changing any Cypher that writes nodes/relationships/properties, in a
graph_dbmixin or a scan tool that persists to the graph.
For placing a whole new recon tool (which includes its graph write), use
recon-tool-integration; this skill is the
graph-write rules it depends on.
Critical Rules
- NEVER add the tenant key to a reference node, and NEVER omit it from an entity
node. Entity nodes (per-project findings) MERGE on
{<natural_key>, user_id, project_id}- the tenant-isolation triple. A MERGE missinguser_id/project_idmerges one project's data into another's, silently. Global reference nodes (e.g.CVE) key on their natural id only (MERGE (c:CVE {id: $cve_id})); adding tenant keys there fragments shared data. - NEVER edit graph_db/neo4j_client.py directly. It is a thin orchestrator that combines the mixins by inheritance. Graph methods live in the mixin for their domain (see the table below).
- NEVER unconditionally
SETa field another tool owns. UseON CREATE SETfor provenance/first-writer fields (e.g.source) so a later tool merging the same node does not clobber them; use plainSETonly for this tool's own enrichment fields. Reference: graph_db/mixins/graphql_mixin.py:189. - NEVER collect a field in a tool and not write it to the graph. Every field in the tool's output dict must land on a node property or relationship, or it is silent data loss. If it fits no node, map it to the closest property or say why it is dropped.
- NEVER delete a finding a person has touched, and NEVER clear findings up
front. A scan MERGEs its findings (which refreshes
updated_at) and afterwards prunes the ones it did not touch - ingest-then-prune, never clear-then-ingest. A half-failed scan that reported nothing would otherwise empty the project, so the CALLER decides whether to prune and only does so after an ingest that actually produced findings. Muted nodes and ones carryingtriage_source = 'human'are never deleted, only stampedstale_since: they hold an operator's mute, verdict and the fix items written against them. Reference:prune_unseen_findingsin graph_db/mixins/base_mixin.py, and the four clears that spare them. - NEVER write an unscoped
MATCHfor an entity node. Uniqueness is the(id, user_id, project_id)triple, so a natural id is NOT unique across the database andMATCH (n {id: $id})can read or write another project's node. Every read and write carriesuser_id/project_id; agent-facing queries go throughscope_query, neverinject_tenant_filteralone. - ALWAYS reuse an existing node label before inventing one. Discovered
hostnames are
Subdomain, not a new label. Check graph_db/schema_sections.md first - that is the single declaration of every label, property and relationship. - ALWAYS declare a new label / relationship / property in ONE place:
graph_db/schema_sections.md, then re-seed
with
python3 tooling/scripts/seed_schema_catalog.py. A uniqueness key also goes in graph_db/schema_keys.py, from whichschema.pyrenders itsCREATE CONSTRAINTstatements. Do NOT copy the schema into the prompt or into GRAPH.SCHEMA.md: the prompt splices the catalog in at__GRAPH_SCHEMA__, and GRAPH.SCHEMA.md deliberately no longer lists labels at all. Three copies is what drifted, and four tests now fail if you make a fourth. Still updateNODE_COLORSin webapp/src/app/graph/config/colors.ts, which is presentation, not schema.
MERGE: the copy target
cypher// entity node - tenant-scoped: the {natural key, user_id, project_id} triple is mandatory MERGE (bu:BaseURL {url: $baseurl, user_id: $user_id, project_id: $project_id}) ON CREATE SET bu.source = 'graphql_scan', bu.updated_at = datetime() // provenance: first writer only MERGE (e:Endpoint {path: $path, method: 'POST', baseurl: $baseurl, user_id: $user_id, project_id: $project_id}) ON CREATE SET e.source = 'graphql_scan' SET e += $props // this tool's own enrichment fields MERGE (bu)-[:HAS_ENDPOINT]->(e) // reference node - global: natural id only, NO tenant key MERGE (c:CVE {id: $cve_id})
Copied from graph_db/mixins/graphql_mixin.py.
Which mixin
| Writing | Mixin |
|---|---|
| core recon phases (subdomains, IPs, ports, HTTP, endpoints) | recon_mixin.py |
| passive OSINT enrichment | osint_mixin.py |
| secrets / credentials | secret_mixin.py |
| vuln scan (GVM) | gvm_mixin.py |
| GraphQL probes | graphql_mixin.py |
| supply-chain packages | supply_chain_mixin.py |
Resources
- graph_db/schema_sections.md - THE declaration: every label, property and relationship
- graph_db/schema_keys.py - each label's uniqueness key; schema.py renders its constraints from it
- docs/readmes/GRAPH.SCHEMA.md - the rationale: design principles, tenancy strategy, the Muted label. No longer lists labels
- graph_db/neo4j_client.py - the mixin MRO (do not edit; edit a mixin)
- Related skill:
recon-tool-integration

