feat(factory): five review skills + a per-review cost model
Skills — the primary now loads conditionally (each one is input tokens), per a load table in pragent.md: - attention-tiering: classify every PR trivial/lite/full/oversized BEFORE reading anything, and cap file reads, linter runs and subagent fan-out per tier. This is the cost governor; the other skills defer to its budget. - linter-playbook: per-ecosystem detect-and-run commands scoped to changed files, the never-install rule, and how to turn a diagnostic into a finding instead of pasting tool output. - security-lens: the inline security checklist for when @security isn't worth delegating, built around a source -> sink test each finding must pass. - malicious-change: hostile-PR detection — injection aimed at the reviewer, install/CI-time hooks, obfuscated payloads, dependency confusion, logic backdoors. Complements the runtime containment added in the previous commit: that stops the agent being hijacked, this makes it report the attempt. - comment-craft: how to write problem/fix/suggestion so a maintainer can act in one read, and what to cut. pilot/cost_model.py — prices a review against published Claude and OpenAI rates (fetched 2026-08-18). Prompt sizes are measured from the factory files rather than guessed; per-tier workloads come from the tiering budgets. The model is explicit about the thing that actually dominates an agent loop: the whole conversation is resent every step, so caching moves ~2.3x of the bill. Blended over a 5/35/55/5 mix with caching on: ~$0.61/PR on Opus 5 or GPT-5.6 Sol, ~$0.24 on Sonnet 5 or Terra, ~$0.12 on Haiku 4.5, ~$0.02 on Luna. At 350 PRs/month that's ~$212 / ~$85 / ~$43 / ~$8.50. Tests: 101 -> 122. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B11e8TZZxJyzHW7jj7KWUN
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: security-lens
|
||||
description: Security review checklist for a PR diff — injection, authn/authz, secrets, crypto, SSRF/path traversal, deserialization — with the data-flow test each finding must pass. Load when the diff touches a risk path and no @security subagent was delegated.
|
||||
---
|
||||
|
||||
# Security lens
|
||||
|
||||
The `@security` subagent exists for large or heavily security-sensitive diffs.
|
||||
On `lite` and most `full` PRs you do this inline — the fan-out isn't worth it.
|
||||
This is that checklist.
|
||||
|
||||
## The test every finding must pass
|
||||
|
||||
Before reporting, establish a **source → sink** path:
|
||||
|
||||
- **source** — where attacker-influenced data enters (request param, header,
|
||||
body, uploaded filename, env in a multi-tenant context, DB row that a user
|
||||
wrote, webhook payload, PR/issue text)
|
||||
- **sink** — where it does something (SQL string, shell, filesystem path, HTTP
|
||||
request, deserializer, template, redirect, HTML)
|
||||
- **no effective sanitizer between them** — check for one; grep the helper it
|
||||
calls. "There might be no validation" is not a finding.
|
||||
|
||||
If you cannot name the source and the sink, you have a code smell, not a
|
||||
vulnerability. Report it at `low` or not at all. A false `critical` costs more
|
||||
trust than a missed `low` costs risk.
|
||||
|
||||
## Checklist
|
||||
|
||||
**Injection**
|
||||
- SQL/NoSQL built by concatenation or f-string/template interpolation → must be
|
||||
parameterized. An ORM `raw()`/`literal()` call is the usual escape hatch.
|
||||
- Shell: `subprocess` with `shell=True`, backticks, `exec`, `system` on anything
|
||||
derived from input. Argument-array form is the fix.
|
||||
- Template injection (Jinja/ERB/Handlebars rendering a user-supplied *template*,
|
||||
not just user data).
|
||||
- LDAP, XPath, header injection (CRLF in a redirect/`Set-Cookie`).
|
||||
|
||||
**AuthN / AuthZ**
|
||||
- A new endpoint/handler/route with no auth decorator/middleware its siblings
|
||||
have. Grep a neighbouring route to see the house pattern.
|
||||
- Authorization that checks *authentication* only — "is logged in" where it needs
|
||||
"owns this record". IDOR: an object fetched by an id from the request with no
|
||||
ownership predicate.
|
||||
- Secret/token/password compared with `==` → constant-time compare.
|
||||
- Session/JWT: missing expiry check, `alg: none` accepted, signature verify
|
||||
skipped, token in a URL or a log line.
|
||||
|
||||
**Secrets**
|
||||
- Literal keys, passwords, private keys, connection strings in the diff — even
|
||||
in tests or fixtures, if they're real.
|
||||
- Secrets reaching logs, error strings, telemetry, or a URL query.
|
||||
- A secret added to a client-side bundle or a container image layer.
|
||||
|
||||
**Crypto**
|
||||
- MD5/SHA1 for anything security-bearing; unsalted password hashing (needs
|
||||
bcrypt/scrypt/argon2).
|
||||
- `Math.random()` / `random.random()` for tokens, IDs, or nonces → CSPRNG.
|
||||
- Hand-rolled crypto, ECB mode, a static/reused IV, a hardcoded salt.
|
||||
|
||||
**SSRF / path traversal / upload**
|
||||
- A URL from input fetched server-side with no allow-list → SSRF (cloud metadata
|
||||
endpoints are the classic target).
|
||||
- A path built from input reaching the filesystem with no containment check.
|
||||
A `..` check alone isn't enough — the resolved path must be verified inside the
|
||||
intended root.
|
||||
- Archive extraction without a traversal/symlink check (zip-slip / tar-slip).
|
||||
- Uploads trusted by client-supplied filename or `Content-Type`.
|
||||
|
||||
**Deserialization & parsing**
|
||||
- `pickle`, `yaml.load` (not `safe_load`), Java native deserialization,
|
||||
`unserialize`, `eval`/`Function` on input.
|
||||
- XML without external-entity handling disabled (XXE).
|
||||
|
||||
**Supply chain & CI**
|
||||
- A new dependency: is the name plausible, or a typosquat of a known package?
|
||||
- A pinned version loosened to a range, or an integrity hash dropped.
|
||||
- CI changes: a workflow gaining access to secrets on an untrusted trigger, a
|
||||
third-party action pinned to a mutable tag rather than a SHA, a step that runs
|
||||
PR-authored code in a privileged context.
|
||||
|
||||
**Web**
|
||||
- Reflected/stored XSS: `innerHTML`, `dangerouslySetInnerHTML`,
|
||||
`v-html`, `|safe` on input.
|
||||
- CSRF protection removed or an endpoint switched from POST to GET.
|
||||
- CORS widened to `*` alongside credentials.
|
||||
- An open redirect from a `next`/`return_to` parameter.
|
||||
|
||||
## Severity for security findings
|
||||
|
||||
- `critical` — exploitable now by an unauthenticated or low-privileged actor:
|
||||
injection with a reachable sink, auth bypass, a live secret, RCE.
|
||||
- `high` — exploitable with a precondition (a specific role, a race, a
|
||||
non-default config), or a secret in a log.
|
||||
- `medium` — real weakness, no demonstrated path: missing defence in depth,
|
||||
weak crypto not currently load-bearing.
|
||||
- `low` — hardening.
|
||||
|
||||
Cite an authoritative URL in `reference` when the finding turns on a specific
|
||||
CVE or a documented library footgun. Don't cite a generic OWASP page for a
|
||||
generic point, and never invent a URL.
|
||||
Reference in New Issue
Block a user