LLM app recon: mapping an AI feature
Brief
The first hour on an AI feature is for mapping what the model can reach, not for trying to make it misbehave. The risk sits in the tools, documents and accounts behind the chat box, and those can be listed through ordinary use, the documentation and the client's own configuration.
On this page
- Check first
- Where does the model sit in the application?
- Which tools and data can the feature reach?
- How do I learn the system prompt without attacking it?
- What rate limits and logging does the feature have?
- Which model and vendor are behind the feature?
- The defender sees
- Done when
- Not covered
- Tools
- Related research
- Reviewed
- Situation
- An application with an AI feature, such as a chat assistant, an agent that takes actions or a search over documents, an intercepting proxy, and test accounts in at least two roles. We test only the applications, tenants and accounts named in the scope letter, and we ask the client for the system prompt and tool definitions rather than trying to extract them.
- Goal
- A one page map of the AI feature: its surface, the tools and data it reaches, its limits, its logs and the model behind it.
- Outputs
- Representative examples on placeholder hosts, trimmed to the lines that matter. Not captured from a public fixture yet.
Check first
- Every request goes through the proxy from the first message, including streamed responses, so the site map shows the AI feature's own routes.
- The client's answer to three written questions: what the system prompt says, which tools the model can call, and which data sources it retrieves from. White box is faster and safer than guessing.
- The product's own help pages for the feature, its release notes and its privacy page, which usually name the model vendor as a subprocessor.
- Test accounts in at least two roles, and test documents we wrote, so anything the model retrieves is ours.
- The agreed request rate for the feature, which is often lower than for the rest of the app because each request costs the client money.
Steps
Where does the model sit in the application?
POST /api/assistant/messages HTTP/1.1
Host: app.example.com
Authorization: Bearer <token for tester-a, redacted>
Content-Type: application/json
Accept: text/event-stream
{"conversation_id":"conv_test_0192","message":"What were my last three invoices?"}
Expected output
HTTP/1.1 200 OK
Content-Type: text/event-stream
event: tool_call
data: {"name":"search_invoices","arguments":{"customer_id":"cus_test_a1","limit":3}} # <- the model calls a tool, so this is an agent, not a chat box
event: message_delta
data: {"text":"Your last three invoices were "}
event: message_done
data: {"conversation_id":"conv_test_0192","model":"vendor-model-2026-05"}
One ordinary question tells us the shape. A response with only text is a chat feature. A tool_call event, or a second request from the browser to another route while the answer streams, means the model takes actions. Citations or document names in the answer mean retrieval. Many features are all three, and each one is a different list of what to test.
Negative result. A plain text stream with no tool events and no citations, across questions that would need data to answer, suggests a chat feature with no reach. Confirm it with the client's tool list before writing it down, because some apps call tools on the server and never stream the events.
Which tools and data can the feature reach?
GET /api/assistant/capabilities HTTP/1.1
Host: app.example.com
Authorization: Bearer <token for tester-a, redacted>
Expected output
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"tools":["search_invoices","create_ticket","share_document","send_email"],"sources":["help_center","workspace_documents"]} # <- two tools that act outside the conversation
Many apps publish this list to drive their own interface; otherwise it is in the help pages or the client's answer. For each tool we write down what it does, whose permission it runs under, and whether the user confirms before it runs. A tool that sends email or shares a document under a service account that sees every tenant is the line the rest of the test hangs on. The MCP analysis is a reminder that the tool layer is ordinary software with its own flaws.
Negative result. An empty list, or a list with only read tools scoped to the user, narrows the test. Repeat it with the second role, because tool lists often differ by role.
Fix. Give each tool the calling user's own token instead of a service account, require a confirmation the user clicks for any tool with an effect outside the conversation, and keep the capability list as short as the feature needs.
How do I learn the system prompt without attacking it?
We ask for it. The client wrote it, and reading it is a code review, the approach set out in reading prompts like code. Where the client cannot share it, we infer its rules from ordinary use: what the feature declines, which topics it steers away from, what it says it can do when asked plainly. We record those as observations, not as the prompt. Extraction attempts belong to a later stage with their own agreement, never to recon.
Ordinary question: What can you help me with in this workspace?
Observed answer: Invoices, support tickets and shared documents. Declines billing changes.
Negative result. No prompt from the client and nothing inferable from use means the map marks the prompt as unknown. That is a gap to state, not a reason to start probing.
Fix. Treat the prompt as configuration under review: keep it in version control, and put nothing in it that is a secret or the only control on an action.
What rate limits and logging does the feature have?
for i in $(seq 1 25); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
-H "Authorization: Bearer $TOKEN_A" -H "Content-Type: application/json" \
-d '{"conversation_id":"conv_test_0193","message":"atarus-recon-0001"}' \
https://app.example.com/api/assistant/messages
done | sort | uniq -c
Expected output
20 200
5 429 # <- a limit that bit after 20 requests in this run; the window is unknown until timed
Twenty five short messages, inside the agreed rate, show whether a limit exists and roughly where it bites. Each streamed answer takes seconds, so note how long the run took before calling the limit per minute or per hour. Each one carries a marker string. We then ask the client to find atarus-recon-0001 in their logs: whether they can, how long it took and whether the tool calls were logged beside it goes on the map.
Negative result. Twenty five 200 responses means no limit at that rate. Report it as an observation with the cost per request if the client knows it; do not raise the rate to find the ceiling.
Fix. Limit requests per account and per tenant, cap spend per account, and log the caller, the conversation, each tool call with its arguments and the model version, so a request can be traced after the fact.
Which model and vendor are behind the feature?
curl -s https://app.example.com/ | grep -oE 'src="/[^"]+\.js"' | cut -d'"' -f2 \
| while read -r js; do curl -s "https://app.example.com$js"; done \
| grep -oE '"(model|provider)":"[^"]+"' | sort -u
Expected output
"model":"vendor-model-2026-05"
"provider":"hosted"
The model name often appears in the response stream, the bundle or a response header. Minified bundles often write keys without quotes, so widen the pattern if nothing matches. The vendor is usually on the privacy page's subprocessor list. Both go on the map because they decide where conversation data travels and which retention terms apply.
Negative result. No model string anywhere means the client's answer is the source. Ask for it in writing.
Fix. Keep the model and vendor on the privacy page's subprocessor list, and pin the model version so a silent change shows up in review.
The defender sees
Ordinary questions look like ordinary use, which is the point of recon. The burst of twenty five messages shows as rate limit responses for one test account, and the marker string is in every conversation log that captures input. The tool call made by the first question appears in whatever log the tool writes. If the client cannot find the marker, or can find the message but not the tool call it caused, that gap belongs in the report.
Done when
The map names every request the feature makes, every tool it can call with the permission each one runs under, the data sources it retrieves from, the rate limit, where conversations are logged and the model and vendor.
Not covered
Prompt injection, jailbreaks and other attacks on the inputs found, which have no page yet; the outputs shown are representative examples on placeholder hosts, not captured runs.