← back

Multi-agent security research workstation

Self-hosted. Three months, ~800 commits. Produced an Intigriti 10.0 / Exceptional report.

What it is

A self-hosted multi-agent system that runs the whole vulnerability-research loop: reconnaissance, hypothesis generation, evidence accumulation, adversarial chain-of-reasoning audit, and report drafting. Targets are live bug-bounty programmes.

Output across the first three months: an Intigriti 10.0 / 10.0 Exceptional rating against CM.com's admin API (broken object-level authorization, cross-tenant — redacted methodology below); authenticated SSRF via webhook header injection on Cambium Networks; OAuth/PKCE flow analysis on Venly; responsible-disclosure on Bild.de NewsBot (system-prompt reconstruction + multi-turn prompt injection bypassing the bot's four stated rules); AI/ML supply-chain disclosures on MLflow, Keras, and Vanna.

Neo4j (knowledge graph) Postgres + pgvector Anthropic SDK / MCP multi-model routing — Claude / DeepSeek / local Qwen / Ollama browser pool with Chrome DevTools Protocol ~18 self-built plugin modules

Architecture, top-down

Four components hold the system together.

1. Shared knowledge graph as a common whiteboard

Facts, hypotheses, refutations, goals — all live in one Neo4j graph that every agent reads and writes. The schema went through repeated rebuilds. An early hypothesis-centric design encouraged agents to invent claims and back-fill evidence after; it was restructured so that facts come first and hypotheses must grow out of them. The graph currently holds ~380,000 stored facts.

Threat ontologies (CVE / CWE) are modelled as graph nodes for native agent retrieval. Hybrid retrieval: vector-semantic plus keyword. Subagent role splits (Plan / Explore) keep the context window of any single agent narrow.

2. The agent roles

Per-target long-running sessions with role-scoped system prompts:

Plus three global skill agents that can be spawned by any of the above: observer (returns to the field with proof), operator (runs structured checklists), plan (steps a question through to an actionable plan without executing).

3. Evidence accumulation, not conclusion-first

Observations enter the graph as small atomic facts with a single label (confirmed / candidate / unverified). Confirmed = runtime-verified; candidate = source-code or static evidence; unverified = inference. A finding is not declared without confirmed evidence at both source and sink.

4. The adversarial review loop

Discussed in detail below. This is the design decision that shaped everything else.


Five generations of "good enough"

The workstation has been rewritten almost continuously. Each generation redefined what "perfect" meant — the previous generation's perfect became the next generation's joke.

Generation 1 — AI as a crutch

Early targets, including Venly's OAuth/PKCE work. I would hand Claude what I didn't understand and let it propose hypotheses. The model was too generative — twenty "if X then Y" chains, all plausible, none evidenced. The "perfect" of this generation was "agent gives me more hypotheses." Wrong axis.

Generation 2 — line-review

Borrowed straight from how Claude Code itself reviews diffs: the reviewer doesn't see your summary of intent, only the diff in front of it. Better, but the reviewer was still grading the working context's own immediate output — not the chain of reasoning that produced it.

Generation 3 — the hypothesis engine (failed)

I tried to fight fabrication head-on by making Hypothesis a first-class node type. It failed. Once "hypothesis" had a slot, the agent fabricated more freely, not less. The lesson — which I now carry across every system I design — is: don't give the model a scaffold that smooths the path for the failure mode.

Generation 4 — if-then controller (failed)

External rules bounding agent action. Wrong again. The rules were either rigid (missed cases) or loose (no constraint). Static rules don't tame a dynamic system.

Generation 5 — evidence-first triple store (current)

Facts deposit first; hypotheses grow out of facts; an adversarial reviewer audits the reasoning chain. This is what's running now. I already know there will be a sixth.


The decision: the adversarial reviewer reads the thinking trace, not the conclusion

The intuitive design is to give a "red team" agent the positive agent's final claim ("this is a vulnerability") and let it judge correctness. I went the other way. The reviewer never sees the conclusion. Only the reasoning chain.

Why

LLMs anchor. Once the model is given a conclusion, its next-token distribution skews toward "go along with this conclusion" — whether the response surface is "support" or "rebut." The anchor is already shaping the response. That isn't a prompt-engineering bug to patch around; that's the transformer's probability behaviour under anchoring.

LLM sycophancy is anchoring. Don't give the model the anchor.

Input shape

What the reviewer actually receives is not "the positive agent says X is a vulnerability — audit it." It is closer to:

"On step 3 of its reasoning, the positive agent claimed: this 200 OK is anomalous, because nearby endpoints return 403. Judge that single inferential step. Does the reasoning hold given the supporting facts referenced from the graph?"

The reviewer sees one reasoning hop at a time. It has no awareness of what larger story the hop belongs to. It can only return yes/no based on whether that single causal jump holds, against the facts the positive agent cited from the graph.

The flow, drawn out

   GRAPH                         POSITIVE                    ADVERSARIAL
   (facts)                       AGENT                       REVIEWER

   evidence ─────────────────►  step 1: claim A
                                   |
                                  cited fact ──────────────► judge step 1
                                                             [yes]
                                step 2: claim B
                                   |
                                  cited fact ──────────────► judge step 2
                                                             [yes]
                                ...
                                step N: claim FINAL
                                   |
                                  cited fact ──────────────► judge step N
                                                             [yes]

   chain accepted only when every hop survives independent audit.
   reviewer never sees the conclusion N until after it has signed off
   on every step that produced it.
  

Why this matters — the CM.com chain only formed because of this

The positive agent wanted to drop the 200 OK as too weak a signal. I pushed it to keep asking why. The reviewer then audited each premise independently:

At each audit, the reviewer had no idea the chain would terminate in 69 customer gateway credentials across 9 organisations. It was only judging local causality. Every hop survived. The chain stood.


This is a root-cause pattern, not a one-off trick

The same shape appears in three other places I've watched closely:

Different surface failures. Same root pattern:

Let an outside context judge what the working context can't see — while the working context is still alive.

Once I had that abstracted, I started using it across the workstation. It now runs in four places:

  1. The adversarial reviewer over chains of reasoning (described above).
  2. End-of-subtask external verifiers — when an agent finishes a step, a separate context confirms the artifact actually exists in the graph and matches the spec.
  3. Spec-vs-runtime two-way diff — running code is taken as ground truth, and stale documentation is automatically flagged for archival.
  4. Periodic self-feedback — agents append flow / tool / sp / efficiency / stuck entries to dated jsonl files; a separate context reads and acts on them.

A working agent can't reliably detect its own gaps. That's a logically false ask of a single context.


Case study: CM.com — Intigriti 10.0 / 10.0 Exceptional

Redacted methodology only. No endpoints, customer names, PoC payloads, or credentials.

Bug class

Broken object-level authorization (BOLA / IDOR-style) across an admin API surface. Cross-tenant read and write on configuration objects belonging to other organisations.

How the chain formed

Weak signal. A scout agent operating from a free-tier account observed a single admin-side path returning 200 OK. The default path of least resistance — drop the signal as noise. I pushed instead: ask why this single endpoint behaves differently from the family of nearby paths. Ask what object it returned. Ask what 403 vs 500 vs 200 means in this product's tenant model.

Mapping by behaviour, not by endpoint name. The next move was to capture browser/API traffic during ordinary product use, map endpoint families, and pay attention to identifier shapes — anything that looked like organisation, customer, route, handler, or gateway IDs. The crucial question was not "is this endpoint documented?" but "does the backend enforce tenant ownership, or does it merely accept object identifiers without re-validation?"

Read first, write second, narrow PoC. Cross-tenant read access was confirmed first, against routing-configuration objects and message-handler records. Only after read was confirmed did I move to state-changing methods (PUT / POST / DELETE), and only on the smallest reversible modification path I could verify. Destructive testing avoided. Returned payloads exposed customer-side credentials, which were validated as usable while keeping verification minimal and documented.

Impact

The remediation recommendations I submitted

What this finding taught me about my own workflow

Earlier OAuth / PKCE work taught me that AI-generated hypotheses are powerful and dangerous on the same axis: an agent can quickly produce many plausible "if X, then Y" chains, but a real exploit has no room for unsupported "ifs." That failure pushed me from a hypothesis-first architecture to an evidence-first one. The CM.com chain was the first case where the new architecture earned its keep — small observation units, hypotheses grown out of them, and an adversarial reviewer auditing the reasoning chain step by step before the chain was trusted.


What's still wrong

The system is not finished and the next generation is already visible:

"Perfect" is not a static endpoint. It's a moving standard. I redefine it about every two weeks as my understanding of the system changes.

← back