August 23, 2026
AgentGG Found 100+ Zero-Days in Open Source Software
Introduction

By Philip Garabandic
13 min read
Introduction
AgentGG is an open source, agentic SAST scanner. Each agent is a readable markdown file that tells an AI investigator what kind of bug to look for, where to look, and what counts as proof. When a scan runs, the agent reads the code, follows imports, traces how functions call each other, and confirms a bug is real before it reports it. You run it on your own machine, with your own model keys, against any codebase you want.
One in three of the projects we scanned had at least one valid high or critical vulnerability. We scanned projects like OpenClaw, Directus, and NocoDB, plus many others, and a lot of geospatial projects we will go through in this article. In total we have found more than 100 zero-days. So far we have disclosed nineteen of them, eleven with CVEs assigned. The rest are still private until a fix is shipped.
AI has made it easier than ever to find exploitable bugs in a codebase, and anyone can do it now, including attackers. The problem is that AI security tools have not kept up with AI coding tools. Code is being written and released far faster than anyone is checking it for security holes. Most open source projects run important infrastructure with no security team and no money to hire one, so the gap between how fast bugs get introduced and how fast they get caught keeps growing.
In this article we will cover what the CLI is, how a scan works, what it costs to run, and we will walk through some of the real bugs we found.
Why these projects matter
For the findings we discuss in this article, we will focus on the geospatial projects we scanned. These are some of the most widely used mapping tools in the world, and they run in production at governments, scientific agencies, and large institutions, handling data that millions of people rely on every day.
GeoServer, built on the GeoTools library, is one of the most widely deployed map servers in the world. It runs at the FAO, the World Bank, and Ordnance Survey, the UK's national mapping agency. MapServer has been used by NASA. The USGS publishes the National Hydrography Dataset, the map of every river, lake, and stream in the United States, through these same map services, and one of its GeoServer deployments alone handles over five million requests a month. In France, GeoServer runs under NexSIS, the national dispatch system for firefighters and civil security.
Most of these are public systems. They expose data over standard protocols, often without any login, because open access is the entire point. A SQL injection or an authentication bypass is a direct path into infrastructure that real institutions depend on.
Almost all of these projects are maintained by a handful of volunteers and a few funded developers. The software is critical, but there is rarely anyone whose job is security.
How a scan works
A scan runs as a sequence of phases. Each one finishes before the next begins, so the output of every stage feeds cleanly into the next.
preamble git setup, agentic auto-exclude
recon one LLM survey of the project
preconditions regex or semgrep census, then LLM gate
agents batched agent pool, the core scan
validate re-check each finding, is it real
score CVSS severity per finding
dedup collapse same-root-cause findings
report write markdown, no LLMpreamble git setup, agentic auto-exclude
recon one LLM survey of the project
preconditions regex or semgrep census, then LLM gate
agents batched agent pool, the core scan
validate re-check each finding, is it real
score CVSS severity per finding
dedup collapse same-root-cause findings
report write markdown, no LLMPreamble sets up the run. It works out which files to scan, and under a diff scan it asks git which files changed. Recon is a quick survey of the project that writes a short brief, the languages, frameworks, and how the app handles things like authentication, so the agents start with the right context.
Preconditions decide which agents are worth running. The catalog ships with over 100 agents, and there is no point running a PHP agent on a Go project. A regex and Semgrep census filters the catalog down to the agents that fit this codebase before any of them run.
Agents is the core phase. Each selected agent reviews its files in small batches, reads the code, follows imports, and chases a suspect call into other files to confirm it. Batches from different agents run in parallel.
Then three passes clean up the raw findings. Validate re-reads the source for each finding, decides whether the bug is real, and checks it against your scope or security policy for what counts as a valid finding. Score attaches a CVSS severity. Dedup collapses findings that share the same root cause so you do not see the same bug five times. Finally report writes everything out as markdown, one file per finding. When it is done, read the markdown directly or open the local web UI.
AgentGG ships with over 100 agents covering common vulnerability classes like SQL injection, XSS, authentication and authorization flaws, secret exposure, SSRF, and path traversal. These are the ones we used for our scanning but you can also write your own to catch a bug class specific to your code. Preconditions, file scoping with regex and Semgrep prefilters, and custom prompts are all part of the format, and we will cover building agents in a follow-up article. For now, you can browse every existing agent in the agent library, and the CLI itself here.
Running a scan
Install it from npm and run the setup wizard once.
npm install -g agentgg
agentgg initnpm install -g agentgg
agentgg initinit is where you choose your AI provider and paste a key. AgentGG runs on Anthropic, OpenAI, OpenRouter, Bedrock, Vertex AI, or a local model through Ollama. More providers are being added regularly if you are missing one you can open a ticket on GitHub.
I know a lot of people will run this on Anthropic. You can set it up with an API key, or with your existing Claude subscription through OAuth so scans draw from your plan instead of a metered key.
agentgg init --provider anthropic # paste an sk-ant-api... key, or a Claude Pro/Max OAuth tokenagentgg init --provider anthropic # paste an sk-ant-api... key, or a Claude Pro/Max OAuth tokenEvery scan in this article ran on GLM 5.2 through OpenRouter. We felt this needed to stay open source. You can set that up with the command below.
agentgg init --provider openrouter # then paste your key; GLM 5.2 is the default modelagentgg init --provider openrouter # then paste your key; GLM 5.2 is the default modelWhichever provider you pick, init saves it and you will not need to pass it again. You can also override it per scan with --provider, --api-key, or --oauth-token without saving anything, which is how you would wire it into CI from a secret.
The simplest scan points at a folder and writes results to an output directory. With no -t flag it runs the full agent library.
agentgg scan ./src -o ./outagentgg scan ./src -o ./outEvery finding is saved as its own markdown file under ./out/findings/, shaped like a GHSA advisory, with a summary, the vulnerable code, a proof of concept, the impact, and the validation verdict. A summary.md collects them with counts per agent and per severity. When it is done, read the markdown directly or open the local web UI.
agentgg view ./out
# or scan and boot the UI in one step:
agentgg scan ./src -o ./out --serveagentgg view ./out
# or scan and boot the UI in one step:
agentgg scan ./src -o ./out --serveChoosing what to scan
Everything above is the default. In practice you scope the scan. Point -t at a single agent, a whole category, or your own file.
agentgg scan ./src -t sql-injection -o ./out # one agent by slug
agentgg scan ./src -t base/injection/ -o ./out # a whole category
agentgg scan ./src -t ./my-agents/ -o ./out # your own agentsagentgg scan ./src -t sql-injection -o ./out # one agent by slug
agentgg scan ./src -t base/injection/ -o ./out # a whole category
agentgg scan ./src -t ./my-agents/ -o ./out # your own agentsFor a pull request, scan only what changed. Each agent's file list is intersected with the touched files, and the diff is handed to the model as a focus hint. This is the mode you run in CI.
agentgg scan ./src --diff origin/main...HEAD -o ./outagentgg scan ./src --diff origin/main...HEAD -o ./outNarrow the file set further with --only and --exclude, both repeatable.
agentgg scan ./src --only "src/api/**" --exclude "**/migrations/**" -o ./outagentgg scan ./src --only "src/api/**" --exclude "**/migrations/**" -o ./outIf your agents use Semgrep prefilters, point --semgrep-rules at your local rule directory and any agent can name those rules from its where block.
agentgg scan ./src --semgrep-rules ./my-semgrep-rules -o ./outagentgg scan ./src --semgrep-rules ./my-semgrep-rules -o ./outTurning phases on and off
Validation, scoring, and dedup are all on by default. You can turn any of them off, swap in your own scope policy, or re-run just one phase later.
agentgg scan ./src --no-validate -o ./out # detection only, no second pass
agentgg scan ./src --scope ./security-policy.md -o ./out # your trust-boundary rules
agentgg scan ./src --no-score --no-dedup -o ./out # skip scoring and dedupagentgg scan ./src --no-validate -o ./out # detection only, no second pass
agentgg scan ./src --scope ./security-policy.md -o ./out # your trust-boundary rules
agentgg scan ./src --no-score --no-dedup -o ./out # skip scoring and dedupControlling cost and scale
A few flags bound how much work a scan does, which matters on a large repo. --concurrency sets how many model sessions run at once. The --max-* flags cap files and batches so one over-broad agent cannot blow up a run.
agentgg scan ./src --concurrency 10 -o ./out # more sessions in parallel
agentgg scan ./src --max-files-per-agent 50 -o ./out # cap files each agent reviews (default 300)
agentgg scan ./src --max-batches 100 -o ./out # cap total batches this run (default 250)agentgg scan ./src --concurrency 10 -o ./out # more sessions in parallel
agentgg scan ./src --max-files-per-agent 50 -o ./out # cap files each agent reviews (default 300)
agentgg scan ./src --max-batches 100 -o ./out # cap total batches this run (default 250)The full flag reference
That is the useful subset. As of writing, this is everything agentgg scan can be configured with.
-t, --template <value> slug, .md path, directory, or .txt list file; repeatable
--semgrep-rules <dir> directory of local semgrep rules an agent's preFilter can name; repeatable
-o, --output <path> output directory (default ./scan-results/)
--source-id <id> stable identity for the scanned source; resume state is reused only when it matches
--validate / --no-validate second-pass validation per finding (on by default)
--score / --no-score CVSS 3.1 scoring phase (on by default)
--scope <path> scope doc the validator consults for trust-boundary rules
--no-scope disable the built-in default scope
--rescan re-analyze files even if a prior run covered them
--revalidate-all re-validate findings that already have a verdict
--diff <commit> scope to a commit or range (accepts <ref>, a..b, or a...b)
--re-recon re-run recon + precondition plan instead of reusing the cached one
--no-recon skip recon AND precondition gating; run every -t agent
--no-summary skip writing the markdown report; state still persists
--max-files-per-batch <n> candidate files per agent batch
--max-anchors-per-batch <n> cap prefilter anchors per batch; a file with more is split (default 150)
--no-max-anchors-per-batch disable the per-batch anchor cap
--max-files-per-agent <n> cap files each agent reviews (default 300)
--no-max-files-per-agent disable the per-agent file cap
--max-batches <n> cap total agent batches this scan (default 250)
--no-max-batches disable the whole-scan batch cap
--concurrency <n> max model sessions in flight across the scan (default 5)
--dedup / --no-dedup final de-duplication pass (on by default)
--delete-duplicates physically remove duplicates instead of marking them
--exclude <pattern> path/glob to exclude (repeatable)
--only <pattern> restrict scan to matching globs (repeatable)
--max-file-size <kb> skip files larger than this (default 500)
--no-max-file-size scan files of any size
--no-default-excludes don't apply built-in excludes (node_modules, .git, lockfiles, binaries)
--auto-exclude / --no-auto-exclude model picks non-runtime folders to skip up front (on by default)
--serve [port] boot the local web UI when the scan finishes (default port 3737)
--provider <name> anthropic | openai | ollama | bedrock | vertex (overrides config)
--api-key <key> one-shot API key for anthropic / openai (not persisted)
--oauth-token <token> one-shot Anthropic OAuth token (not persisted)
--base-url <url> one-shot Ollama base URL (not persisted)
--region <name> one-shot region: AWS (Bedrock) or Vertex region pool
--project <id> one-shot GCP project ID for Vertex AI (not persisted)
--effort <level> reasoning effort: low | medium | high | max
--thinking <mode> thinking mode: off | adaptive | enabled (Anthropic only)
-v, --verbose verbose output-t, --template <value> slug, .md path, directory, or .txt list file; repeatable
--semgrep-rules <dir> directory of local semgrep rules an agent's preFilter can name; repeatable
-o, --output <path> output directory (default ./scan-results/)
--source-id <id> stable identity for the scanned source; resume state is reused only when it matches
--validate / --no-validate second-pass validation per finding (on by default)
--score / --no-score CVSS 3.1 scoring phase (on by default)
--scope <path> scope doc the validator consults for trust-boundary rules
--no-scope disable the built-in default scope
--rescan re-analyze files even if a prior run covered them
--revalidate-all re-validate findings that already have a verdict
--diff <commit> scope to a commit or range (accepts <ref>, a..b, or a...b)
--re-recon re-run recon + precondition plan instead of reusing the cached one
--no-recon skip recon AND precondition gating; run every -t agent
--no-summary skip writing the markdown report; state still persists
--max-files-per-batch <n> candidate files per agent batch
--max-anchors-per-batch <n> cap prefilter anchors per batch; a file with more is split (default 150)
--no-max-anchors-per-batch disable the per-batch anchor cap
--max-files-per-agent <n> cap files each agent reviews (default 300)
--no-max-files-per-agent disable the per-agent file cap
--max-batches <n> cap total agent batches this scan (default 250)
--no-max-batches disable the whole-scan batch cap
--concurrency <n> max model sessions in flight across the scan (default 5)
--dedup / --no-dedup final de-duplication pass (on by default)
--delete-duplicates physically remove duplicates instead of marking them
--exclude <pattern> path/glob to exclude (repeatable)
--only <pattern> restrict scan to matching globs (repeatable)
--max-file-size <kb> skip files larger than this (default 500)
--no-max-file-size scan files of any size
--no-default-excludes don't apply built-in excludes (node_modules, .git, lockfiles, binaries)
--auto-exclude / --no-auto-exclude model picks non-runtime folders to skip up front (on by default)
--serve [port] boot the local web UI when the scan finishes (default port 3737)
--provider <name> anthropic | openai | ollama | bedrock | vertex (overrides config)
--api-key <key> one-shot API key for anthropic / openai (not persisted)
--oauth-token <token> one-shot Anthropic OAuth token (not persisted)
--base-url <url> one-shot Ollama base URL (not persisted)
--region <name> one-shot region: AWS (Bedrock) or Vertex region pool
--project <id> one-shot GCP project ID for Vertex AI (not persisted)
--effort <level> reasoning effort: low | medium | high | max
--thinking <mode> thinking mode: off | adaptive | enabled (Anthropic only)
-v, --verbose verbose outputWhat a scan costs
Cost comes down to how much code the model reads, so it scales with the size of the scan.
A diff scan on a pull request is usually a few cents. It only looks at the files that changed, so there is very little for the model to read. This is the mode you run on every PR, and at that price you can run it on every PR without thinking about it.
A full scan of a large repository runs somewhere between two and twenty dollars, depending on the model you choose and the size of the codebase. The CLI is built around prompt caching, so the parts of each prompt that stay the same across a scan are billed at a large discount, and a second scan of the same code costs far less than the first.
If you would rather not pay per token at all, point AgentGG at your local Claude OAuth token. Scans then run against your existing Claude subscription instead of a metered API key.
False positives
With the AgentGG orchestration and the AgentGG agents, false positives dropped to under 5%. That is the result of a few things working together: the validation pass re-reads each finding and confirms it against the actual code. The scoping you can add tells it where your trust boundaries sit. The prompts are written to be specific about what counts as a real bug, and the file context handed to each agent keeps it focused on the code that matters.
In the cases where a finding was a false positive, it was not a hallucination. For example an XSS that you have to be an admin to exploit, on a page where that same admin can already modify everything anyway. The code does what the agent said, but it does not matter, because the attacker would already need the access the bug would give them. The bug was real, it just had no real impact.
This was not done with the most powerful model available. We ran GLM 5.2, an open source model, and still landed under 5%.
The findings
For this round of scanning we focused primarily on injection bug types. We will not go through every finding one by one, there are simply too many, but here are two that are already public and have CVEs assigned. We scanned each project for under five dollars, and we did not even run the full library for these, just the injection agents.
GeoTools: unauthenticated SQL injection (CVE-2026–76904, Critical)
GeoTools is the library GeoServer is built on, so this bug reaches every GeoServer in production, much of it public and unauthenticated. The vulnerable code is in the PostGIS filter encoder. When the jsonArrayContains filter function is used, the encoder takes the user-supplied value and writes it straight into a SQL string with no escaping:
// FilterToSqlHelper.java
return "(@.%s == \"%s\")".formatted(jsonPath[lastIndex], value);// FilterToSqlHelper.java
return "(@.%s == \"%s\")".formatted(jsonPath[lastIndex], value);That value comes from an OGC or CQL filter, which a public WFS or WMS endpoint accepts from anyone. A single quote in the value closes the SQL string and everything after it runs as SQL. No login required, and it runs with the database connection's privileges, which opens the door to data theft, modification, and in some cases command execution on the host machine.
An older, near-identical branch in the same file gets this right. It routes through escapeLiteral, which doubles the quotes. The vulnerable path is newer, added for PostgreSQL 12 and up, and it just skips that step. To catch the bug you have to look at both branches, see that one calls the escape helper and the other doesn't, and realize they end up in the same place. The agent traced the filter function from where it is registered, through the encoder, down to the SQL write, and compared the safe branch against the broken one. A pattern matcher would flag the String.format and leave a engineer to figure out the rest. It is also buried deep enough in the codebase that just asking an LLM to "find security issues" would never turn it up.
MapServer: unauthenticated reflected XSS (CVE-2026–77323, Moderate)
MapServer's OGC API Features output builds an HTML page from a template, and the offset and limit values from the URL get dropped straight into a <script> block without any escaping:
offset = {{ template.params.offset }};offset = {{ template.params.offset }};Those values come right off the raw request, and the template engine doesn't escape anything, so you can send an offset that closes the script tag and injects your own markup:
offset=</script><img src=x onerror=alert(document.domain)>offset=</script><img src=x onerror=alert(document.domain)>The browser closes the script at the injected </script>, reads the image tag, and runs the payload under the MapServer origin. A victim would just need to open the malicious link to be affected.
This XSS only fires when the page meets a specific precondition. The offset value is only reflected unescaped when the query returns no features, so a request that lands on a map with data at those coordinates gets rejected and looks completely safe. Even fuzzing every parameter might have missed this, because you would need the right parameter, the right payload, and an empty result set all at the same time.
A traditional SAST tool can identify an XSS sink, but it would have still required someone to read and put the pieces together for this XSS. The agent saw the value go from the URL straight into an unescaped script block, saw that the one check on it didn't cover that specific path that renders the page, and flagged it. This is the kind of XSS you either need to be very good at auditing code to catch, or throw a large amount of fuzzing at to stumble into. It will often slip past security engineers, and usually only organizations with a security budget can afford to hunt down issues like this.
Other Findings
If you want to see the full up-to-date list of public AgentGG findings, you can find them at agentgg.dev/advisories.
Run it on every pull request
Scanning once finds the issues in your code today, but it does not prevent new ones from entering. The real value comes from running AgentGG on every pull request, so a new issue gets caught while the code is still in review, before it ever ships.
The CLI is built for this. A diff scan only looks at the files that changed, so it stays fast and cheap, and it drops straight into a GitHub Actions workflow. Point it at the diff, and it runs on every PR automatically.
agentgg scan . --diff origin/main...HEAD -o ./outagentgg scan . --diff origin/main...HEAD -o ./outIf you would rather not manage the CI wiring, the model keys, and the compute yourself, the AgentGG platform does all of it for you. Connect a repo, and it scans every pull request automatically, posts findings inline on the exact lines, and gives your whole team one place to work from. It handles organizations, projects, and risk tracking across all your repos, so you can manage findings at the team level instead of one scan at a time. Same engine, same agents, nothing to host.
Closing
Open source runs a huge amount of critical infrastructure, and most of it is maintained by people with no security budget and no time to audit their own code. That gap is getting wider as AI helps people write and ship code faster than anyone can review it.
Securing this code is not something any one team can do alone, so we want AgentGG in as many hands as possible, running it and building agents for it. If you maintain a project, you can scan it yourself in a few minutes and fix issues before anyone else finds them. If you are a security researcher, it is a fast way to audit a codebase for zero-days.
npm install -g agentgg
agentgg init
agentgg scan ./src -o ./out --servenpm install -g agentgg
agentgg init
agentgg scan ./src -o ./out --serve