@kiwidata/grimoire 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -4
- package/README.md +107 -59
- package/dist/cli/index.js +7 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/check.js +1 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/configure.d.ts +3 -0
- package/dist/commands/configure.d.ts.map +1 -0
- package/dist/commands/configure.js +19 -0
- package/dist/commands/configure.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +2 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/core/check.d.ts.map +1 -1
- package/dist/core/check.js +165 -111
- package/dist/core/check.js.map +1 -1
- package/dist/core/ci.d.ts.map +1 -1
- package/dist/core/ci.js +50 -69
- package/dist/core/ci.js.map +1 -1
- package/dist/core/configure.d.ts +14 -0
- package/dist/core/configure.d.ts.map +1 -0
- package/dist/core/configure.js +434 -0
- package/dist/core/configure.js.map +1 -0
- package/dist/core/detect.d.ts.map +1 -1
- package/dist/core/detect.js +153 -26
- package/dist/core/detect.js.map +1 -1
- package/dist/core/diff.d.ts.map +1 -1
- package/dist/core/diff.js +62 -93
- package/dist/core/diff.js.map +1 -1
- package/dist/core/doc-style.d.ts +0 -4
- package/dist/core/doc-style.d.ts.map +1 -1
- package/dist/core/doc-style.js +103 -22
- package/dist/core/doc-style.js.map +1 -1
- package/dist/core/docs.js +202 -170
- package/dist/core/docs.js.map +1 -1
- package/dist/core/health.d.ts +6 -0
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +133 -96
- package/dist/core/health.js.map +1 -1
- package/dist/core/hooks.d.ts +0 -3
- package/dist/core/hooks.d.ts.map +1 -1
- package/dist/core/hooks.js +11 -16
- package/dist/core/hooks.js.map +1 -1
- package/dist/core/init.d.ts +2 -0
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +230 -406
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +55 -65
- package/dist/core/list.js.map +1 -1
- package/dist/core/risk-register.d.ts +17 -0
- package/dist/core/risk-register.d.ts.map +1 -0
- package/dist/core/risk-register.js +73 -0
- package/dist/core/risk-register.js.map +1 -0
- package/dist/core/shared-setup.d.ts +0 -40
- package/dist/core/shared-setup.d.ts.map +1 -1
- package/dist/core/shared-setup.js +92 -56
- package/dist/core/shared-setup.js.map +1 -1
- package/dist/core/status.d.ts.map +1 -1
- package/dist/core/status.js +42 -52
- package/dist/core/status.js.map +1 -1
- package/dist/core/test-quality.d.ts +0 -8
- package/dist/core/test-quality.d.ts.map +1 -1
- package/dist/core/test-quality.js +24 -30
- package/dist/core/test-quality.js.map +1 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/trace.js +67 -75
- package/dist/core/trace.js.map +1 -1
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +61 -11
- package/dist/core/update.js.map +1 -1
- package/dist/core/validate.d.ts +1 -4
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/core/validate.js +126 -148
- package/dist/core/validate.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +15 -5
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +63 -42
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/fs.d.ts +0 -12
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +0 -12
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/paths.d.ts +0 -6
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +0 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/spawn.d.ts +0 -3
- package/dist/utils/spawn.d.ts.map +1 -1
- package/dist/utils/spawn.js +0 -3
- package/dist/utils/spawn.js.map +1 -1
- package/package.json +1 -1
- package/skills/grimoire-apply/SKILL.md +89 -25
- package/skills/grimoire-audit/SKILL.md +21 -1
- package/skills/grimoire-bug/SKILL.md +48 -9
- package/skills/grimoire-commit/SKILL.md +3 -2
- package/skills/grimoire-design/SKILL.md +259 -0
- package/skills/grimoire-design-consult/SKILL.md +200 -0
- package/skills/grimoire-discover/SKILL.md +139 -109
- package/skills/grimoire-draft/SKILL.md +131 -15
- package/skills/grimoire-plan/SKILL.md +119 -46
- package/skills/grimoire-pr/SKILL.md +7 -10
- package/skills/grimoire-pr-review/SKILL.md +46 -115
- package/skills/grimoire-precommit-review/SKILL.md +205 -0
- package/skills/grimoire-refactor/SKILL.md +6 -6
- package/skills/grimoire-review/SKILL.md +95 -156
- package/skills/grimoire-verify/SKILL.md +40 -7
- package/skills/grimoire-vuln-remediate/SKILL.md +107 -0
- package/skills/grimoire-vuln-triage/SKILL.md +109 -0
- package/skills/references/adversarial-personas.md +225 -0
- package/skills/references/brand-tokens-format.md +186 -0
- package/skills/references/code-quality.md +172 -0
- package/skills/references/container-scan-triage.md +102 -0
- package/skills/references/dependency-vuln-triage.md +236 -0
- package/skills/references/design-heuristics.md +138 -0
- package/skills/references/design-input-formats.md +190 -0
- package/skills/references/pattern-guard.md +180 -0
- package/skills/references/principles.md +82 -0
- package/skills/references/refactor-scan-categories.md +154 -2
- package/skills/references/review-personas.md +406 -0
- package/skills/references/security-compliance.md +22 -1
- package/skills/references/testing-contracts.md +1 -1
- package/skills/references/visual-fidelity.md +206 -0
- package/templates/accepted-risks.yml +47 -0
- package/templates/brand-tokens-example.json +13 -0
- package/templates/brand-voice-example.md +22 -0
- package/templates/constraints.md +25 -0
- package/templates/design-tool-setup-stub.md +59 -0
- package/dist/commands/archive.d.ts +0 -3
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/archive.js +0 -22
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/log.d.ts +0 -3
- package/dist/commands/log.d.ts.map +0 -1
- package/dist/commands/log.js +0 -15
- package/dist/commands/log.js.map +0 -1
- package/dist/commands/map.d.ts +0 -3
- package/dist/commands/map.d.ts.map +0 -1
- package/dist/commands/map.js +0 -17
- package/dist/commands/map.js.map +0 -1
- package/dist/core/archive.d.ts +0 -9
- package/dist/core/archive.d.ts.map +0 -1
- package/dist/core/archive.js +0 -92
- package/dist/core/archive.js.map +0 -1
- package/dist/core/log.d.ts +0 -8
- package/dist/core/log.d.ts.map +0 -1
- package/dist/core/log.js +0 -150
- package/dist/core/log.js.map +0 -1
- package/dist/core/map.d.ts +0 -9
- package/dist/core/map.d.ts.map +0 -1
- package/dist/core/map.js +0 -302
- package/dist/core/map.js.map +0 -1
- package/templates/dupignore +0 -93
- package/templates/mapignore +0 -58
- package/templates/mapkeys +0 -65
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: grimoire-verify
|
|
3
|
-
description: Verify that implementation matches feature specs and decision records. Use after apply is complete, before
|
|
3
|
+
description: Verify that implementation matches feature specs and decision records. Use after apply is complete, before committing and opening a PR.
|
|
4
4
|
compatibility: Designed for Claude Code (or similar products)
|
|
5
5
|
metadata:
|
|
6
6
|
author: kiwi-data
|
|
@@ -9,11 +9,11 @@ metadata:
|
|
|
9
9
|
|
|
10
10
|
# grimoire-verify
|
|
11
11
|
|
|
12
|
-
Verify that implementation matches the feature specs and decision records. Run after apply, before
|
|
12
|
+
Verify that implementation matches the feature specs and decision records. Run after apply, before commit and PR.
|
|
13
13
|
|
|
14
14
|
## Triggers
|
|
15
15
|
- User wants to verify a grimoire change is correctly implemented
|
|
16
|
-
- User asks to check, verify, or review a change before
|
|
16
|
+
- User asks to check, verify, or review a change before committing
|
|
17
17
|
- Loose match: "verify", "check", "review" with a change reference
|
|
18
18
|
|
|
19
19
|
## Routing
|
|
@@ -170,6 +170,36 @@ Check for features that exist in specs but may no longer be implemented:
|
|
|
170
170
|
- Step definitions with `pass` or `NotImplementedError` bodies
|
|
171
171
|
- Features tagged `@skip` or `@wip` that have been in that state for a long time
|
|
172
172
|
|
|
173
|
+
### 6b. Code Quality Audit
|
|
174
|
+
|
|
175
|
+
For every production file changed in this implementation, run an independent quality check — not a re-read of what the implementing agent self-reported.
|
|
176
|
+
|
|
177
|
+
**A. Walk the seven-point checklist from `../references/code-quality.md` on each changed file:**
|
|
178
|
+
|
|
179
|
+
1. **Reuse before write** — any new helper/function that duplicates existing code? Flag the duplicate and the existing function.
|
|
180
|
+
2. **Branching budget** — any function with more than ~7 branches (`if`/`else`/`case`/ternary/`&&`/`||`)? Name it and count.
|
|
181
|
+
3. **Function size** — any function body over ~30 lines? Flag it. "One job per function" — if the name needs "and", it's two functions.
|
|
182
|
+
4. **Defensive code inside trust boundary** — `if x is None` guards on non-nullable types, `isinstance` checks on values the codebase just built, `try/except` with no real recovery? Flag each.
|
|
183
|
+
5. **Names** — any local named `data`, `result`, `temp`, `info`, `obj`, `item`, or `value` when a specific name would fit?
|
|
184
|
+
6. **Premature abstraction** — any new `BaseX`, factory, strategy, config object, or registry pattern with a single caller? Any wrapper function that only renames arguments?
|
|
185
|
+
7. **Comments** — any comment that restates the code (`# loop over users`), references the current task/PR/ticket, is a multi-line docstring on a private function, or whose removal would not confuse a future reader?
|
|
186
|
+
|
|
187
|
+
**B. Classify findings:**
|
|
188
|
+
- **[critical]** — premature abstraction (new base class / factory / strategy for one caller), or dead code (function/class written but never called)
|
|
189
|
+
- **[warning]** — function too large, too many branches, defensive guards inside trust boundary, generic names
|
|
190
|
+
- **[suggestion]** — comment noise, minor naming issue
|
|
191
|
+
|
|
192
|
+
**C. Report format:**
|
|
193
|
+
```
|
|
194
|
+
## Code Quality
|
|
195
|
+
- [ ] **[critical]** `src/auth.ts:12` — `BaseTokenValidator` has one subclass; inline it
|
|
196
|
+
- [ ] **[critical]** `src/auth.ts:88` — `_validate_helper` never called outside `validate()`; inline it
|
|
197
|
+
- [ ] **[warning]** `src/auth.ts:45` — `process_request` is 62 lines with 9 branches; split by responsibility
|
|
198
|
+
- [ ] **[suggestion]** `src/auth.ts:31` — comment "# check if token is expired" restates the code; remove
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If no issues: `## Code Quality — clean`.
|
|
202
|
+
|
|
173
203
|
### 7. Generate Report
|
|
174
204
|
Produce a structured report:
|
|
175
205
|
|
|
@@ -190,6 +220,9 @@ Produce a structured report:
|
|
|
190
220
|
- [ ] **[critical]** [OWASP/CWE tag] <violation> — `file:line`
|
|
191
221
|
- [ ] **[warning]** [OWASP/CWE tag] <concern> — `file:line`
|
|
192
222
|
|
|
223
|
+
## Code Quality
|
|
224
|
+
- [ ] **[critical/warning/suggestion]** <issue> — `file:line`
|
|
225
|
+
|
|
193
226
|
## Warnings
|
|
194
227
|
- [ ] <issue description> — `file:line`
|
|
195
228
|
|
|
@@ -203,8 +236,8 @@ Produce a structured report:
|
|
|
203
236
|
|
|
204
237
|
### 8. Recommend Next Steps
|
|
205
238
|
Based on the report:
|
|
206
|
-
- **All clear** → recommend
|
|
207
|
-
- **Critical issues** → must fix before
|
|
239
|
+
- **All clear** → recommend committing and opening a PR (git diff is the staging area, the PR is the changelog)
|
|
240
|
+
- **Critical issues** → must fix before committing
|
|
208
241
|
- **Warnings only** → user decides whether to fix or accept
|
|
209
242
|
- **Dead features found** → suggest a removal change or updating the features
|
|
210
243
|
|
|
@@ -218,6 +251,6 @@ Based on the report:
|
|
|
218
251
|
|
|
219
252
|
## Done
|
|
220
253
|
When the verification report is presented, the workflow is complete. Suggest next steps based on findings:
|
|
221
|
-
- **All clear** → `grimoire
|
|
222
|
-
- **Critical issues** → must fix before
|
|
254
|
+
- **All clear** → `grimoire-commit` then `grimoire-pr`
|
|
255
|
+
- **Critical issues** → must fix before committing
|
|
223
256
|
- **Warnings only** → user decides whether to fix or accept
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grimoire-vuln-remediate
|
|
3
|
+
description: File the dev work from a vulnerability triage — turn affected findings into tickets in the configured bug tracker, record risk-accepted items with expiry, and stub grimoire changes for non-trivial fixes. Consumes a grimoire-vuln-triage record. Use after triage, when you're ready to action the findings that actually matter.
|
|
4
|
+
compatibility: Designed for Claude Code (or similar products)
|
|
5
|
+
metadata:
|
|
6
|
+
author: kiwi-data
|
|
7
|
+
version: "0.1"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# grimoire-vuln-remediate
|
|
11
|
+
|
|
12
|
+
`grimoire-vuln-triage` decides *what matters*. This skill **files the work** for the findings that survived: it turns `affected` advisories into tickets in the team's configured bug reporting system, records `accept` items in a risk-acceptance register with an expiry, and stubs a grimoire change for fixes too big to be a one-line bump. It never invents urgency — it acts on the triage's verdicts.
|
|
13
|
+
|
|
14
|
+
The split is deliberate: triage classifies (read-only, repeatable), remediate commits to action (writes tickets, branches, registers). Run triage first.
|
|
15
|
+
|
|
16
|
+
## Triggers
|
|
17
|
+
- After triage: "file these", "create the tickets", "remediate the CVEs", "action the vuln triage"
|
|
18
|
+
- "open tickets for the affected vulnerabilities", "track the risk-accepted ones"
|
|
19
|
+
- User points at a triage record and asks to take it forward
|
|
20
|
+
- Loose match: "vuln remediate", "file vulnerabilities", "security tickets", "remediation plan"
|
|
21
|
+
|
|
22
|
+
## Routing
|
|
23
|
+
- No triage record yet → `grimoire-vuln-triage` first. Do not file tickets off a raw scanner dump — file only what triage marked `affected`.
|
|
24
|
+
- A fix that's a real code change (new abstraction, behavior change, schema) → stub a change and hand to `grimoire-draft` / `grimoire-plan` / `grimoire-apply`.
|
|
25
|
+
- A confirmed-exploitable code defect a developer will fix immediately → `grimoire-bug` (reproduction-first).
|
|
26
|
+
- Infra follow-ups (base-image bump, secrets-in-image, IaC misconfig from the triage's "Infra follow-ups") → infra board / `grimoire-draft`, not app remediation.
|
|
27
|
+
|
|
28
|
+
## Prerequisites
|
|
29
|
+
- A triage record at `.grimoire/security/vulns/<run-date>/triage.md` (from `grimoire-vuln-triage`). If the user points at a different path, use it.
|
|
30
|
+
- `.grimoire/config.yaml` — read `bug_trackers` (MCP) for where to file. If `none`, fall back to a local remediation doc.
|
|
31
|
+
|
|
32
|
+
## Workflow
|
|
33
|
+
|
|
34
|
+
### 1. Read the triage record
|
|
35
|
+
|
|
36
|
+
Load the triage. Pull the actionable buckets — **ignore `fixed` and `not_affected`** (triage already dismissed them; they are the audit trail, not work):
|
|
37
|
+
- **hotfix-now** — expedited, confidential handling.
|
|
38
|
+
- **next-release** — normal release-cycle work.
|
|
39
|
+
- **accept** — no fix available / low risk → goes to the risk-acceptance register, not a fix ticket.
|
|
40
|
+
- **under_investigation** — file a time-boxed investigation task, not a fix. **Does not enter the risk register** — it isn't a decision yet. When the investigation resolves, re-run remediate so it routes to close / ticket / register.
|
|
41
|
+
- **Infra follow-ups** — route separately (Step 5).
|
|
42
|
+
|
|
43
|
+
Cross-check the record's totals against what you're about to file so nothing is dropped or invented.
|
|
44
|
+
|
|
45
|
+
### 2. Decide the fix type per affected item
|
|
46
|
+
|
|
47
|
+
For each `affected` advisory, classify the remediation so it routes correctly:
|
|
48
|
+
- **Trivial bump** — a patch/minor version with a fix exists (`upgrade X 1.2.3 → 1.2.4`), no API break. Can be a direct PR or a simple ticket.
|
|
49
|
+
- **Non-trivial fix** — major-version bump, code changes, base-image rebuild, or a transitive dep that needs a constraint/override. Needs design → stub a grimoire change (Step 4).
|
|
50
|
+
- **No fix available** — `will_not_fix` / no fixed version. Not a fix ticket → risk-acceptance register with an expiry (Step 3). Never file an "upgrade X" ticket when no fixed version exists.
|
|
51
|
+
|
|
52
|
+
### 3. File the work into the configured bug reporting system
|
|
53
|
+
|
|
54
|
+
Read `bug_trackers` in `.grimoire/config.yaml`.
|
|
55
|
+
|
|
56
|
+
**If a tracker MCP is configured (Jira / Linear / GitHub):**
|
|
57
|
+
- **Idempotency first** — search the tracker for the CVE/GHSA id before creating anything. If a ticket exists, update it (link the triage, refresh urgency); don't duplicate. Mirror both directions like `grimoire-bug-triage`.
|
|
58
|
+
- **One ticket per advisory** (or per package-upgrade when several CVEs share one bump — group by the fix, not the CVE). Include: id(s), component + version, fixed version / action, urgency, reachability + exposure summary, and a link back to `.grimoire/security/vulns/<run-date>/triage.md`. Set priority from urgency (hotfix-now → highest).
|
|
59
|
+
- **hotfix-now is confidential** — mirror `grimoire-bug-triage` §7. Do **not** post exploit details, reachability specifics, or a step-by-step in a public tracker. If the tracker is public, the ticket states impact + "fix in progress, details held privately"; the detail stays in the local triage record. Notify the security owner out of band. Recommend an expedited branch (non-descriptive name) + out-of-band release.
|
|
60
|
+
- **under_investigation** → a time-boxed task ("confirm reachability of <CVE> in <path> by <date>"), assigned, with the open question from triage.
|
|
61
|
+
|
|
62
|
+
**If `bug_trackers: none`:**
|
|
63
|
+
- Write `.grimoire/security/vulns/<run-date>/remediation.md` — a checklist: one entry per actionable item with `[ ]` checkbox, id(s), action (exact upgrade or change-stub link or accept-ref), urgency, suggested owner, and the triage link. This is the deliverable the team works from until a tracker exists. Tell the user where it is.
|
|
64
|
+
|
|
65
|
+
### 4. Stub a grimoire change for non-trivial fixes
|
|
66
|
+
|
|
67
|
+
For each non-trivial fix, create `.grimoire/changes/<change-id>/manifest.md` so the normal build workflow takes over:
|
|
68
|
+
- Frontmatter `status: proposed`, plus `source: vuln-triage`, the CVE id(s), and the triage path.
|
|
69
|
+
- **Why**: the vulnerability + why a one-line bump isn't enough (major break, code path affected, transitive constraint).
|
|
70
|
+
- **Scope**: the upgrade/change needed and the blast radius from triage (which code paths touch the vulnerable surface).
|
|
71
|
+
- Point the user at `grimoire-draft` / `grimoire-plan` to continue. Reference the change-id from the ticket so the trail is intact.
|
|
72
|
+
|
|
73
|
+
### 5. Record risk-accepted items (with expiry)
|
|
74
|
+
|
|
75
|
+
**Register invariant: only a settled `accept` verdict enters the register.** The register means "we triaged this, decided, and consciously accepted the residual risk." Two states must stay out of it:
|
|
76
|
+
- **`under_investigation`** — not decided yet. It gets an investigation task (Step 3), nothing in the register. The register's `vex_justification` must be a real VEX code, not "pending"; a finding you can't yet justify hasn't been accepted. When the investigation resolves, *then* it routes — to `not_affected` (close), `affected` (ticket/change), or `accept` (register).
|
|
77
|
+
- **`not_affected`** — already dismissed by triage, deterministically, every scan. It needs no register entry; adding one would bloat the register with the unreachable os-package noise the reachability verdict already suppresses.
|
|
78
|
+
|
|
79
|
+
For every settled `accept` item, append to `.grimoire/security/accepted-risks.yml` (create from the template if absent). Each entry: `cve`, `component`, `vex_justification` (a real code), `reason` (why it's acceptable — reachability/exposure/no-fix), `owner`, `accepted` date, and an **`expires`** date (when to re-triage — sooner for higher residual risk, default ~90 days). An accepted risk is not closed — it's scheduled for re-evaluation.
|
|
80
|
+
|
|
81
|
+
**This register is read back by `grimoire-vuln-triage` during reconciliation** — an unexpired entry auto-suppresses that CVE as a known-accepted, so the same finding doesn't re-flood the queue next scan. An **expired** entry is re-surfaced for re-triage. Don't accept the same CVE twice; update the existing entry.
|
|
82
|
+
|
|
83
|
+
A no-fix os-package finding that is genuinely **`affected` and accepted** (reachable, no patched base yet) belongs here — with `component_type: os-package` and the trace summary from `container-scan-triage.md`. One that is **`not_affected`** (unreachable, like a headless API's GPU/terminal libs) does **not**.
|
|
84
|
+
|
|
85
|
+
### 6. Optionally execute trivial bumps (skippable)
|
|
86
|
+
|
|
87
|
+
Offer — don't assume — to apply trivial bumps directly: edit the manifest pin, run the lockfile update (`uv lock` / `npm install` / etc.), and run the configured `dep_audit` to confirm the advisory clears. Keep it on a branch. This step is **opt-in**; if the user just wants tickets, file and stop. If you do apply, the supply-chain rules in `security-compliance.md` still hold (committed lockfile + integrity hashes).
|
|
88
|
+
|
|
89
|
+
### 7. Report and update the record
|
|
90
|
+
|
|
91
|
+
Update the triage record (or a sibling `remediation.md`) with what was filed: ticket refs, change-ids, register entries. Report the headline:
|
|
92
|
+
- N tickets filed (M hotfix-now expedited), K risk-accepted (next review dates), J change stubs, I investigations.
|
|
93
|
+
- Any hotfix-now: restate it's flagged for the security owner and expedited.
|
|
94
|
+
- Where the artifacts are (tracker links / local docs / register).
|
|
95
|
+
|
|
96
|
+
## Important
|
|
97
|
+
- **Act only on triage verdicts.** This skill files what triage marked `affected` / `accept` / `under_investigation`. It does not re-triage, re-rank, or escalate on its own. If a verdict looks wrong, send it back to `grimoire-vuln-triage`, don't override it here.
|
|
98
|
+
- **No fixed version → no upgrade ticket.** A `will_not_fix` / no-fix finding goes to the risk register with an expiry, or to infra for a base bump. Filing "upgrade X" when X has no fix wastes a developer's time.
|
|
99
|
+
- **Group by the fix, not the CVE.** One upgrade often clears several advisories — one ticket, listing all the CVEs it resolves.
|
|
100
|
+
- **hotfix-now is confidential.** No exploit detail in public trackers; impact-only, details local, security owner notified out of band, expedited branch.
|
|
101
|
+
- **Idempotent.** Search before filing; update existing tickets and register entries instead of duplicating. Sync local ↔ tracker both ways.
|
|
102
|
+
- **`accept` carries an expiry and feeds back into triage.** The register is the loop that stops accepted noise from re-flooding every scan — and re-surfaces it when the expiry passes.
|
|
103
|
+
- **The register holds only settled `accept` verdicts.** `under_investigation` gets an investigation task, never a register entry (it isn't decided); `not_affected` is already suppressed by triage's reachability verdict each run. Keep the invariant clean: register = decided + accepted, with a real VEX justification.
|
|
104
|
+
- **Steps are skippable.** Filing, change-stubbing, and direct bumps are independent — do what the user asked for and stop.
|
|
105
|
+
|
|
106
|
+
## Done
|
|
107
|
+
When every actionable triage finding has a home — a ticket (or remediation.md entry), a change stub, or a risk-register entry with an expiry — and the triage record reflects what was filed, remediation is complete. Hotfix-now items are flagged and confidential; accepted items are scheduled for re-triage.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: grimoire-vuln-triage
|
|
3
|
+
description: Triage vulnerability scans from any source — npm audit, pip-audit, osv-scanner, Trivy, Grype, Snyk, Dependabot, SARIF, or a report a teammate forwards — against our actual deployment model and recorded mitigating controls. Reconciles stale scans against the current tree, then decides the one thing that matters per finding — drop-everything hotfix vs next release cycle — and suppresses non-actionable noise with VEX verdicts. Use when a scanner produces a flood of CVEs and you need to know which actually matter here.
|
|
4
|
+
compatibility: Designed for Claude Code (or similar products)
|
|
5
|
+
metadata:
|
|
6
|
+
author: kiwi-data
|
|
7
|
+
version: "0.2"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# grimoire-vuln-triage
|
|
11
|
+
|
|
12
|
+
Vulnerability scanners flag every CVE that *exists* in your tree or image, ranked by CVSS base score — which knows nothing about your deployment, and nothing about whether you already upgraded past it. Most findings are not exploitable as you actually run the code. This skill is **scanner-agnostic**: it normalizes whatever it's handed (npm audit, pip-audit, osv-scanner, Trivy, Grype, Snyk, Dependabot, SARIF, or a freeform CSV/markdown report) into one canonical model, reconciles it against the current tree, and triages each surviving advisory against **our** deployment and controls to answer the only question that drives action:
|
|
13
|
+
|
|
14
|
+
> **Drop everything and hotfix now, or let it ride the normal testing / release cycle?**
|
|
15
|
+
|
|
16
|
+
It produces VEX verdicts (`fixed` / `not_affected` / `affected` / `under_investigation`) so non-actionable findings are dismissed with an auditor-defensible justification, and an urgency (`hotfix-now` / `next-release` / `accept`) for the ones that survive. Covers application dependencies, OS packages from container scans, and runtime/build tooling alike.
|
|
17
|
+
|
|
18
|
+
This skill **classifies**. Filing the dev work (tickets in the configured bug reporting system) is the job of `grimoire-vuln-remediate`, which consumes this triage.
|
|
19
|
+
|
|
20
|
+
## Triggers
|
|
21
|
+
- A scanner produces a wall of findings: "npm audit found 40 vulnerabilities", "pip-audit is screaming", "trivy flagged 200 CVEs in the image", "triage these CVEs"
|
|
22
|
+
- "Is this CVE actually a problem for us?", "do we need to hotfix this or can it wait?"
|
|
23
|
+
- "which of these vulnerabilities actually matter", "filter out the noise from the scan"
|
|
24
|
+
- A teammate forwards a scan report (any tool, any format) and asks what's real
|
|
25
|
+
- Loose match: "vuln triage", "CVE triage", "security scan", "trivy/grype/snyk results", "audit results", "image scan"
|
|
26
|
+
|
|
27
|
+
## Routing
|
|
28
|
+
- A *reported* security bug (not a scanner finding) → `grimoire-bug-triage` (it has a security classification path)
|
|
29
|
+
- A dependency *add/upgrade* review (lockfile, floating ranges, supply chain) → review-time; see `../references/security-compliance.md` § Supply Chain Defense, enforced by `grimoire-review` / `grimoire-precommit-review`
|
|
30
|
+
- After this triage, to file dev work → `grimoire-vuln-remediate`
|
|
31
|
+
- Persistent IaC/container misconfig (root user, no limits, `:latest` base) → `grimoire-draft`/infra, not an app hotfix
|
|
32
|
+
- A control gap surfaced here (a mitigation assumed but never recorded) → `grimoire-draft` to write the MADR
|
|
33
|
+
|
|
34
|
+
## Prerequisites
|
|
35
|
+
- A scan to triage: output of `config.tools.dep_audit` / `config.tools.security`, a saved scan file (e.g. `reports/security/...`), or pasted text. Prefer machine-readable (`--json` / SARIF) over a human table.
|
|
36
|
+
- The repo's current lockfile/manifest (or deployed image tag) available for reconciliation.
|
|
37
|
+
- Network access for KEV + EPSS enrichment (degrades gracefully to CVSS-only if offline).
|
|
38
|
+
- Best results with `codebase-memory-mcp` for reachability; falls back to grep.
|
|
39
|
+
|
|
40
|
+
## Workflow
|
|
41
|
+
|
|
42
|
+
Read `../references/dependency-vuln-triage.md` now — it has the canonical model, the format adapters, the reconciliation rule, the enrichment feeds, the type-aware reachability rules, the VEX statuses, the urgency tree, and the record format. Follow it. The steps below are the spine.
|
|
43
|
+
|
|
44
|
+
### 1. Normalize the scan into the canonical model (any scanner)
|
|
45
|
+
|
|
46
|
+
Identify the source and map each finding to the canonical advisory (reference § Step 1): `id`, `aliases`, `cve`, `component`, `component_type` (`library`/`os-package`/`container`/`iac`/`runtime`), `installed_version`, `fixed_version`, `severity`/`cvss`, `target`, `scanner`. Use the format adapter for the tool you were handed — **never assume one tool's field names apply to another** (npm's `isDirect`/`via` ≠ pip-audit's `aliases`/`fix_versions` ≠ Trivy's `Results[].Vulnerabilities[]` with `Class`/`Type`/`Status`). For an unknown/freeform report, extract the minimum (`id`, `component`, version, fixed version) and mark the rest `unknown`. If you can't parse it, ask for `--json`/SARIF rather than guessing.
|
|
47
|
+
|
|
48
|
+
**Dedup + non-CVE results.** Collapse the same CVE listed across multiple packages (Trivy does this constantly) to unique `(id, component_type)`, keeping the package list — report `raw_findings → unique_advisories` so the noise reduction is visible. Don't discard `Class: secret` / `Class: config` results — they aren't package CVEs; route them (secrets-in-image, Dockerfile/k8s misconfig) to infra/`grimoire-draft`, not triage.
|
|
49
|
+
|
|
50
|
+
### 2. Reconcile against the current tree FIRST (mandatory)
|
|
51
|
+
|
|
52
|
+
Before any enrichment, compare each advisory's `installed_version` against what the repo resolves **right now** (reference § Step 2): read the live lockfile/manifest (`uv.lock`/`poetry.lock`/`package-lock.json`/`go.sum`/`Cargo.lock`/`Gemfile.lock`), or for container/OS findings check the currently deployed image tag / Dockerfile base. If the current version ≥ `fixed_version`, mark **`fixed`** and drop it before enrichment — record it under "Already fixed" as the audit trail. Honor manifest comments / prior triage that already dismiss a CVE. **Never file remediation for an advisory you haven't confirmed still exists.** On a stale scan this pass clears most of the queue.
|
|
53
|
+
|
|
54
|
+
**Honor the risk-acceptance register.** Read `.grimoire/security/accepted-risks.yml` (written by `grimoire-vuln-remediate`). An **unexpired** entry for a CVE means it was already triaged and consciously accepted → carry it as known-accepted, don't re-escalate (cite the register entry). An **expired** entry → re-triage it fresh (the acceptance lapsed). This is what stops accepted findings from re-flooding the queue every scan.
|
|
55
|
+
|
|
56
|
+
### 3. Enrich the survivors — KEV then EPSS
|
|
57
|
+
|
|
58
|
+
Per the reference: fetch the **CISA KEV** catalog once and match every `cve`/`aliases` (known-exploited = strongest hotfix signal); fetch **EPSS** for all CVE ids (batch, comma-separated) for exploit probability. Cache both in the run dir. If offline, record `kev-feed: offline` / `epss-fetched: false` and proceed on CVSS + reachability + exposure — say so. IaC/config findings skip threat-intel (no CVE).
|
|
59
|
+
|
|
60
|
+
### 4. Reachability — type-aware, the cheapest big filter
|
|
61
|
+
|
|
62
|
+
Judge reachability by `component_type` (reference § Reachability):
|
|
63
|
+
- **library** — dev/test-only (infer from lockfile groups, not a flag) → `not_affected` in prod; imported at all? (`search_graph`/`search_code`); vulnerable function actually called? (`trace_path`).
|
|
64
|
+
- **os-package** (container scan) — judge **two separate axes**: *reachability* (is the vulnerable code called by untrusted input? grep the consumer, not the C package name) and *removability* (how installed — explicit/transitive/base-image/builder-only — and what breaks). **Unreachable ≠ removable.** Never recommend removing a package (or "slim the base image") without tracing the install path and naming the post-change test; many base-OS/transitive libs aren't removable. No-fix / `will_not_fix` → accept or base bump, never an "upgrade X" ticket. Full discipline + maps + anti-patterns in `../references/container-scan-triage.md`.
|
|
65
|
+
- **runtime** (interpreter/build tool, e.g. pip) — invoked at runtime or only at build time? Build-only, not in the running container → `not_affected` at runtime (check entrypoint/CMD).
|
|
66
|
+
- **container/iac** — not a CVE; triage on whether the misconfig is reachable in our deployment; route persistent ones to infra.
|
|
67
|
+
|
|
68
|
+
Also check **advisory preconditions** against real config — many CVEs are conditional (a setting, ASGI-vs-WSGI, a middleware). Precondition met → raises urgency; absent → clean `not_affected`. Record reachability provenance (`graph-verified`/`grep-asserted`/`image-layer`/`unknown`).
|
|
69
|
+
|
|
70
|
+
**Resolve unknowns in the moment — don't default to `under_investigation`.** When reachability isn't settled by grep: trace deeper (`trace_path` from routes to the vulnerable binding), then **ask the human the one decisive question** (e.g. "does any endpoint parse user-supplied XML?") — a single yes/no usually collapses several findings to `not_affected` or `affected` on the spot, sparing both a register entry and a follow-up task. Reserve `under_investigation` for questions nobody in the session can answer (needs a runtime check / a teammate / an external dependency); time-box those and name what must be checked.
|
|
71
|
+
|
|
72
|
+
### 5. Exposure & controls — read, don't invent
|
|
73
|
+
|
|
74
|
+
Per reference § Exposure & Controls: `.grimoire/docs/context.yml` (internet-facing vs internal vs lambda/batch, infra, services) and MADR decisions (`Security (CIA)` rows, WAF/network-isolation/auth/tenancy decisions). A documented control that breaks the attack path is a legitimate damper / VEX `inline_mitigations_already_exist`. **Do not create a controls config file** — controls live in MADR + context.yml. A verdict-changing control that's recorded nowhere → log under "Control gaps", don't credit it silently.
|
|
75
|
+
|
|
76
|
+
### 6. Assign VEX verdict + urgency
|
|
77
|
+
|
|
78
|
+
Apply the decision tree (reference § VEX + § Urgency): `fixed` (Step 2) → `not_affected` (reachability/precondition) → `affected` with **hotfix-now** / **next-release** / **accept** (with expiry) → `under_investigation` (time-boxed). Fail safe on unknowns (KEV + public + unknown reachability → hotfix-now); don't manufacture emergencies (no KEV + low EPSS + unknown → under_investigation + next-release).
|
|
79
|
+
|
|
80
|
+
### 7. Contrarian pass — calibrate before you escalate
|
|
81
|
+
|
|
82
|
+
Run the **Contrarian calibration pass** (`../references/review-personas.md` §4.8) over every `hotfix-now` and `affected` verdict: steel-man "we are not affected", name the assumption, run the inversion test (does a rushed hotfix / base-image swap ship *new* risk?), check severity clears all three bars (reachable + exploitable-as-deployed + real blast radius). Emit `[hotfix upheld]` / `[hotfix → next-release]` / `[finding dropped]` per escalation with one line of evidence. Summary counts are **post-Contrarian**. Calibration, not veto.
|
|
83
|
+
|
|
84
|
+
### 8. Write the triage record
|
|
85
|
+
|
|
86
|
+
Write `.grimoire/security/vulns/<run-date>/triage.md` in the reference format: frontmatter totals, then sections — Hotfix now / Next release / Risk-accepted / **Already fixed** (the stale-scan audit trail) / Not affected / Under investigation / Control gaps. Cache the KEV snapshot and EPSS responses alongside for reproducibility.
|
|
87
|
+
|
|
88
|
+
### 9. Report and hand off
|
|
89
|
+
|
|
90
|
+
Headline: how many findings, how many already **fixed** (stale scan), how many **not_affected** (noise) with the dominant reason, how many real (`affected`), how many **hotfix-now** — and *why* the hotfixes are hotfixes (one line each).
|
|
91
|
+
- **Any hotfix-now** → flag immediately, notify the security owner, recommend expedited fix.
|
|
92
|
+
- **affected (any urgency)** → "Run `grimoire-vuln-remediate` to file these into the bug tracker."
|
|
93
|
+
- **Control gaps** → "Assumed but unrecorded — `grimoire-draft` to capture them."
|
|
94
|
+
|
|
95
|
+
## Important
|
|
96
|
+
- **Reconcile before you triage.** A scan is a snapshot; the tree moves. Confirm each finding still exists in the current lockfile/image before spending any effort on it — it's the single highest-leverage step and stops you filing dead tickets.
|
|
97
|
+
- **Scanner-agnostic by design.** Normalize any tool into the canonical model, then triage that. The verdict logic must never depend on npm's or pip's or Trivy's field names. New tool? Add an adapter, not a new triage path.
|
|
98
|
+
- **CVSS ranks the world; we triage our deployment.** A "critical" in a dev-only, unreachable, or base-OS-cruft component is noise; a "medium" KEV hit on a public endpoint is a hotfix.
|
|
99
|
+
- **Reachability is type-aware.** App import ≠ OS package ≠ build-time tool ≠ IaC misconfig. Judge each on its own terms; a flagged base-image lib the app never calls is not a prod emergency.
|
|
100
|
+
- **Check the precondition.** Conditional CVEs are common — read the actual setting. Met → escalate; absent → `not_affected`.
|
|
101
|
+
- **`not_affected` requires a justification code.** The code is what makes the dismissal defensible to an auditor.
|
|
102
|
+
- **Controls must be recorded to count.** Flag undocumented ones, don't assume them.
|
|
103
|
+
- **Fail safe, don't fearmonger.** Escalate on KEV + public + unknown reachability; don't turn a low-EPSS, non-KEV, internal-only finding into a fire drill.
|
|
104
|
+
- **The Contrarian is the noise filter, not a silencer.**
|
|
105
|
+
- **`accept` carries an expiry.** Re-triaged when the fix ships; never silently permanent.
|
|
106
|
+
- **This skill does not fix or file.** It classifies. Code changes, ticket filing, image rebuilds are downstream (`grimoire-vuln-remediate`, `grimoire-bug`/`grimoire-draft` for non-trivial fixes).
|
|
107
|
+
|
|
108
|
+
## Done
|
|
109
|
+
When `.grimoire/security/vulns/<run-date>/triage.md` exists with every finding normalized, reconciled, and assigned a VEX verdict (and for `affected`, an urgency), the Contrarian pass applied to escalations, and the headline reported, triage is complete. Hand off to `grimoire-vuln-remediate` to file the dev work.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Adversarial Personas Reference
|
|
2
|
+
|
|
3
|
+
Loaded by `grimoire-review`, `grimoire-pr-review`, and `grimoire-precommit-review` when the change touches a user-facing surface. Defines the surface-conditional adversarial personas that complement the engineering / product / security personas in `./review-personas.md`.
|
|
4
|
+
|
|
5
|
+
Each persona inhabits a user the design might fail. Their job is to find the failure paths a happy-path-focused reviewer would miss: the keyboard-only user who can't tab to the submit button, the screen-reader user lost in an unlabelled icon grid, the user on a 3G connection waiting for a 4 MB hero image.
|
|
6
|
+
|
|
7
|
+
The set engaged on a given review is filtered by `project.surface` (see §Activation Matrix). All personas inherit the project briefing (§1 of `./review-personas.md`), the materiality gate (§2), and the steel-man requirement (§2a).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Persona Catalog
|
|
12
|
+
|
|
13
|
+
Each persona below names: **identity** (whose constraints they hold), **evaluation criteria** (what they look at), **what triggers a finding** (the concrete observation that becomes feedback).
|
|
14
|
+
|
|
15
|
+
### Keyboard-Only User
|
|
16
|
+
|
|
17
|
+
- **Identity**: Power user navigating exclusively via keyboard (Tab, Shift-Tab, arrow keys, Enter, Esc). Mouse unavailable due to RSI, motor impairment, terminal-only environment, or preference for speed.
|
|
18
|
+
- **Evaluation criteria**:
|
|
19
|
+
- Every interactive element reachable via Tab in a sensible order
|
|
20
|
+
- Focus indicator visible on every focusable element
|
|
21
|
+
- No keyboard traps (except modal dialogs, which must escape on Esc)
|
|
22
|
+
- Shortcuts documented and consistent (don't shadow OS / browser defaults)
|
|
23
|
+
- Custom controls (combobox, datepicker, drag-drop) implement full keyboard interaction patterns per WAI-ARIA Authoring Practices
|
|
24
|
+
- **Triggers a finding**:
|
|
25
|
+
- Click-only handler without keyboard equivalent
|
|
26
|
+
- Focus indicator suppressed (`outline: none` without replacement)
|
|
27
|
+
- Tab order jumps visually unrelated regions
|
|
28
|
+
- Modal dialog cannot be dismissed with Esc
|
|
29
|
+
- Custom dropdown reachable but not operable from keyboard
|
|
30
|
+
|
|
31
|
+
### Screen-Reader User
|
|
32
|
+
|
|
33
|
+
- **Identity**: User of VoiceOver (macOS/iOS), NVDA (Windows), JAWS (Windows), or TalkBack (Android). Navigates by reading aloud announcements; relies on semantic markup and ARIA.
|
|
34
|
+
- **Evaluation criteria**:
|
|
35
|
+
- Semantic HTML used in preference to ARIA (`<button>` not `<div role="button">`)
|
|
36
|
+
- Every form input has a programmatically-associated label
|
|
37
|
+
- Icons that convey meaning have accessible names (`aria-label` or visually-hidden text)
|
|
38
|
+
- Dynamic content changes are announced (live regions or focus shifts)
|
|
39
|
+
- Heading hierarchy is meaningful (H1 once, H2 sections, no skipped levels)
|
|
40
|
+
- Image alt text describes meaning, not appearance (decorative images: `alt=""`)
|
|
41
|
+
- **Triggers a finding**:
|
|
42
|
+
- Icon-only button without accessible name
|
|
43
|
+
- Form field with placeholder used as the only label
|
|
44
|
+
- Live error announcement missing (form submit → silent failure)
|
|
45
|
+
- Decorative SVG read aloud as a long filename
|
|
46
|
+
- `<div onclick>` instead of `<button>`
|
|
47
|
+
|
|
48
|
+
### Low-Vision / Color-Blind User
|
|
49
|
+
|
|
50
|
+
- **Identity**: User with low vision (uses browser zoom 200%+, OS magnifier), or color vision deficiency (deuteranopia, protanopia, tritanopia, monochromacy — affects ~8% of men, ~0.5% of women).
|
|
51
|
+
- **Evaluation criteria**:
|
|
52
|
+
- Body text contrast ≥ 4.5:1 (WCAG AA)
|
|
53
|
+
- UI component contrast ≥ 3:1 vs adjacent
|
|
54
|
+
- Information not conveyed by color alone (status badges have icons or text; error fields have text labels not just red borders)
|
|
55
|
+
- Page reflows cleanly at 200% zoom (no horizontal scroll, no overlapping content)
|
|
56
|
+
- Text scales when user adjusts browser font size (no fixed `px` font sizes on body text where `rem` would work)
|
|
57
|
+
- **Triggers a finding**:
|
|
58
|
+
- Gray text on white below 4.5:1 (the perennial offender)
|
|
59
|
+
- Red/green status indicator with no icon or text differentiator
|
|
60
|
+
- Layout breaks at 200% zoom (content cut off, controls overlap)
|
|
61
|
+
- Required-field marker conveyed only by red asterisk color (no actual asterisk character)
|
|
62
|
+
|
|
63
|
+
### Touch-Target User
|
|
64
|
+
|
|
65
|
+
- **Identity**: Mobile user, often one-handed, often with imprecise thumb input (in motion, accessibility-impaired motor control, or just normal use on a small screen).
|
|
66
|
+
- **Evaluation criteria**:
|
|
67
|
+
- Interactive targets ≥ 24×24 CSS pixels (WCAG 2.2 AA minimum); ≥ 44×44 preferred (iOS HIG)
|
|
68
|
+
- 8px+ spacing between adjacent targets
|
|
69
|
+
- Primary actions reachable in the thumb zone (bottom half of phone screen)
|
|
70
|
+
- No hover-only affordances (hover doesn't exist on touch)
|
|
71
|
+
- Long-press, swipe, pinch interactions have a tap-only equivalent
|
|
72
|
+
- **Triggers a finding**:
|
|
73
|
+
- Buttons sized below 24×24 in the actual rendered design
|
|
74
|
+
- Adjacent links in a list with no spacing (mis-tap risk)
|
|
75
|
+
- Primary CTA in the top-right corner (unreachable for one-handed use on large phones)
|
|
76
|
+
- Tooltip is the only source of help text on a touch surface
|
|
77
|
+
|
|
78
|
+
### Responsive-Breakpoint User
|
|
79
|
+
|
|
80
|
+
- **Identity**: User on a viewport the designer didn't focus-test — 320px-wide phones, foldables in their open state, 5K desktops, browser windows the user resized.
|
|
81
|
+
- **Evaluation criteria**:
|
|
82
|
+
- Layout works from 320px viewport width upward (smallest common phone)
|
|
83
|
+
- No horizontal scroll except for content where scrolling is intentional (tables, code blocks)
|
|
84
|
+
- Breakpoints handle landscape phone orientation (short, wide viewports)
|
|
85
|
+
- Text remains readable line length (45-75 characters) across breakpoints — does not stretch to full ultra-wide width
|
|
86
|
+
- Touch targets remain ≥ 24×24 at the smallest breakpoint
|
|
87
|
+
- **Triggers a finding**:
|
|
88
|
+
- Card layout breaks below 360px (content overflows or overlaps)
|
|
89
|
+
- Sidebar that collapses to a hamburger but the hamburger button is itself 12×12
|
|
90
|
+
- Line length unconstrained on ultra-wide displays (text spans 2000px)
|
|
91
|
+
- Modal dialog wider than the viewport, requiring horizontal scroll inside the modal
|
|
92
|
+
|
|
93
|
+
### RTL / i18n User
|
|
94
|
+
|
|
95
|
+
- **Identity**: User of a right-to-left script (Arabic, Hebrew, Persian, Urdu) or a language with different text expansion (German, Finnish — typically 30% longer; Chinese, Japanese, Korean — often shorter but with line-break rules).
|
|
96
|
+
- **Evaluation criteria**:
|
|
97
|
+
- Layout mirrors correctly under `dir="rtl"` (logical properties used: `margin-inline-start` not `margin-left`)
|
|
98
|
+
- Icons that imply direction (arrows, chevrons) flip; icons that don't (clock, magnifying glass) don't
|
|
99
|
+
- Strings interpolated with placeholders use named/positional substitution, not concatenation
|
|
100
|
+
- Hardcoded strings flagged; user-visible text comes from a translation catalog
|
|
101
|
+
- Date, number, currency formats use the user's locale
|
|
102
|
+
- **Triggers a finding**:
|
|
103
|
+
- User-visible string hardcoded in the markup (not extracted for translation)
|
|
104
|
+
- Layout breaks visually under `dir="rtl"` (margins on the wrong side, icons not mirrored)
|
|
105
|
+
- Date displayed as `MM/DD/YYYY` without locale awareness (ambiguous globally)
|
|
106
|
+
- String concatenation that won't translate cleanly: `"You have " + count + " items"` → use a translation function with placeholders
|
|
107
|
+
|
|
108
|
+
### Low-Bandwidth / Offline User
|
|
109
|
+
|
|
110
|
+
- **Identity**: User on slow or intermittent network (3G, congested wifi, satellite, train tunnel). Frequent in markets the project may not actively target but where users still exist.
|
|
111
|
+
- **Evaluation criteria**:
|
|
112
|
+
- Total payload weight for first meaningful render reasonable for the surface (web < 200 KB JS gzipped is a starting bar; mobile native should function offline-first for read paths)
|
|
113
|
+
- Images sized to actual display dimensions; modern formats (AVIF, WebP) with fallbacks
|
|
114
|
+
- Network failures fail gracefully (cached state, queued writes, retry with backoff)
|
|
115
|
+
- Loading states address slow connections, not just fast ones (skeleton at 100ms, escalated message at 5s)
|
|
116
|
+
- **Triggers a finding**:
|
|
117
|
+
- Hero image > 1 MB without responsive `srcset`
|
|
118
|
+
- Form submit hangs indefinitely when network is dropped (no timeout, no queue)
|
|
119
|
+
- Critical content loaded via blocking JS bundle > 500 KB
|
|
120
|
+
- No offline state — app crashes or shows blank screen when network is lost
|
|
121
|
+
|
|
122
|
+
### Hostile Actor
|
|
123
|
+
|
|
124
|
+
- **Identity**: User actively trying to misuse the design — abuse a flow, exfiltrate data, deceive other users via the system, or weaponize the UI as part of a social-engineering attack. Adversarial UX, not just adversarial security.
|
|
125
|
+
- **Evaluation criteria**:
|
|
126
|
+
- User-generated content is escaped and rendered safely (no XSS, no markdown that smuggles HTML)
|
|
127
|
+
- Rate limits visible to legitimate users (so they understand why an action was blocked) but unbypassable
|
|
128
|
+
- Sharing / impersonation features have abuse mitigation (report button, mute, block)
|
|
129
|
+
- Display-name fields don't accept Unicode lookalikes that impersonate other users (homograph attack)
|
|
130
|
+
- Email / SMS templates can't be hijacked for phishing-by-proxy (user-supplied URL appearing in an authority-branded message)
|
|
131
|
+
- Sensitive actions require recent re-authentication, not just an open session
|
|
132
|
+
- **Triggers a finding**:
|
|
133
|
+
- Comment field renders user-supplied HTML
|
|
134
|
+
- Search field reflects input back into the page without escaping
|
|
135
|
+
- Profile URL accepts `javascript:` scheme
|
|
136
|
+
- "Invite via email" feature sends arbitrary user-supplied text from the project's domain
|
|
137
|
+
|
|
138
|
+
### API Conventions Reviewer
|
|
139
|
+
|
|
140
|
+
- **Identity**: API consumer (internal team, partner, third-party developer). Evaluates the *design* of public APIs the same way users evaluate UI — for consistency, predictability, and minimum surprise.
|
|
141
|
+
- **Evaluation criteria**:
|
|
142
|
+
- REST: resource-oriented URLs, correct HTTP verbs and status codes, consistent error envelope, predictable pagination
|
|
143
|
+
- GraphQL: schema follows naming conventions (camelCase fields, PascalCase types), no over-fetching defaults, deprecation flow uses `@deprecated` not removal
|
|
144
|
+
- Idempotency: PUT and DELETE are idempotent; POST that should be idempotent uses an `Idempotency-Key` header
|
|
145
|
+
- Versioning: breaking change has a versioning plan (URL path, header, or media type — but consistent across the API)
|
|
146
|
+
- Discoverability: spec is published (OpenAPI, GraphQL introspection); errors point to docs
|
|
147
|
+
- **Triggers a finding**:
|
|
148
|
+
- New `POST /deleteUser` endpoint (verb in URL — should be `DELETE /users/:id`)
|
|
149
|
+
- Inconsistent error envelopes across endpoints (some `{ error: "..." }`, some `{ message: "..." }`)
|
|
150
|
+
- Breaking change to a documented field with no version bump or deprecation notice
|
|
151
|
+
- 200 OK returned for an operation that failed (status code masks the error)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Activation Matrix
|
|
156
|
+
|
|
157
|
+
Engagement of each persona is determined by `project.surface`. The matrix below comes from ADR-0019.
|
|
158
|
+
|
|
159
|
+
| Persona | TUI | Web | Mobile | API | Mixed |
|
|
160
|
+
|---|---|---|---|---|---|
|
|
161
|
+
| Keyboard-only | required | required | n/a | n/a | required |
|
|
162
|
+
| Screen-reader | n/a | required | required | n/a | required |
|
|
163
|
+
| Low-vision / color-blind | n/a | required | required | n/a | required |
|
|
164
|
+
| Touch-target | n/a | n/a | required | n/a | required |
|
|
165
|
+
| Responsive-breakpoint | n/a | required | n/a | n/a | required |
|
|
166
|
+
| RTL / i18n | conditional | conditional | conditional | n/a | conditional |
|
|
167
|
+
| Low-bandwidth / offline | n/a | required | required | conditional | required |
|
|
168
|
+
| Hostile actor | required | required | required | required | required |
|
|
169
|
+
| API conventions | n/a | n/a | n/a | required | required |
|
|
170
|
+
|
|
171
|
+
**Legend**:
|
|
172
|
+
- **required** — persona engages by default on this surface
|
|
173
|
+
- **conditional** — persona engages only when a project briefing axis flags it (e.g., `i18n` tag has count > 0 in the feature inventory; project compliance includes a region requiring RTL)
|
|
174
|
+
- **n/a** — persona does not engage by default; user can still invoke via `--personas=...`
|
|
175
|
+
|
|
176
|
+
**User override always available** via `--personas=keyboard,low-vision` or "skip <persona>". Surface is the default; the user is the override.
|
|
177
|
+
|
|
178
|
+
**Mixed surface** runs the union of all surface-specific personas. Projects that genuinely span TUI + web + mobile should set `surface: mixed`; that is the cost of breadth.
|
|
179
|
+
|
|
180
|
+
**Unknown surface** (config field absent) falls back to `mixed`. Better to over-engage than to silently skip critical personas.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Materiality Gate Cross-Reference
|
|
185
|
+
|
|
186
|
+
Every adversarial finding must cite an axis from the Project Briefing (§1 of `./review-personas.md`). The adversarial set extends the briefing axes the original engine considered:
|
|
187
|
+
|
|
188
|
+
- **Surface axis** (this engine adds it) — TUI vs web vs mobile vs API determines which personas engage at all
|
|
189
|
+
- **Brand axis** — if `.grimoire/brand/tokens.json` defines a contrast ratio target, the low-vision persona uses it; otherwise WCAG AA default
|
|
190
|
+
- **Compliance axis** — `project.compliance` mentions WCAG, ADA, EAA (European Accessibility Act), Section 508 → screen-reader and low-vision findings escalate from suggestion to blocker by default
|
|
191
|
+
- **Threat-surface tags** (existing) — `i18n=N` count > 0 promotes RTL persona from conditional to required; `mobile=N` count > 0 promotes touch-target persona
|
|
192
|
+
|
|
193
|
+
A finding from any persona below that lacks a briefing anchor is dropped. The full materiality rules from `./review-personas.md` §2 apply unchanged.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Steel-Man Requirement
|
|
198
|
+
|
|
199
|
+
Per `./review-personas.md` §2a, every adversarial finding must include a steel-man before submission:
|
|
200
|
+
|
|
201
|
+
- **Steel-man**: "The designer likely chose this because <strongest plausible reason — convention, performance, scope, stage>."
|
|
202
|
+
- **Why it still fails**: "Despite that, <concrete harm path tied to a briefing axis — named user impacted, named state, named consequence>."
|
|
203
|
+
|
|
204
|
+
If either line cannot be completed with substance, the finding is dropped. "An accessibility issue" is not a finding; "screen-reader user submitting the login form receives no audible feedback on validation error — they cannot recover without sighted assistance" is a finding.
|
|
205
|
+
|
|
206
|
+
Vague harm paths fail this gate. The adversarial personas exist precisely to name specific harms; generic ones are noise.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Severity Calibration
|
|
211
|
+
|
|
212
|
+
The default for an adversarial finding is **suggestion**. A finding is a **blocker** when *all three* hold (mirroring `./review-personas.md` §2b):
|
|
213
|
+
|
|
214
|
+
1. **Concrete harm path** — named user (the persona), named trigger, named consequence
|
|
215
|
+
2. **Briefing-anchored** — the consequence threatens a briefing axis (surface, compliance, brand contrast target, threat-surface tag)
|
|
216
|
+
3. **Not already mitigated** — neighbor code, design-system component, or sibling feature does not already handle it
|
|
217
|
+
|
|
218
|
+
Adversarial-specific severity rules:
|
|
219
|
+
|
|
220
|
+
- WCAG AA violations on a project that lists WCAG / ADA / EAA in `project.compliance` → blocker by default (regulator anchor)
|
|
221
|
+
- Deceptive patterns (see `./design-heuristics.md` §3) → blocker by default on customer-facing stage; suggestion on internal-tools stage
|
|
222
|
+
- Hostile-actor findings with a working harm path → blocker, regardless of stage (security overlap)
|
|
223
|
+
- Touch-target findings on a mobile surface where the project ships → blocker if a user couldn't complete a primary task; suggestion if it's a secondary control
|
|
224
|
+
|
|
225
|
+
Zero findings is a valid outcome. A persona that returns "no material findings under the briefing" is doing its job. The Contrarian pass (§4.8 of `./review-personas.md`) calibrates inflated findings post-hoc.
|