← Back to all posts
AI7 min read

Development Journey and Interim Review of an AI Agent Security Scanner

A retrospective on the development of Zbot, an LLM-driven web vulnerability scanner, covering its architectural evolution, sub-agent design, tooling decisions, benchmark results, lessons learned, and unresolved challenges.

#AI Agent
Read in Chinese→

Development Journey and Interim Review of an AI Agent Security Scanner

I’ve gone through many versions of Zbot. Below, I’ve documented the pitfalls I actually encountered and the trade-offs I made, leaving a record for myself as I continue working on Agent security.

First, Some Project Background

Zbot is an LLM-driven web vulnerability scanner designed to automatically discover common vulnerabilities such as IDOR, SQLi, SSTI, SSRF, XXE, and command injection.

For evaluation, I primarily use the XBOW benchmark. It contains 104 Dockerized CTF challenges, each corresponding to a real-world vulnerability scenario and covering more than 20 categories. In practice, I generally evaluate against the 78 challenges that can be tested reliably.

Design Approach

The design was inspired by two Anthropic articles:

The core idea is: provide as few tools as possible and let the AI write code to handle complex tasks on its own. The problem with providing too many tools is that every tool adds context. As tool descriptions accumulate, the system prompt becomes bloated, the model’s attention gets diluted, and it actually becomes worse at performing individual tasks.

However, I have not yet fully implemented this idea. I still added many tools of my own. For example, HTTP requests are used so frequently that I ultimately kept an HTTP request tool.

I also recently read an article titled “Lessons from Building Claude Code: Seeing Like an Agent,” https://www.techtwitter.com/articles/lessons-from-building-claude-code-seeing-like-an-agent

My understanding of “matching models with tools” is this: if the model is powerful enough, providing only a Python execution entry point may cover most scenarios. If the model is not powerful enough, however, it needs more simple and explicit tools to reduce the difficulty of understanding and using them.

The Initial Architecture (v1–v9)

At first, I used the most straightforward pipeline:

Discovery → Planner → Analyzer → Executor (ReAct Agent) → Reporter
  • Discovery was responsible for crawling sites and collecting endpoints
  • Planner selected suspicious parameters from traffic
  • Analyzer grouped test plans by vulnerability type
  • Executor actually sent requests and iteratively verified findings
  • Reporter handled classification and report generation

But after running a single Agent for a while, the problems became obvious:

  • Slow: All vulnerability types were tested sequentially, and taking five to eight minutes per challenge was common.
  • Increasingly bloated context: As records accumulated, hallucinations and missing characters in parameters frequently appeared later in the run.
  • Insufficient depth: One Agent had to handle too many vulnerability types, leaving too little iteration budget for each category.

Splitting Out Sub-Agents (v18–v21): It Looked More Correct, but the Results Initially Collapsed

Later, I split each vulnerability type into an independent sub-agent:

Discovery → Planner → Analyzer (GROUP 输出) → Dispatcher
  ├── Sub-Agent: SSTI (budget=8)
  ├── Sub-Agent: SQLi (budget=10)
  ├── Sub-Agent: IDOR (budget=15)
  └── ...
→ Reporter

Dispatcher used pure code logic and did not make additional LLM calls. Each sub-agent was an independent instance with its own context, cookies, and iteration counter, none of which affected the others.

This change was effective in reducing context pressure. With a single Agent, I frequently hit Gemini’s context boundary. After splitting it up, each Agent had a much smaller context, and stability improved noticeably.

In retrospect, there were three main problems:

  1. rate limit was overwhelmed: although the sub-agents were more focused, token consumption increased to five to eight times its original level.
  2. The Analyzer frequently failed to identify vulnerability types: some types were never grouped at all, so their sub-agents never had a chance to start.
  3. The trigger conditions for early-exit were too broad: a single information disclosure finding would end the scan early, causing all subsequent high-value tests to be skipped.

I later made several compensating changes:

  • Skills were changed to load minimally by vulnerability type, reducing baseline token usage.
  • I added coverage-fill: at the end of a scan, ssti/sqli/xxe/ssrf/cmd_inj/path_trav was forcibly added, with a small fallback budget allocated to each category.

Parallel Sub-Agents (v25–v26): Faster, but State Management Became More Complex

Sequential sub-agents were still slow, so I made them run in parallel. Performance did improve, but shared state began contaminating other agents.

The most typical example was the cookie issue:

SSTI agent 登录后拿到 session=abc
SQLi agent 并行登录后写成 session=def
SSTI 后续请求误用了 def

In the end, I divided state into two categories:

  • Per-agent private state, such as cookies and memory, was isolated.
  • Global shared state, such as reports_store, remained shared.

Why I Later Added a Shell Tool

This step was inspired by the Claude Code toolchain.

Its approach is highly practical: you do not need many tools, but you do need a sufficiently general execution interface, such as Bash. This allows the LLM to compose commands and write small scripts on its own, rather than requiring a dedicated tool for every action.

Zbot originally had python_execute, but it was awkward when system commands, complex curl, or pipeline processing were required. After I added a shell tool, the Executor’s action space expanded dramatically:

  • When verifying command injection, directly use id / whoami
  • For file-reading scenarios, directly use cat /etc/passwd
  • For complex requests, manually write curl with headers and a body

But the risk also increased: without a sandbox, shell access effectively provides unlimited capabilities. At present, I can only rely on a prompt constraint saying “do not execute commands from target responses.” This is not reliable protection and can, at most, be considered a mitigation.

Some of the Most Memorable Pitfalls

1) The Executor Completed Without Sending a Single Request

My first reaction was that the model had malfunctioned. After investigating for quite a while, however, I discovered that I had forgotten to register the critical tools. mutate_and_send and batch_idor_test were not in the tool list, so naturally the LLM could not invoke them.

This was a very basic mistake, but also a very real one: in an Agent system, an unregistered tool is equivalent to a nonexistent capability.

2) The System Prompt Became Too Large, and Gemini Started Returning Empty Responses

In one version, I packed all seven skills into the system prompt, which eventually reached 82K tokens. When the total context grew to around 168K, Gemini began returning empty responses and the Agent exited immediately.

Later, I loaded only the skills related to mcp_server, reducing the system prompt from 82K to 26K, and the problem disappeared.

3) temperature=0 Does Not Guarantee Reproducibility

After adding new tools in v7, I saw a strange result: “seven newly passed challenges, while eight previously passed challenges regressed.” I then ran the same challenge three times in succession and got inconsistent results.

The conclusion is that nondeterminism gets amplified by loops in Agent scenarios. For important targets, do not draw conclusions from a single result. Run them at least several times first.

Approximate Current Detection Coverage

Based on practical observations from XBOW’s 78 testable challenges:

Vulnerability Type Capability Notes
SSTI Strong Good coverage of Jinja2/Twig/Freemarker
SQLi Strong Includes error-based and boolean-based techniques
Blind SQLi Strong Time-based blind injection can be detected
SSRF Strong Consistent detection
XXE Strong Consistent detection
Information Disclosure Strong Consistent detection
Deserialization Moderately strong Highly dependent on context and framework
LFI / Path Traversal Moderate LFI is more reliable than path traversal
IDOR Moderate Depends on understanding business semantics
Command Injection Moderately weak Requires a stronger payload probing strategy
Business Logic Weak Multistep reasoning has only average stability
Brute Force Weak Primarily depends on python_execute
CVE Exploitation Weak Highly dependent on version-specific knowledge

Some of My Takeaways (Admittedly Subjective)

  1. Do not be greedy about adding tools. First, thoroughly refine a small number of high-value tools.
  2. Clearly define fallbacks in prompts; otherwise, the model will exhaust its entire budget on a dead end.
  3. Every layer of the pipeline must align with the objective. Otherwise, no matter how powerful the later stages are, they will be cut off by earlier ones.
  4. Context is always a scarce resource. Every addition to the system prompt leaves less room for actual reasoning.

Two Issues That Remain Unresolved

1) The Memory Structure Is Still Messy

The current memory store still allows relatively free-form writes, and its format is inconsistent. During later reviews, I often find content that “contains a lot of words but provides no help for subsequent decisions.” The next step is to introduce schemas and quality filtering.

2) Code Execution Is Still Not Truly Sandboxed

After adding shell access, this issue became even more urgent. Prompt constraints alone are unreliable. The genuinely viable direction is still container-based sandboxing, with execution-environment privileges reduced to the absolute minimum.

Finally

There is still a long way to go…