Audit a Java backend
Run a read-only, proof-anchored security audit of a Java backend using one multi-agent prompt.
Verified 2026-08-14 · Written against Spring Boot 4 / Spring Security 7, Spring Modulith, JPA/Hibernate, Jackson, Spring Kafka, Flyway, Keycloak, Testcontainers. Reviewed adversarially by two independent models.
How to run it
Copy the one block below and paste it into an agent that can launch subagents.
That is the whole procedure. Replace $ARGUMENTS with a path if you want to
scope it, or leave it for the whole repository.
You do not paste anything else. The worker briefings are inside the prompt — the master hands each subagent its own briefing when it launches it.
Read-only, on a codebase you own. It never writes to the repository, and it never touches a deployed environment.
The prompt
You are the lead of an authorised, read-only Java application security audit of a
codebase we own. You own coverage, evidence quality, adjudication and reporting.
You do not audit directly except to verify: agreement between subagents is not proof.
TARGET: $ARGUMENTS (default: the whole repository)
NEVER print a credential value — no token, connection string, cookie, private key
or signed URL, not even truncated. Record the name, the location, and whether it is
hardcoded, referenced or unresolved.
THREAT ACTORS. Not only an HTTP caller. Treat each as potentially adversarial until
evidence says otherwise: anonymous callers; authenticated users of another tenant;
privileged users; compromised service accounts; message producers; data persisted
earlier and reprocessed later; uploaded files; tool or MCP clients; administrators;
dependencies and build infrastructure.
═══ PHASE 0a — EVIDENCE MANIFEST ═══
Launch ONE subagent with briefing [B-RECON]. Wait for it. Do not proceed without it.
═══ PHASE 0b — PATTERN SWEEP ═══
Launch ONE subagent with briefing [B-SWEEP]. Wait for it.
═══ PHASE 1 — WORKERS (launch all of these in parallel) ═══
- One subagent per application module, each with [B-COMMON] + [B-MODULE] and the
module name. If more than eight modules exist, order by exposure, take the top
eight, and name the deferred ones in the report.
- One subagent with [B-COMMON] + [B-SECURITY-RUNTIME]
- One subagent with [B-COMMON] + [B-DATA-RUNTIME]
- One subagent with [B-COMMON] + [B-MESSAGING-JOBS]
- One subagent with [B-COMMON] + [B-BUILD-DEPLOY]
Give every worker the evidence manifest and the pattern-sweep hit list.
═══ PHASE 1c — ABUSE CASES (after Phase 1 returns) ═══
Launch ONE subagent with [B-COMMON] + [B-ABUSE], plus every candidate returned so far.
═══ PHASE 2 — ADJUDICATION (you, not a fan-out) ═══
Reject any candidate that did not arrive through the proof gate — return it instead
of guessing. Adjudicate in batches grouped by root cause and owning module. Open the
cited code yourself; the manifest is evidence, not truth.
Independently re-verify:
- every candidate whose impact would be Critical or High;
- every Conditional candidate that would be High or Critical if resolved;
- one rejected candidate per entry-point kind per worker;
- every manifest entry for which no worker returned either a candidate or an
explicit "checked, control present" record.
Do NOT reject a candidate because a deployment fact is missing. Mark it Conditional
and name the single fact that would resolve it.
DYNAMIC CONFIRMATION — for surviving P0 and P1 findings only, and only if the
repository can stand itself up in isolation: execute the attack path against a
disposable local instance in a scratch worktree, and paste request and response as
evidence. Never against a shared or deployed environment. If you cannot, say so and
keep the finding as statically reached.
═══ PHASE 3 — REPORT ═══
Four sections, never merged:
CONFIRMED — complete attack path, control verified absent or defeated.
CONDITIONAL — credible path blocked only by an unknown fact, which is named.
HARDENING — a control worth adding, with no demonstrated path.
COVERAGE — what could not be examined, and why.
Dedupe only when one corrective change fixes every site; keep every affected site
listed under the root finding.
PRIORITY = likelihood × business impact, after verified controls. Not a CVSS score:
base scores measure severity rather than risk, the environment is the whole point of
an internal audit, and vector arithmetic gets fabricated.
Likelihood High — reachable by the stated actor, repeatable, no unverified prerequisite
Medium — reachable with one credible prerequisite, or limited privilege
Low — needs rare timing, privileged access, or several independent failures
Impact Critical — systemic privilege compromise, broad cross-tenant compromise,
or irreversible material loss
High — cross-tenant access, privileged action, sensitive-data compromise
Moderate — single-tenant or bounded-record compromise, recovery available
Low — narrow defence-in-depth failure with a demonstrated consequence
Tie-break by exposure, affected population, whether it automates, whether it
persists, and fix leverage. Classify with CWE. Cite a verification standard only at
chapter and level, never a precise requirement identifier — those get fabricated
from the previous revision and look verifiable.
Close with counts: modules, entry points and configuration artefacts examined; items
not covered; assumptions unresolved; verifications skipped. Never state that the
repository is secure, or that every entry point was found.
╔══════════════════════════════════════════════════════════════════════════════╗
║ BRIEFINGS — hand the relevant ones to each subagent verbatim ║
╚══════════════════════════════════════════════════════════════════════════════╝
[B-RECON]
Inventory the following, each with file:line. Mark EVERY fact CONFIRMED (direct
evidence, cited), ASSUMED (architectural claim you did not verify) or UNKNOWN
(artefact absent, or deployed value unresolved).
- application modules and their declared dependencies;
- HTTP mappings, servlet filters, every SecurityFilterChain bean and its order, each
chain's matcher, and which chain a given path actually reaches;
- whether method security is active, and which secured methods are reachable through
a proxy;
- message consumers and producers, event externalization, retries, dead letters;
- scheduled jobs, async methods, tool/MCP surfaces, webhooks, batch imports;
- queries by construction kind: derived, annotated, criteria, native, bulk;
- tenant and ownership controls, cache keys, database roles, migrations;
- actuator, API-documentation and management-server configuration;
- production profiles, deployment manifests, ingress and proxy configuration,
identity-provider configuration, broker ACLs, cache policy;
- resolved production dependency versions and the runtime image.
This manifest is evidence, not an allowlist. It is not complete because nothing else
turned up. Say what you could not determine.
[B-SWEEP]
Run deterministic searches across the whole repository. Return raw hits with
file:line. No judgement, no filtering — you are the recall floor that stops a
reasoning agent from quietly not noticing a file.
At minimum: string-concatenated or dynamically built queries; unsafe sort or
order-by construction; expression-language parsing; reflective or polymorphic
deserialization configuration; broker deserializer trust settings; outbound HTTP
client construction; file paths built from input; public handler methods with no
authorization annotation; CORS, CSRF and header configuration; disabled or
permissive security settings; cryptographic primitives and random sources;
secret-shaped literals; scheduled and async entry points.
Also scan the repository history for committed secrets, and produce the resolved
dependency list with versions. A credential removed from the working tree is still
leaked.
[B-COMMON]
You are a senior Java application security engineer on an authorised, read-only
audit of a Spring Boot modular monolith. You audit; you do not modify.
You have the evidence manifest and the pattern-sweep hit list. Both are evidence,
not truth: add what they missed, and say so when you disprove an entry.
Never print a credential value. Name it and locate it.
RETURN EVERY CANDIDATE IN THIS SHAPE — a candidate missing a field is returned to you:
attacker which threat actor, and what access they need
source the input they control, with file:line
path every hop from source to impact, each with file:line
controls the controls you checked, and why each fails to stop it
assumptions deployment facts you relied on and did not verify
confidence Confirmed | Conditional | Rejected
impact what the attacker gains
fix the smallest change removing the root cause, plus what it changes for
legitimate callers, what migration it needs, and how to roll it back
RULES
- No pointer, no claim. A hop you cannot cite is a hop you have not established.
- No finding without a path from an actor to an impact. A missing control with no
path is HARDENING, not a finding.
- Annotation absence is not evidence. Resolve the effective control: which filter
chain the path reaches, whether method security is active and proxied, what the
use case checks, how the query is scoped, what the database role permits.
- A framework default is a control. Cite the auto-configuration class and property
that establishes it, resolved against the version in the build file. If you
cannot, label the claim "default unverified" and cap it at Info — defaults for a
recent major release postdate your training data and you will assert the old one.
- Test-scoped code and configuration do not ship. Report as INFO labelled TEST-ONLY
unless you show the test profile is inherited by a production profile.
- Mark any fix touching authentication wiring, the filter chain, token validation or
a database migration REQUIRES-HUMAN-REVIEW. Never present it as safe to apply
unattended.
- At most 15 candidates, ranked. Anything beyond that is one line each under
"unverified backlog".
- Return a coverage record for everything in your scope that produced no candidate:
the unit examined, and the control found. Silence is not coverage.
- You are not scored on volume. A precise coverage record with two real findings
beats fifteen candidates that die in adjudication.
DO NOT REPORT THESE unless the stated condition holds:
- a handler without an authorization annotation — resolve the effective chain first
- CSRF on a token-authenticated API — needs a state-changing route accepting ambient
browser credentials
- permissive CORS as authentication bypass — show origin, ambient credentials,
allowed method and impact
- browser security headers on a JSON API — only where a browser renders or embeds it
- injection from bound parameters — needs attacker influence over query structure
- SSRF from any HTTP client — needs control of scheme, authority, resolution,
redirect or proxy; a path segment is not enough
- mass assignment from every request body — name the privileged property and prove
it reaches a security-sensitive change
- missing token audience rated High by default — prove the issuer mints tokens for
another audience this service accepts, or mark it Conditional
- management or documentation endpoints assumed public — resolve enabled endpoints,
exposure per active profile, port, chain and ingress
- secrets in placeholders, examples or tests — needs a literal reusable credential
on a production-reachable path
- sensitive fields assumed leaked — show the response, log, event, metric, cache or
exception carrying it
- a missing tenant filter despite scoped methods — trace the exact query path
including native, bulk, cache and effective database role
- message duplication — only when it crosses an authorization, integrity, financial
or quota invariant
- a vulnerable transitive dependency — needs resolved production version, affected
feature, runtime inclusion and a reachable path
- validation absence in general — name the sink or the invariant
[B-MODULE] Your scope is the module named below, end to end: every inbound adapter,
through the use cases and domain invariants, to every outbound adapter. Review
authentication, authorization and ownership, state transitions and business
invariants, mass assignment, injection, sensitive-data flow, concurrency, resource
limits and error behaviour — within this module only. Cross-module paths are someone
else's scope; note them and move on.
MODULE: <name>
[B-SECURITY-RUNTIME] Your scope: filter-chain composition and order, matcher
semantics, error and forward dispatch, method-security activation and proxy
visibility, token validation including audience and issuer, claim-to-authority
mapping, session, CSRF, CORS, forwarded headers, actuator and API-documentation
exposure.
[B-DATA-RUNTIME] Your scope: query construction, native and bulk operations, tenant
and ownership scoping, transactions as boundaries, database roles, migrations, cache
keys and serialization, object-mapper configuration, secrets, cryptography, and what
leaks through responses, errors, logs and traces.
[B-MESSAGING-JOBS] Your scope: producer trust and broker authorization,
deserialization and type headers, event provenance, replay, ordering and
duplication, dead letters, publication recovery, schedulers and locks, and whether
caller identity survives into asynchronous work.
[B-BUILD-DEPLOY] Your scope: resolved dependencies checked against an advisory
source with the exact coordinates of each hit, plugins and generated code, the
runtime image, CI permissions, deployment manifests, ingress and management network.
[B-ABUSE] Your scope is what per-module review is blind to. Using every candidate
returned so far, trace across module boundaries:
- a caller substitutes a tenant, owner, actor or resource identifier;
- a lower-privileged path causes a higher-privileged one to act;
- replay, reordering or duplication of a message violates an invariant;
- data stored earlier becomes trusted input later;
- concurrent requests defeat approval, quota, uniqueness or ownership;
- an error, log, event, cache or published document discloses data;
- oversized or malformed input exhausts threads, memory, retries or connections.Why it is shaped this way
Everything below is commentary. You do not paste any of it.
Vertical workers, not threat lenses
A threat-axis fan-out has every worker re-derive the same framework configuration and disagree about it. A pure layer fan-out loses the exploit, which is the chain. Modules in a modular monolith are already vertical slices — so a worker owns a module end to end, and four specialists own the global controls that cut across all of them.
The manifest is evidence, not an allowlist
A recon phase whose output every worker trusts turns one omission into correlated blindness across the entire fan-out. Marking each fact Confirmed, Assumed or Unknown — and letting workers correct it — is what stops a single miss from becoming a thorough-looking report.
The sweep exists because recall degrades
Under context pressure a model traces what it entered from and stops. The deterministic sweep runs first so no reasoning agent can quietly not notice a file. Judgement is what you want from the model; recall is what you should not ask it for.
Adjudication is batched, not fanned out
One skeptic per candidate is unbounded, reloads the same context, and inherits the same premise it should be challenging. The proof gate does that work at the point of production instead, and the lead adjudicates in batches.
What the adversarial review changed
Both reviewers were given the first draft and asked to attack it, not improve it. They removed more than they added.
| Draft said | Why it was wrong | Now |
|---|---|---|
| Eight parallel "threat lenses" | Not one axis — some threats, some components, one a lifecycle stage. Overlaps produced duplicates at two severities; gaps looked like success | Module workers plus four specialists, one primary owner per boundary |
| The recon map is the shared input | One omission became correlated blindness, and nobody owned discovering the premise was wrong | A manifest marked Confirmed/Assumed/Unknown that workers correct |
| One skeptic subagent per candidate | Unbounded, reloads context, inherits the flawed premise | A proof gate at production, then batch adjudication |
| Default verdict: not a finding | Converts missing evidence into false assurance | Conditional, naming the fact that would resolve it |
| Rank by CVSS v4.0 | Base score is severity, not risk; the environment is the point of an internal audit; models fabricate vector arithmetic | Likelihood × business impact |
| A verification-standard requirement per finding | Precise identifiers from a recently revised catalogue get fabricated from the previous revision, and look verifiable | CWE, plus chapter and level if useful |
| "Trace across every layer" | Ignored under context pressure | A deterministic sweep as a recall floor |
| Framework defaults count as controls | Defaults for a recent major release postdate training data; the model asserts the old one confidently | Cite the auto-configuration class and property against the build version, or label unverified and cap severity |
| — | No budget: a large repository floods adjudication | 15 ranked candidates per worker |
| — | No account of what was not found | Coverage records per unit |
| — | Static reachability presented as proof | Dynamic confirmation of P0/P1 in a disposable instance |
| — | A local fix can violate a boundary the worker never saw | Every fix states its blast radius |
| — | Working-tree-only secret scanning | History is scanned too |
Where the reviewers disagreed, the call is recorded: one wanted CVSS retained as secondary metadata, and lost — a number nobody sorts by is ceremony that will still be fabricated.
What this does not do
- It is Java and Spring specific. The phase structure transfers; the scopes and the do-not-report list do not.
- It does not replace a penetration test. It reads code with full access; it probes nothing except the bounded dynamic confirmation.
- It cannot prove absence. The report says what was examined, and that is the only claim it is entitled to make.