2026.08.21

Active Reconnaissance of AI Systems: Turning Architecture Guesses into Runtime Evidence

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.

Scope. This describes a method for authorised assessment. Everything shown here runs against a demonstration environment built for the walkthrough, with synthetic hostnames, tokens and documents. Nothing here is a result until the boundary and the impact have both been established.

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.

A note-taking application showing an active validation queue table. Four rows labelled AR-01 to AR-04 each pair a passive observation with a runtime hypothesis, a safest first test and a refutation condition. Below the table, a stop conditions list covers unexpected authentication behaviour, production data appearing, service degradation, contact from the defender, and side effects outside the test namespace.
Figure 1Passive observations converted into bounded tests before any active interaction. The refutation column is what keeps the phase honest.Controlled demonstration lab; synthetic environment.

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.

Terminal showing a curl request to the demonstration assistant API with a redacted bearer token, and the JSON response. The response contains a conversation identifier, a request identifier, a model field reading local-chat, a grounded flag set to true, one source document titled public-support-options.md with a score of 0.79 and chunk identifier doc-101 chunk 041, an empty tool calls array, token usage and a latency of 180 milliseconds.
Figure 2A clean baseline through the documented user path. Capture the whole exchange, not just the answer.Controlled demonstration lab; synthetic environment.

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.

Terminal showing a route matrix run against the demonstration host. The assistant endpoint returns 401 without authentication and 200 with a bearer token. A chat completions path returns 404. The health endpoint returns 200 on GET. A comment notes that 401 means the route exists behind authentication while 404 may mean absent, hidden or rewritten upstream.
Figure 3Four routes, each justified by passive evidence. A 401 and a 404 are not opposites, and neither is a conclusion.Controlled demonstration lab; synthetic environment.

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.

Terminal showing two model identification probes. Asking the assistant what model it is returns a message saying it has no source for that in the available documentation, while the response metadata reports the model as local-chat. A grep of the deployment environment file shows a model API base pointing at a self-hosted inference endpoint. A comment notes three signals, one conflict, and no conclusion yet.
Figure 4Model identity assembled from independent signals. Where they disagree, the disagreement is the thing worth recording.Controlled demonstration lab; synthetic environment.

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:

  1. Self-reported identity and provider
  2. API and tokenizer metadata exposed by the application
  3. Claimed knowledge boundary, checked against dated facts
  4. Context retention under controlled marker tests
  5. Stable response-format or capability characteristics
  6. 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.

Terminal showing four retrieval probes. A general knowledge question returns no sources and is not grounded. A refund policy question returns two ranked sources, the seeded canary doc-114 at 0.86 and the public support document doc-101 at 0.79, and is grounded. A request for another tenant's onboarding document returns no sources. An absent topic returns no sources. A final probe repeats the other-tenant question using tenant B's own token and does retrieve the onboarding runbook canary at a score of 0.78, confirming the earlier empty result came from the tenant filter rather than a failed match.
Figure 5Paired controls make the retrieval transition visible. The last probe is the positive control that turns an empty result into evidence.Controlled demonstration lab; synthetic environment.

Four query classes, and only one of them retrieves anything:

Query classExpected sourceSource returnedGroundedBoundary result
General knowledgeNoneNoneNoFallback confirmed, no retrieval
Authorised policy canaryTest policy documentdoc-114 at 0.86, doc-101 at 0.79YesRetrieval path confirmed
Other-tenant canaryNoneNoneNoIsolation held on this request
Absent topicNoneNoneNoNegative 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:

  1. The schema exists
  2. The tool is offered to the model at runtime
  3. The orchestrator issues the call
  4. The external system records the side effect
Terminal showing an agent tool test in three steps. The assistant returns a create_ticket tool call with status awaiting human approval. A ticket creation request returns a ticket LAB-4100 in the lab-canary queue created by the service identity, carrying a correlation identifier matching the earlier request. A PATCH request to update the same ticket returns HTTP 403 with a message stating the service identity lacks the ticket update permission.
Figure 6The pending tool call confirms that the agent can request the action. The direct API checks separately validate the service identity's create-only permissions; they do not prove end-to-end orchestrator execution.Controlled demonstration lab; synthetic environment.

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.

A log search interface filtered on a request identifier and the tickets path, showing nine events. Six assistant requests return 200 with durations around 180 milliseconds, a health check returns 200, a ticket creation returns 201 and a ticket update returns 403. The row matching the correlated request identifier is highlighted. A footer notes that prompt text is not present in these events and only metadata was recorded.
Figure 7The same activity from the defender's side. What was recorded matters as much as what the application returned.Controlled demonstration lab; synthetic environment.

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.

IDHypothesisOutcomeEvidence
AR-01The route invokes an AI-backed serviceConfirmedAuthenticated POST returns model, grounding and usage metadata
AR-02The published /v1/chat/completions route is liveRefuted on tested pathThe route returned 404; no compatible response was observed there
AR-03Some questions trigger a RAG pipelineConfirmedPaired controls plus a tenant-B positive control
AR-04The agent can request a write actionRequest confirmed; execution unverifiedPending 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

  1. Confirm the rules of engagement, the identities and the stop conditions.
  2. Convert passive observations into explicit tests with refutation conditions.
  3. Record a clean baseline through the normal user path, more than once.
  4. Validate routes you have provenance for before expanding discovery.
  5. Triangulate model signals instead of trusting the self-report.
  6. Use canary documents and paired controls to map retrieval.
  7. Confirm tools through traces and reversible side effects.
  8. Review telemetry from the defender's side.
  9. Reproduce material failures and run the negative control.
  10. 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.