One skill maps your codebase · thirty-three hunt a single vulnerability class each · one turns the results into a ranked report
/plugin marketplace add emre-guler/websec
/plugin install websec@websec/websec:scan # full review — architecture, every applicable detector, one reportNothing runs on its own. No hooks, no git hooks, no build gates. You invoke a review when you want one.
/websec:analysis ──▶ .websec/out/architecture.md
map the codebase stack · entry points · auth model · trust boundaries
│
▼
33 detectors, in parallel ──▶ .websec/out/<detector>-results.md
find candidates → check each one file per vulnerability class
│
▼
/websec:report ──▶ .websec/out/final-report.md
rank and consolidate + findings.json
/websec:scan runs all three and skips detectors the architecture rules out — no GraphQL schema, no graphql run.
Detectors do their work through two agents that ship with the plugin: websec:recon finds candidate sites, websec:verify traces and classifies them. Both are restricted to reading, searching, and writing their own output file — neither can edit your code, run your project, or reach the network. Ceilings in policy.yaml bound how many run at once and how many candidates each pass covers; anything left over is reported as unverified rather than dropped.
Already know what you are looking for? Run one detector directly:
/websec:access-control
/websec:sql-injectionEvery finding carries the code path that justifies it — not just a line number.
### [VULNERABLE] Order lookup accepts any order ID
- **File**: `app/controllers/orders_controller.rb` (lines 41–47)
- **Endpoint**: `GET /orders/:id`
- **Issue**: The handler loads the order by the path parameter alone. The
`before_action :require_login` on line 6 establishes identity but never
checks ownership, and no policy call runs before the record is rendered.
- **Impact**: Any authenticated user can read any other customer's order,
including billing address and line items.
- **Proof**: route (`config/routes.rb:22`) → `require_login`
(`orders_controller.rb:6`, session lookup only) → `Order.find(params[:id])`
(`:44`) → `render json: @order` (`:47`). No `current_user` constraint on the
query and no comparison after it.
- **Remediation**: Scope the query to the caller — `current_user.orders.find(params[:id])`
— so a foreign ID raises `RecordNotFound` instead of returning the record.
- **Dynamic Test**: authenticate as user A, request `GET <BASE_URL>/orders/<USER_B_ORDER_ID>`,
and check whether the response body contains user B's data.
- **Confidence**: high| Label | Means | Requires | |
|---|---|---|---|
| 🔴 | VULNERABLE |
The flaw is present and reachable | Full path traced, no effective control on it |
| 🟠 | LIKELY VULNERABLE |
A control exists but is incomplete, conditional, or bypassable | Path traced; the remaining doubt named |
| 🟢 | NOT VULNERABLE |
A specific control was found | The control identified at file:lines and explained |
| 🔵 | NEEDS MANUAL REVIEW |
The path could not be traced with confidence | Where tracing stopped and what a human should inspect |
🔵
NEEDS MANUAL REVIEWis a real answer, not a failure. For classes decided by deployed infrastructure — the proxy chain, hop versions, parser defaults — it is the honest one, and the finding still names the exact thing to go and check.
| 💉 Injection and execution |
|
| 🔑 Authorization and identity |
|
| 🌐 Client and browser |
|
| 📡 Requests, proxies and caches |
|
| 📦 Data, logic and surface |
|
| 🔐 Secrets and cryptography |
|
Each detector states plainly what it is not, and hands neighbouring findings to the sibling that owns them — so an authorization gap does not get filed as injection, and the same flaw is not counted twice.
Drop a .websec/policy.yaml in your repository. Any key you set replaces the default; anything you omit keeps it.
version: 1
classes:
disabled: [prototype-pollution] # no JavaScript in the runtime path
severity:
overrides: { access-control: Critical } # our objects are customer financial records
fail_threshold: High # report prints an informational Gate line
rules:
access-control:
extra_checks: ["Every query must be scoped by tenant_id, not only user_id."]
ignore_paths: ["legacy/**"]Full schema
version: 1
output_dir: .websec/out # where artefacts are written
batch_size: 3 # candidates checked per parallel worker
limits: # these multiply — see the note below
max_candidates_per_detector: 24 # excess is reported as unverified, never dropped
max_parallel_batches: 3 # concurrent workers per detector
max_detectors_in_flight: 2 # concurrent detectors in /websec:scan
classes:
disabled: [] # detectors /websec:scan should skip
severity:
overrides: {} # detector name → Critical | High | Medium | Low
fail_threshold: null # null | Critical | High | Medium | Low
rules:
<detector>:
extra_checks: [] # extra questions, plain sentences
ignore_paths: [] # globs excluded from the search
notes: "" # free text passed to the detector
report:
include_not_vulnerable: falseSee references/policy.md for the merge rules and examples/policy.example.yaml for an annotated version.
.websec/
├── policy.yaml # yours — commit it
└── out/ # generated — gitignore it
├── architecture.md
├── <detector>-results.md
├── final-report.md
└── findings.json
findings.json carries the same findings as structured data, for whatever you want to build on top.
The plugin never decides when to run — teams write that into their own CLAUDE.md. A common shape:
Run
analysisonce. On a pull request, run the detectors that match what changed. Do not recommend merging while a Critical or High finding is open, and never treatNEEDS MANUAL REVIEWas a pass.
See examples/CLAUDE.md.snippet for a paste-ready version.
New detectors, corrections, and boundary fixes are welcome. CONTRIBUTING.md covers what a good detector does and what the checker enforces; docs/detector-template.md is the starting point, and skills/access-control/SKILL.md is the calibrated example to match.
| ✅ In | Source-code review of web application vulnerability classes, plus the configuration that ships in the repository |
| ❌ Out | Sending requests to a running application, changing your code, enforcing anything |
Findings include a Dynamic Test field, but confirming it is a human step — run against a system you are authorised to test.
MIT licensed · see LICENSE