The passive phase ends with a map I do not believe yet. Every box on it came from a manifest, a config file or a job advert, which means every box is a guess with a source attached.
Active reconnaissance is where the guesses get tested. That sounds routine until the target is an AI application. A conventional API returns the same answer to the same request. An AI system varies its wording, retrieves different context, calls a tool, carries conversation state, and refuses things for reasons that have nothing to do with access control. One strange response proves close to nothing.
The job is not to make the model say something surprising. It is to work out which components handled the request, which boundary the request touched, and what evidence would survive someone else's review.
Convert the register into a queue before you send anything
The passive register lists what I saw and what I think it means. The validation queue adds the part that makes it testable: what result would prove me wrong.
That last column does most of the work. Without a refutation condition you will find confirmation everywhere, because AI systems are generous with output that can be read as agreement.
The queue also stops scope drift, which is the characteristic failure of this phase. If AR-03 is about retrieval and the response comes back looking odd in some other way, I write the new lead into the register and finish the test I started. Following the interesting thing is how a bounded assessment turns into an afternoon of prompt games with no record of what was tried.
Stop conditions get written before the first request, not after something goes wrong. Authentication behaving strangely, production data surfacing in a response, latency climbing, the defender making contact, or any side effect landing outside the test namespace. Any one of those ends the probe. Active work is visible and sometimes stateful, and both of those can bite you.
Baseline first, and repeat it
Before any discovery probe, I run the ordinary user path and keep the whole thing: request, response, identity, timestamps, and what the logs recorded.
One request answers a surprising number of questions. Which fields are mandatory. Whether the service issues a conversation identifier. Whether the response carries model, latency, token or retrieval metadata. Whether citations appear for an ordinary query. In the lab they do, and the response names the document and its retrieval score, which is already worth noting: the application discloses its sources and its scoring to any authenticated caller.
Then I run it again, several times, because the point of a baseline is knowing what normal variation looks like. Wording moves around. The route, the status code, the metadata fields and the presence or absence of side effects should not.
Skip this and every later result is ambiguous. A refusal might be policy enforcement, a broken client, a rate limit or a backend that fell over thirty seconds ago. You will not be able to tell, and neither will the person reading your report.
Validate the service, not the port
AI features live inside ordinary web applications. A port scan tells you there is HTTP on 443. It tells you nothing about whether there is hosted inference, local inference, retrieval or tools behind it.
The commands in the figures below assume a small lab-env.sh holding the base URL, the headers and an ask helper, so that each probe is one readable line rather than four.
Client-side assets usually give the first route. A config object like this one establishes what the browser was built to call:
window.APP_CONFIG = {
apiBase: "/api/v2",
assistantEndpoint: "/api/v2/assistant",
aiEnabled: true
};
What it does not establish is whether that endpoint is authenticated, or whether the feature is switched on server side. Those are separate questions with separate tests.
Four requests against ai-lab.example.test, each with provenance. I can say why I sent every one of them, which matters when the client asks.
The same route returns 401 unauthenticated and 200 with a bearer token, which settles AR-01: something is listening and it wants credentials. /health came from deployment documentation and returns 200, so it is unauthenticated. I include it only because health endpoints were named in scope; if they are not in yours, leave it out, because an unauthenticated endpoint is exactly the kind of thing that looks like an unapproved probe in someone's logs.
Read the codes carefully. A 401 usually means the route exists behind an authentication check, and that is useful. A 404 is weaker than people assume: absent, deliberately hidden, rewritten by a gateway, or covered by a uniform-response policy all produce the same code. I record the observation and resist turning one status code into a conclusion.
Note what I did not do. No wordlist, no fuzzing, no enumeration of routes nobody mentioned. Broad discovery against an AI endpoint generates cost as well as noise, since every request that reaches the model is billable somewhere.
Model fingerprinting is triangulation, not a question
Asking an assistant what model it is makes a reasonable first probe and terrible evidence. The answer can come from the model, a system prompt, an application template or a gateway, and it can simply be wrong.
In the lab the self-report gives nothing at all, because the application only answers from retrieved context. That is itself a signal about how the system is wired. The response metadata names local-chat. The deployment config collected during the passive phase points at a self-hosted inference endpoint.
Six signal types, none of them conclusive alone:
- Self-reported identity and provider
- API and tokenizer metadata exposed by the application
- Claimed knowledge boundary, checked against dated facts
- Context retention under controlled marker tests
- Stable response-format or capability characteristics
- Deployment evidence from repositories and configuration
When they disagree, record the conflict rather than picking the answer you like. If the API says one thing, the assistant claims another and the deployment files suggest a third, the disagreement itself is what goes in the register.
Two traps here. A correct answer about a recent event does not prove the information is in the weights, because the application may have retrieval or web access, so establish whether those are active before you test knowledge boundaries. And a context marker that disappears tells you the effective retention of the deployed application, not the model's published context window. The orchestrator may be trimming history, summarising earlier turns or capping tokens well below what the inference server allows. Runtime behaviour is what the assessment cares about, but do not label it as a property of the base model.
Find where retrieval switches on
A RAG application blends model output with text pulled from an index, and the transition is rarely announced. Paired controls make it visible.
The strongest version uses seeded canary documents, where I know the names, the access labels and the exact harmless phrases before I start. Production content cannot give you that ground truth.
Four query classes, and only one of them retrieves anything:
| Query class | Expected source | Source returned | Grounded | Boundary result |
|---|---|---|---|---|
| General knowledge | None | None | No | Fallback confirmed, no retrieval |
| Authorised policy canary | Test policy document | doc-114 at 0.86, doc-101 at 0.79 | Yes | Retrieval path confirmed |
| Other-tenant canary | None | None | No | Isolation held on this request |
| Absent topic | None | None | No | Negative control passed |
The third row is the one people misread, and on its own it proves nothing. Tenant A asked for a tenant B document and got nothing back, but an empty result has two explanations: the filter blocked it, or the query simply did not match anything. Those are very different security properties.
So the run ends with a positive control. Tenant B asks for the same document and retrieves it at 0.78. Now the null result means something, because the only variable that changed was the identity. Even then it is one observation on one path at one moment, and the report should say so in those words.
The canary query returns two documents, not one, ranked 0.86 and 0.79. That is ordinary top-k behaviour and it matters for later work: whatever reaches the model is the whole retrieved set, not just the best match.
The source metadata is worth reading closely. Titles expose naming conventions, chunk identifiers expose structure, scores expose the retrieval configuration. Seeing chunk_041 tells you the document is split and indexed. It does not tell you that chunk_042 is reachable, and a score of 0.86 has no universal meaning across retrieval systems.
If an other-tenant canary ever does come back, slow down rather than speeding up. Reproduce it, confirm the identity and the collection state, rule out test-data contamination, preserve the source evidence, and stop before you collect content you do not need. The finding is the boundary failure, not the volume of data you can pull through it.
Agent capabilities need side-effect evidence
An assistant that talks about a tool may not be able to use it. A schema naming create_ticket proves a capability was described somewhere. A response saying "I have created the ticket" proves that text was generated. Neither proves anything happened.
There are four stages, and they fail independently:
- The schema exists
- The tool is offered to the model at runtime
- The orchestrator issues the call
- The external system records the side effect
The pending tool call establishes that the schema was offered and the orchestrator issued a request for the tool. It does not establish that approval occurred or that the orchestrator executed the side effect. The later direct API request tests the service identity separately, so the evidence for stage four remains incomplete.
Permission labels deserve the same scepticism as everything else. Words like read-only, create-only and approval-required might be prompt text, schema description or genuine server-side enforcement, and those are three different properties. Here a direct canary create using the service identity succeeded and an update returned 403, validating create-only permissions for that identity. It does not show that the agent completed the pending action.
That restraint is deliberate. You can usually establish that a constraint is enforced without trying the operation it forbids, either by checking the enforcement point directly or by reviewing the identity's permissions with the client. Reach for a destructive test only when nothing else will settle it, and only with that specific action authorised in writing.
Human approval needs the same treatment. If the interface claims a person must approve a tool call, capture the pending state, the approval event and the final side effect as three separate things. If the action lands before the approval, the finding is about workflow enforcement, and it has nothing to do with how the model worded its reply.
Treat telemetry as part of the target
Everything above generated evidence for the defenders too. I want to know what they got.
After a small batch of probes I go through the authorised logs, ideally sitting with the blue team, asking a short list of questions. Was the full prompt logged, or only metadata? Were retrieved chunks and source identifiers captured? Do the logs preserve user, session and request correlation? Were tool arguments and outcomes recorded? Did any rate limit or detection rule fire? Could someone reconstruct the sequence without reading model prose by hand?
In the lab the answer to the first one is metadata only. The prompt text never reaches the log. That is a reasonable privacy posture and a genuine investigation gap at the same time, and saying both is more useful than picking a side.
This is not about evading detection. If a probe trips an alert, that is good control evidence. What the assessment measures is whether the alert is meaningful, correctly attributed, and carries enough context for someone to investigate it at two in the morning.
Detection testing needs controls just like model testing. An ordinary user query should stay quiet. An agreed reconnaissance pattern should fire. A semantically similar variation shows whether the rule catches intent or just a literal string. Tell the blue team this validation is part of the exercise, because burning their afternoon on a real-looking incident you caused is a poor use of the engagement.
Close the queue before you write anything up
Four tests went in. Here is what came out, and one of them died.
| ID | Hypothesis | Outcome | Evidence |
|---|---|---|---|
| AR-01 | The route invokes an AI-backed service | Confirmed | Authenticated POST returns model, grounding and usage metadata |
| AR-02 | The published /v1/chat/completions route is live | Refuted on tested path | The route returned 404; no compatible response was observed there |
| AR-03 | Some questions trigger a RAG pipeline | Confirmed | Paired controls plus a tenant-B positive control |
| AR-04 | The agent can request a write action | Request confirmed; execution unverified | Pending tool call; separate direct API checks establish create-only identity permissions |
AR-02 is useful because it refutes a narrower claim: the published /v1/chat/completions route was not live on the tested path at that moment. The 404 does not prove that no compatible interface exists elsewhere. It proves only that this documented candidate route did not provide one during the test.
Write refutations down with the same care as confirmations. They are what stops the next tester repeating the work, and they are the difference between a system model and a wish list.
What counts as a finding
Active reconnaissance produces a lot of leads, and most of them are not vulnerabilities. These are all interesting and none of them are findings on their own:
- the assistant names a model;
- a protected endpoint returns 401;
- a response includes a document title;
- the model describes a tool it has;
- a prompt produces unusual wording;
- a detector fires on a keyword.
A finding needs a security consequence and a boundary that was crossed. Unauthorised document retrieval. A tool action beyond the caller's permission. Data from another session. Approval that was not enforced. Sensitive metadata reaching an identity that should not have received it.
By that standard this run produced one reportable control gap, with impact still requiring separate validation. It came from putting the two phases side by side.
The configuration collected passively sets expose_retrieval_scores: false. The runtime response returns "score": 0.79 alongside the document title and a chunk identifier, to any authenticated caller. Declared intent and observed behaviour disagree, and the observed behaviour is the weaker one.
The response exposes document names, chunk structure and relative retrieval behaviour despite configuration that says scores should be hidden. That is a configuration and disclosure-control failure. The evidence shown here does not establish that inaccessible documents can be inferred or retrieved, so that stronger impact must not be claimed without a separate authorised test. Depending on the threat model and document sensitivity, this may be a vulnerability or a hardening observation. Everything else in this article remains a lead.
For anything material I preserve the clean baseline, the minimal input that triggered it, the exact identity and configuration, the application output, the external side effect or protected data, the relevant logs, the negative control, and the cleanup result. Eight items, and the negative control is the one people skip and later wish they had.
Reproduce in a fresh session where you can. Non-determinism is not something to hide from the client. If a boundary fails once in ten attempts, that may still be serious, but the report has to say once in ten.
The working sequence
- Confirm the rules of engagement, the identities and the stop conditions.
- Convert passive observations into explicit tests with refutation conditions.
- Record a clean baseline through the normal user path, more than once.
- Validate routes you have provenance for before expanding discovery.
- Triangulate model signals instead of trusting the self-report.
- Use canary documents and paired controls to map retrieval.
- Confirm tools through traces and reversible side effects.
- Review telemetry from the defender's side.
- Reproduce material failures and run the negative control.
- Update the map and the register after every result.
The output is not a catalogue of clever prompts. It is a corrected system model with runtime evidence behind each box.
Sometimes this phase kills the most interesting theory from the passive work. A protected route turns out to be a dead compatibility shim. A tool exists in source but is never offered to the deployed agent. Retrieval reaches a synthetic public collection rather than the sensitive namespace an old config file implied.
That is a good outcome. Establishing that a suspected path is not real saves time later and keeps the rest of the assessment honest.