@kiwidata/grimoire 0.1.4 → 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/README.md +79 -58
- package/dist/cli/index.js +5 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/core/check.d.ts.map +1 -1
- package/dist/core/check.js +54 -11
- package/dist/core/check.js.map +1 -1
- package/dist/core/doc-style.d.ts.map +1 -1
- package/dist/core/doc-style.js +76 -0
- package/dist/core/doc-style.js.map +1 -1
- package/dist/core/docs.js +96 -70
- 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 +78 -19
- package/dist/core/health.js.map +1 -1
- package/dist/core/hooks.js +11 -5
- package/dist/core/hooks.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.map +1 -1
- package/dist/core/shared-setup.js +5 -4
- package/dist/core/shared-setup.js.map +1 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/trace.js +37 -35
- package/dist/core/trace.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/package.json +1 -1
- package/skills/grimoire-apply/SKILL.md +35 -39
- package/skills/grimoire-commit/SKILL.md +1 -1
- package/skills/grimoire-design/SKILL.md +3 -3
- package/skills/grimoire-discover/SKILL.md +77 -110
- package/skills/grimoire-draft/SKILL.md +51 -18
- package/skills/grimoire-plan/SKILL.md +62 -32
- package/skills/grimoire-pr/SKILL.md +7 -8
- package/skills/grimoire-pr-review/SKILL.md +1 -1
- package/skills/grimoire-refactor/SKILL.md +2 -2
- package/skills/grimoire-review/SKILL.md +12 -0
- package/skills/grimoire-verify/SKILL.md +7 -7
- package/skills/grimoire-vuln-remediate/SKILL.md +107 -0
- package/skills/grimoire-vuln-triage/SKILL.md +109 -0
- package/skills/references/code-quality.md +41 -9
- package/skills/references/container-scan-triage.md +102 -0
- package/skills/references/dependency-vuln-triage.md +236 -0
- package/skills/references/principles.md +82 -0
- package/skills/references/refactor-scan-categories.md +2 -2
- package/skills/references/review-personas.md +4 -3
- package/skills/references/testing-contracts.md +1 -1
- package/templates/accepted-risks.yml +47 -0
- package/templates/constraints.md +25 -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 -16
- 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 -81
- 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 -140
- package/dist/core/log.js.map +0 -1
- package/dist/core/map.d.ts +0 -22
- package/dist/core/map.d.ts.map +0 -1
- package/dist/core/map.js +0 -365
- package/dist/core/map.js.map +0 -1
- package/templates/dupignore +0 -93
- package/templates/mapignore +0 -58
- package/templates/mapkeys +0 -65
|
@@ -14,7 +14,7 @@ For each production file you wrote or edited, walk the seven checks below. Any f
|
|
|
14
14
|
|
|
15
15
|
### 1. Reuse before write
|
|
16
16
|
|
|
17
|
-
Before adding a function, helper, type, or constant:
|
|
17
|
+
Before adding a function, helper, type, or constant: query the graph (`search_graph` by concept and by name) for an existing one. Then grep, and check neighbors in the same directory.
|
|
18
18
|
|
|
19
19
|
- If a function with the same job already exists → call it. Don't re-implement.
|
|
20
20
|
- If something *almost* fits → use it directly first, refactor it once a second caller actually needs the change. Don't generalize on speculation.
|
|
@@ -83,21 +83,53 @@ Keep:
|
|
|
83
83
|
|
|
84
84
|
Fail: a new `BaseFoo` / `FooStrategy` / `FooFactory` introduced for a single caller.
|
|
85
85
|
|
|
86
|
-
### 7. Comments earn their place
|
|
86
|
+
### 7. Comments earn their place — terse, self-contained, no essays
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
Write comments like a senior engineer with no time: dense, professional, zero filler.
|
|
89
|
+
|
|
90
|
+
**Voice: terse.** "Resolve model by id; raises on unknown provider." — not "This function is responsible for resolving the model by its id, and it will raise an exception if the provider is not known." Drop "this function", "we", hedging, and restated types. Fragments are fine; full prose grammar is not required.
|
|
91
|
+
|
|
92
|
+
**Self-contained.** A comment describes the function/class on its own terms only. It must NOT name an external artifact that changes independently — feature flags / `.feature` files / scenario names, unit or integration test names, MADR/ADR numbers, change-ids, issue/PR numbers, tag codes (`LOG-OBS-003`). Those orphan the moment the artifact moves, and rot silently. Describe the *behavior*, not where it's specced.
|
|
93
|
+
- OK: `# skip third-party sinks (e.g. behave capture)` — generic, about the code.
|
|
94
|
+
- Not OK: `# implements scenario LOG-OBS-003 from logging.feature` — points at an artifact that will move.
|
|
95
|
+
|
|
96
|
+
**No paragraphs.** Summary is one line, two at most. No prose block explaining the whole design before the params. If the rationale needs a paragraph, it belongs in a decision record — not the code.
|
|
97
|
+
|
|
98
|
+
**Params per `comment_style` are fine.** If the project's style (sphinx/google/jsdoc/…) calls for `:param`/`Args:`/`@param`, keep them — but describe a param only when its name + type don't already say it, and don't precede them with prose.
|
|
89
99
|
|
|
90
100
|
Drop:
|
|
91
101
|
- Comments that restate the code (`# loop over users`).
|
|
92
|
-
-
|
|
93
|
-
- Multi-line docstrings on private functions whose name
|
|
102
|
+
- Any reference to a task / PR / ticket / feature / scenario / ADR / specific test (`# added for issue #123`, `# covers scenario X`, `# see test_foo`). Self-contained or gone.
|
|
103
|
+
- Multi-line prose docstrings on private functions whose name + signature already say everything.
|
|
94
104
|
- Commented-out code. Delete it; git remembers.
|
|
95
105
|
|
|
96
106
|
Keep:
|
|
97
|
-
- One
|
|
98
|
-
-
|
|
107
|
+
- One terse line of *why* when non-obvious — a hidden constraint, a workaround, a surprising invariant — stated in terms of the code itself.
|
|
108
|
+
- The structured `comment_style` param/return section, terse.
|
|
109
|
+
|
|
110
|
+
Fail: any comment that (a) wouldn't confuse a future reader if removed, (b) names an external artifact, or (c) runs to a prose paragraph.
|
|
111
|
+
|
|
112
|
+
**Before / after** (the offender this rule targets):
|
|
113
|
+
```python
|
|
114
|
+
# BEFORE — orphan-prone essay
|
|
115
|
+
def build_chat(model_id):
|
|
116
|
+
"""
|
|
117
|
+
Build and return a chat model for the given model id. This is the primary
|
|
118
|
+
entry point used by every agent and team in the system, as specified by
|
|
119
|
+
scenario LOG-OBS-003 in logging.feature and decided in ADR-0001. See
|
|
120
|
+
test_build_chat for the expected behavior. Added as part of add-2fa-login.
|
|
121
|
+
|
|
122
|
+
:param model_id: the id of the model to build
|
|
123
|
+
:return: the chat model
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# AFTER — terse, self-contained
|
|
127
|
+
def build_chat(model_id):
|
|
128
|
+
"""Resolve a chat model by id. Raises on an unknown provider.
|
|
99
129
|
|
|
100
|
-
|
|
130
|
+
:param model_id: provider-prefixed model id (e.g. "gpt-4.1-mini")
|
|
131
|
+
"""
|
|
132
|
+
```
|
|
101
133
|
|
|
102
134
|
---
|
|
103
135
|
|
|
@@ -110,7 +142,7 @@ Before marking a task `[x]`:
|
|
|
110
142
|
- [ ] No guards / try-except / type-checks inside the trust boundary (§4)
|
|
111
143
|
- [ ] No locals named `data`, `result`, `temp`, `info`, `obj` — names reveal intent (§5)
|
|
112
144
|
- [ ] No new abstractions, interfaces, or wrappers with a single caller (§6)
|
|
113
|
-
- [ ]
|
|
145
|
+
- [ ] Comments are terse, self-contained, ≤2 lines of prose — no *what*, no external-artifact refs (feature/scenario/ADR/test/ticket) (§7)
|
|
114
146
|
- [ ] Diff stays inside the task's scope — no "while I'm here" refactors
|
|
115
147
|
|
|
116
148
|
If any box can't be ticked, fix the code (not the checklist) and re-run tests.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Container & OS-Package Scan Triage Reference
|
|
2
|
+
|
|
3
|
+
The deep-dive for `os-package` / `container` / `iac` findings from image scanners (Trivy, Grype). Loaded by `grimoire-vuln-triage` when a scan carries container/OS-package results. The general rubric — normalize, reconcile, KEV/EPSS, VEX, urgency, Contrarian — lives in `./dependency-vuln-triage.md`; this file is the discipline for the part that goes wrong most: deciding what to *do* about a base-OS CVE.
|
|
4
|
+
|
|
5
|
+
Written after a review recommended removing `Mesa`, `ncurses`, and `krb5` from a headless Django API with the rationale "no business in a headless API." Two of the three could not be removed without breaking the image. This guide exists so that mistake is not repeated.
|
|
6
|
+
|
|
7
|
+
## Separate the two axes — they are not the same question
|
|
8
|
+
|
|
9
|
+
- **Reachability** — *is the vulnerable code path actually called by untrusted input in this service?* Decides **urgency** (hotfix / next-release / accept). A library present but never invoked is low risk even at CVSS CRITICAL.
|
|
10
|
+
- **Removability** — *can we delete the package, and what breaks if we do?* Decides **remediation** (Dockerfile edit / base bump / accept+document).
|
|
11
|
+
|
|
12
|
+
Conflating them produces the classic error: "this lib is unreachable, so remove it." Unreachable ≠ removable. A headless API genuinely cannot reach Mesa's OpenGL code — **and also cannot remove Mesa** if it arrived transitively behind a package it needs. Judge both, separately.
|
|
13
|
+
|
|
14
|
+
## Core rule: trace before you recommend
|
|
15
|
+
|
|
16
|
+
A CVE scanner reports *what is present*, not *why* or *whether it is removable*. Never recommend removing a package until you have answered all three:
|
|
17
|
+
|
|
18
|
+
1. **How does it get in?** Directly installed, transitive, base image, or builder-stage-only?
|
|
19
|
+
2. **What depends on it?** Application code, a required runtime lib, or the base OS?
|
|
20
|
+
3. **What breaks if it's gone?** Build, runtime, or nothing — and what's the test that proves it?
|
|
21
|
+
|
|
22
|
+
"This doesn't belong in a headless API" is an assumption, not an analysis.
|
|
23
|
+
|
|
24
|
+
## Step A — How is it installed?
|
|
25
|
+
|
|
26
|
+
Search the Dockerfile for an explicit install line first.
|
|
27
|
+
|
|
28
|
+
- **Explicitly installed** (named in `apt-get install` / `pip install`): a real removal candidate — continue to Step B.
|
|
29
|
+
- **Not named anywhere**: it's transitive — pulled by another package or shipped in the base image. You cannot `apt-get remove` it without breaking its parent. Identify the parent before saying anything.
|
|
30
|
+
|
|
31
|
+
Common transitive sources — map these before flagging:
|
|
32
|
+
|
|
33
|
+
| Flagged lib | Usually pulled in by | Notes |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| krb5 / libgssapi-krb5 | `libpq5`, `postgresql-client`, `curl` | Postgres GSSAPI/Kerberos auth |
|
|
36
|
+
| ncurses / libtinfo | base image | bash, apt, dpkg, python readline link it |
|
|
37
|
+
| Mesa / libgl1 / libgbm | `libgl1`, `libglib2.0-0` | OpenCV / docling / easyocr deps |
|
|
38
|
+
| OpenSSL / libssl | base image + most TLS clients | almost never removable |
|
|
39
|
+
| libexpat1 | base image + python (`pyexpat`) | stdlib XML |
|
|
40
|
+
|
|
41
|
+
## Step B — What actually depends on it?
|
|
42
|
+
|
|
43
|
+
Do not assume. Check the repo:
|
|
44
|
+
|
|
45
|
+
- **App imports** — grep for the consuming module (`import cv2`, `import magic`, `import pyodbc`, `gssapi`, `lxml`).
|
|
46
|
+
- **System tools used at runtime** — grep code, scripts, and the entrypoint for the binary (`psql`, `pg_dump`, `pg_isready`).
|
|
47
|
+
- **Driver bundling** — many Python wheels bundle their native lib, making the system package redundant. `psycopg-binary` bundles libpq → system `libpq5` not needed for the driver; `pylibmagic` bundles libmagic → system `libmagic1` may be redundant. When a binary wheel is present, the matching system runtime package is often dead weight — **verify, then say so**.
|
|
48
|
+
- **Cross-service config trap** — env vars or paths in this repo may configure a *different* container. `EASYOCR_MODULE_PATH` / `DOCLING_ARTIFACTS_PATH` in bake are passed by `job_runner.py` to the **ricky** pipeline container — they do **not** mean bake runs easyocr/docling. Never justify or condemn a package using a string that belongs to another service.
|
|
49
|
+
|
|
50
|
+
## Step C — Know what is not removable
|
|
51
|
+
|
|
52
|
+
Some findings are not actionable by editing an install line:
|
|
53
|
+
|
|
54
|
+
- **Base-image packages** (ncurses, OpenSSL, glibc, zlib, expat): part of the OS. Removing breaks bash/apt/dpkg or the Python runtime. The only real mitigations are: **switch to a smaller/distroless base**, **bump the base image** for patched versions, or **accept and document** the risk. State which — do not tell the user to "remove" it.
|
|
55
|
+
- **Transitive deps of required libs** (e.g. krb5 behind `libpq5`): removable only by also removing the parent, and only if the parent is itself unneeded.
|
|
56
|
+
|
|
57
|
+
## Step D — Multi-stage builds: target the right stage
|
|
58
|
+
|
|
59
|
+
In a multi-stage Dockerfile only the final stage ships. Packages in a `builder` stage (compilers, `-dev` headers) do **not** appear in the runtime image if only the artifact (`/opt/venv` or equivalent) is copied forward. They are not runtime attack surface. Don't flag builder-stage packages as runtime risk; if you mention them, label them **build-only**.
|
|
60
|
+
|
|
61
|
+
## Step E — Assess real risk, not just presence (reachability)
|
|
62
|
+
|
|
63
|
+
Maps to `./dependency-vuln-triage.md` § Reachability. A CVE in a library never reached by untrusted input is lower priority than its score. For each finding note:
|
|
64
|
+
|
|
65
|
+
- Is the vulnerable code path reachable in *this* service? (use the consumer map below — grep the **consumer**, not the C package name; the app never `import`s `libexpat1`)
|
|
66
|
+
- Network/user-input exposed, or internal-only?
|
|
67
|
+
- Headless API context: no display, no user shell, no interactive TTY → GUI/terminal libs (Mesa, ncurses) are usually unreachable even when present.
|
|
68
|
+
|
|
69
|
+
| OS package | Reached only if the app… | Grep for |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| libexpat1 | parses XML via stdlib | `xml.etree`, `xml.sax`, `pyexpat`, `minidom` |
|
|
72
|
+
| libxml2 / libxslt | parses XML/XSLT via lxml | `import lxml`, `etree`, `XSLT` |
|
|
73
|
+
| krb5 / libgssapi | does Kerberos/GSSAPI auth | `gssapi`, `kerberos`, `requests_kerberos` |
|
|
74
|
+
| mesa / libGL / libgbm | does GPU/OpenGL rendering | `OpenGL`, `moderngl`, `cv2` (headless API: none) |
|
|
75
|
+
| ncurses / libtinfo | drives an interactive terminal | `curses`, `pty`, `readline` (web process: none) |
|
|
76
|
+
| libssl / openssl | does TLS | usually reachable — judge on impact |
|
|
77
|
+
| imagemagick / libvips | processes user-uploaded images | the upload/convert path |
|
|
78
|
+
|
|
79
|
+
**Grep can lie — verify the binding.** `Price.fromstring()` (price-parser) is not `etree.fromstring()` (XML). Confirm the match is the real vulnerable call site before asserting `not_affected` or `affected`; if you can't, mark `under_investigation` and name what a human must check. Prefer reachability-based prioritization over raw CVSS.
|
|
80
|
+
|
|
81
|
+
## Honor the scanner's fix-state
|
|
82
|
+
|
|
83
|
+
Trivy `Status` (and Grype `fix.state`): `fixed` → a patched package exists; upgrade/rebuild is the lever. `affected` / `will_not_fix` / `end_of_life` → **no fix available**; the lever is *accept with expiry* or *rebuild on a newer/slimmer base when one ships* — **not** an "upgrade X" ticket. `under_investigation` → distro hasn't ruled; mirror it. Never file an upgrade task for a no-fixed-version finding.
|
|
84
|
+
|
|
85
|
+
## Output per flagged package
|
|
86
|
+
|
|
87
|
+
1. **Package + CVE(s)** — what the scanner said (and the dedup count if one CVE spans many packages).
|
|
88
|
+
2. **How it's installed** — explicit line N / transitive via `<parent>` / base image / builder-only.
|
|
89
|
+
3. **What depends on it** — app module / runtime tool / OS, with grep evidence.
|
|
90
|
+
4. **Reachable?** — vulnerable path called by untrusted input? (provenance: graph / grep / image-layer / unknown)
|
|
91
|
+
5. **Removable?** — Yes (safe) / Yes (after removing `<parent>`, test X) / No (base OS) / No (required by Y).
|
|
92
|
+
6. **Recommendation** — exact Dockerfile edit, or "patch/bump base image", or "accept + document", **plus the post-change test** (build, import, DB connect). Route image-structure changes to infra / `grimoire-draft`, not app remediation.
|
|
93
|
+
|
|
94
|
+
## Anti-patterns — do not do these
|
|
95
|
+
|
|
96
|
+
- ❌ "X has no business in a headless API" with no trace of how X got installed.
|
|
97
|
+
- ❌ Recommending `apt-get remove` of a base-image or transitive package.
|
|
98
|
+
- ❌ Treating scanner presence as equal to exploitable risk.
|
|
99
|
+
- ❌ Justifying or condemning a package using config that targets another service.
|
|
100
|
+
- ❌ Flagging builder-stage packages as runtime attack surface.
|
|
101
|
+
- ❌ Recommending removal without naming the post-change test.
|
|
102
|
+
- ❌ Filing an "upgrade" ticket for a `will_not_fix` / no-fixed-version finding.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Vulnerability Triage Reference
|
|
2
|
+
|
|
3
|
+
Loaded by `grimoire-vuln-triage` (and later `grimoire-vuln-remediate`). Turns **any** vulnerability scan — `npm audit`, `pip-audit`, `osv-scanner`, Trivy, Grype, Snyk, Dependabot, a SARIF file, or a CSV/markdown report a teammate forwards — into per-advisory verdicts whose single most important output is one decision:
|
|
4
|
+
|
|
5
|
+
> **Drop everything and hotfix now, or let it ride the normal testing / release cycle?**
|
|
6
|
+
|
|
7
|
+
Everything below exists to answer that, honestly, for *our* deployment — not in the abstract. The skill is **scanner-agnostic**: it normalizes whatever it's handed into one canonical model, then triages that. Covers application dependencies (npm/PyPI/Go/Cargo/…), OS packages (Debian/Alpine/RPM from container scans), and container/IaC findings alike.
|
|
8
|
+
|
|
9
|
+
## Why raw scanner severity is not the answer
|
|
10
|
+
|
|
11
|
+
Scanners rank by **CVSS base score**, which describes the vulnerability in a vacuum. It knows nothing about whether our code reaches the vulnerable function, whether the package even runs in production, whether the service is internet-facing, what controls sit in front of it, or **whether we already upgraded past it**. CVSS alone over-escalates: most "high"/"critical" findings are not actionable in a given deployment. Commercial reachability tooling suppresses 70–90% of findings for exactly this reason. We get most of that signal for free from reconciliation + KEV + EPSS + reachability + our own context.
|
|
12
|
+
|
|
13
|
+
The triage rubric is **Threat × Exposure × Impact**:
|
|
14
|
+
- **Threat** — is it actually being exploited / likely to be? (KEV, EPSS)
|
|
15
|
+
- **Exposure** — can an attacker reach the vulnerable code in our deployment? (reachability + network exposure)
|
|
16
|
+
- **Impact** — what is the blast radius if they do? (data sensitivity, privilege)
|
|
17
|
+
|
|
18
|
+
## Step 1 — Normalize the scan into the canonical advisory model
|
|
19
|
+
|
|
20
|
+
**Do this before anything else, regardless of source.** Different scanners emit wildly different shapes; triage logic must never be coupled to one format. Map each finding to:
|
|
21
|
+
|
|
22
|
+
| Field | Meaning |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `id` | Primary advisory id (CVE, GHSA, OSV, vendor id) |
|
|
25
|
+
| `aliases` | All other ids (so KEV/EPSS lookups can find the CVE) |
|
|
26
|
+
| `cve` | The CVE alias if any (KEV/EPSS key); may be absent |
|
|
27
|
+
| `component` | Package / module / OS-package / image name |
|
|
28
|
+
| `component_type` | `library` \| `os-package` \| `container` \| `iac` \| `runtime` — drives how reachability is judged |
|
|
29
|
+
| `installed_version` | What the scan saw |
|
|
30
|
+
| `fixed_version` | First fixed version, or `none` |
|
|
31
|
+
| `severity` / `cvss` | Scanner-reported, treated as a prior only |
|
|
32
|
+
| `target` | Where it was found (lockfile, image layer, Dockerfile, repo) |
|
|
33
|
+
| `scanner` | Which tool produced it |
|
|
34
|
+
| `advisory_url` / `description` | For reading what the bug actually is |
|
|
35
|
+
|
|
36
|
+
### Format adapters
|
|
37
|
+
|
|
38
|
+
Read the right fields per scanner — **do not** assume one tool's field names apply to another:
|
|
39
|
+
|
|
40
|
+
- **npm audit** (`--json`): `vulnerabilities{}` keyed by package → `severity`, `via[]` (string or advisory object with `title`/`url`), `isDirect`, `fixAvailable`, `nodes[]`. Dev deps appear via `effects`/dependency graph (no single `dev` flag on every entry).
|
|
41
|
+
- **pip-audit** (`-f json`): `dependencies[]` → `{name, version, vulns[]}`; each vuln has `id`, `aliases[]` (find the `CVE-` one), `fix_versions[]`, `description`. **No dev flag in the data** — infer dev/runtime from lockfile groups (`[tool.uv] dev-dependencies`, poetry `group.dev`, `requirements-dev.txt`).
|
|
42
|
+
- **osv-scanner** (`--format json`): `results[].packages[].vulnerabilities[]` with OSV ids + `aliases`; `results[].source.path` is the manifest.
|
|
43
|
+
- **Trivy** (`--format json`): `Results[]` each with a `Class` (`os-pkgs` \| `lang-pkgs` \| `config`) and `Type` (debian, alpine, gobinary, python-pkg, …); `Results[].Vulnerabilities[]` → `VulnerabilityID`, `PkgName`, `InstalledVersion`, `FixedVersion`, `Severity`, `CVSS{}`. **`Class: os-pkgs` → `component_type: os-package`** (base-image OS cruft — judged differently from app deps). `Results[].Class: config` → `component_type: iac` (Dockerfile/k8s misconfig, not a CVE — triage on exposure, no KEV/EPSS).
|
|
44
|
+
- **Grype** (`-o json`): `matches[].vulnerability` (`id`, `severity`, `fix.versions[]`) + `matches[].artifact` (`name`, `version`, `type`).
|
|
45
|
+
- **Snyk / Dependabot / SARIF**: pull `ruleId`/`cve`, the package coordinate, and fixed version from `results[]` / alerts / `runs[].results[]`. SARIF `level` maps to severity.
|
|
46
|
+
- **Unknown / freeform (CSV, markdown, pasted text):** extract the minimum — `id` (CVE/GHSA), `component`, `installed_version`, `fixed_version` if stated. Anything you can't fill is `unknown`, not a guess. Triage proceeds on what you have; record `scanner: <described>` and note reduced confidence.
|
|
47
|
+
|
|
48
|
+
If you genuinely can't parse a format, say so and ask for `--json`/SARIF rather than guessing at a table.
|
|
49
|
+
|
|
50
|
+
### Deduplicate before triaging
|
|
51
|
+
|
|
52
|
+
Scanners — Trivy especially — emit the **same CVE once per affected package** (e.g. `CVE-2026-40393` listed against `libgbm1`, `libgl1-mesa-dri`, `libglx-mesa0`, `mesa-libgallium` = 4 findings, 1 vulnerability). Collapse to **unique `(id, component_type)`**, keeping the list of affected packages on the single entry. Triage and count the deduplicated set — a "200 CVE" image scan is often 30 real advisories. Report both numbers (raw findings → unique advisories) so the noise reduction is visible.
|
|
53
|
+
|
|
54
|
+
### Container scans also carry non-CVE results — don't drop them
|
|
55
|
+
|
|
56
|
+
Trivy/Grype image scans include result classes that are **not** package CVEs and need routing, not triage:
|
|
57
|
+
- **secret** (`Class: secret`) — a credential/key found in an image layer (e.g. an `.env` file baked in). Even zero-hit secret *targets* are a smell (why is an env file in the image?). Route to infra/`grimoire-draft`, treat any real hit as a confidential security issue.
|
|
58
|
+
- **config / misconfig** (`Class: config`, `component_type: iac`) — Dockerfile/k8s findings (root user, no resource limits, exposed port, `:latest` base). Triage on exposure; route persistent ones to infra/`grimoire-draft`. Not an app hotfix.
|
|
59
|
+
|
|
60
|
+
## Step 2 — Reconcile against the current tree FIRST *(mandatory, highest-leverage)*
|
|
61
|
+
|
|
62
|
+
**Scan artifacts go stale.** A report from last week was taken against versions you may have already upgraded. Before spending any effort on enrichment or reachability, compare each advisory's `installed_version` against what the repo resolves **right now**:
|
|
63
|
+
|
|
64
|
+
- Read the live lockfile / manifest: `package-lock.json` / `pnpm-lock.yaml` / `yarn.lock`, `uv.lock` / `poetry.lock` / `requirements.txt`, `go.mod`/`go.sum`, `Cargo.lock`, `Gemfile.lock`. For container/OS findings, the equivalent is "is this image still deployed?" — check the current image tag / Dockerfile base.
|
|
65
|
+
- If the **currently resolved version ≥ `fixed_version`**, the advisory is **`fixed`** → drop it from the queue before enrichment. Record it in the "Already fixed" section as the audit trail.
|
|
66
|
+
- If a manifest comment or prior triage already dismisses the CVE (e.g. `urllib3>=2.7.0 # CVE-2026-44431`), treat as `fixed`/known and don't re-litigate.
|
|
67
|
+
|
|
68
|
+
This single pass routinely clears the majority of a stale scan and saves the expensive work for findings that are actually still present. **Never file remediation for an advisory without confirming it still exists in the current tree.**
|
|
69
|
+
|
|
70
|
+
## The enrichment signals (for advisories that survive reconciliation)
|
|
71
|
+
|
|
72
|
+
Gather what you can. Degrade gracefully — a missing signal is "unknown", not "safe". OS-package and library findings both get KEV/EPSS (they're CVEs); IaC/config findings skip threat-intel and triage on exposure.
|
|
73
|
+
|
|
74
|
+
### KEV — CISA Known Exploited Vulnerabilities *(Threat, binary)*
|
|
75
|
+
|
|
76
|
+
Fetch once per run and match every advisory's `cve`/`aliases`:
|
|
77
|
+
`https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json`
|
|
78
|
+
|
|
79
|
+
KEV membership is binary, evidence-grounded, auditor-defensible. **A reachable KEV vulnerability is a hotfix candidate regardless of CVSS.**
|
|
80
|
+
|
|
81
|
+
### EPSS — Exploit Prediction Scoring System *(Threat, probability)*
|
|
82
|
+
|
|
83
|
+
Fetch per CVE (batchable, comma-separated):
|
|
84
|
+
`https://api.first.org/data/v1/epss?cve=CVE-2024-XXXX,CVE-2024-YYYY`
|
|
85
|
+
|
|
86
|
+
Daily-refreshed probability (0–1) of exploitation in the next 30 days. Ranks the long tail KEV is silent on. Rough bands: `≥0.5` high, `0.1–0.5` elevated, `<0.1` low. A prior, not a verdict.
|
|
87
|
+
|
|
88
|
+
### Reachability — is the vulnerable code in our execute path? *(Exposure)* — judge by `component_type`
|
|
89
|
+
|
|
90
|
+
The strongest noise filter. **How you judge reachability depends on what kind of component it is:**
|
|
91
|
+
|
|
92
|
+
- **`library` (app dependency):**
|
|
93
|
+
- *Dev/test only?* Not shipped or run in prod → **not_affected** in prod (`vulnerable_code_not_in_execute_path`). Infer dev/runtime from lockfile groups — there is usually no single flag.
|
|
94
|
+
- *Imported at all?* `search_graph(name_pattern=<pkg>)` / `search_code(<pkg>)`. Unused transitive → low exposure.
|
|
95
|
+
- *Vulnerable function reached?* When the advisory names the affected API, `trace_path` / `search_code` to confirm our code calls *that* surface. Not present / not on a reachable path → **not_affected**.
|
|
96
|
+
- **`os-package` (Debian/Alpine/RPM from a container scan):** judge **two separate axes** — *reachability* (is the vulnerable code path called by untrusted input?) drives urgency; *removability* (can we delete it, and what breaks?) drives remediation. **They are not the same — unreachable ≠ removable.** A headless API can't reach Mesa's OpenGL code *and* can't remove Mesa if it arrived transitively behind a package it needs. A C library is only reachable if something in the running app binds to it, so grep the **consumer**, not the C package name (the app never `import`s `libexpat1`). Honor the scanner's fix-state: `will_not_fix`/`end_of_life`/no-`FixedVersion` means **no fix exists** → the lever is accept-with-expiry or a base-image bump/rebuild, **never** an "upgrade X" ticket. **Before recommending any removal or "slim the base image," trace how the package is installed (explicit / transitive / base-image / builder-only) and name the post-change test** — see `./container-scan-triage.md` for the full discipline, the transitive-source and consumer maps, and the anti-patterns. Route image-structure changes to infra/`grimoire-draft`, not app remediation.
|
|
97
|
+
- **`runtime` (the interpreter/build tool itself — e.g. `pip`, `node`, `setuptools`):** is it invoked **at runtime** or only at **build time**? A build-time tool not present/called in the running container → **not_affected** at runtime (`vulnerable_code_not_in_execute_path`); note it as build-image hygiene at most. Check the container entrypoint/CMD.
|
|
98
|
+
- **`container` / `iac` (Dockerfile, k8s, compose misconfig):** not a CVE — no KEV/EPSS. Triage on exposure + control: does the misconfig (root user, no resource limits, exposed port, `:latest` base) actually create reachable risk in our deployment? Route persistent ones to `grimoire-draft`/infra, not an app hotfix.
|
|
99
|
+
|
|
100
|
+
**Grep can lie — verify the binding.** A bare symbol/name match is not proof: `Price.fromstring()` (the `price-parser` library) is not XML `etree.fromstring()`; a package named in a comment isn't a call. Confirm the match is the actual vulnerable binding (right import, right call site) before asserting `not_affected` **or** `affected`.
|
|
101
|
+
|
|
102
|
+
**Resolve unknowns now — `under_investigation` is a last resort, not a default.** When grep doesn't settle reachability, work the question before deferring it:
|
|
103
|
+
1. **Trace deeper** — `codebase-memory-mcp` `trace_path` from the entry points (routes/handlers) to the vulnerable binding; read the actual call site, not just its name.
|
|
104
|
+
2. **Ask the decisive question.** Most reachability unknowns collapse to one yes/no the human can answer instantly — ask it inline rather than filing a task. The pattern: *"Does <surface> ever receive attacker-controlled <input>?"* e.g. "Does any endpoint parse user-supplied XML (SAML/SSO metadata, uploads, external responses)?" → "no" makes expat/libxml2 `not_affected` on the spot; "yes" makes them `affected`. One question can clear several findings **and** spare a register entry and a follow-up task.
|
|
105
|
+
3. Only when the answer genuinely needs work nobody in the session can do — a runtime check, a teammate's knowledge, an external dependency — mark **`under_investigation`**, time-box it, and name exactly what must be checked. Don't force a verdict to look decisive; but don't punt one you could resolve with a trace or a question, either.
|
|
106
|
+
|
|
107
|
+
Record reachability provenance: `graph-verified` (codebase-memory-mcp), `grep-asserted` (fallback), `image-layer` (container scan), or `unknown`.
|
|
108
|
+
|
|
109
|
+
### Exposure & Controls — our deployment, our mitigations *(Exposure + Impact damping)*
|
|
110
|
+
|
|
111
|
+
Read these — do **not** invent a controls config file; the truth already lives here:
|
|
112
|
+
|
|
113
|
+
- **`.grimoire/docs/context.yml`** — `deployment` (internet-facing vs internal vs lambda/batch), `infrastructure`, `services`. An internal-only service behind an auth gateway has far less exposure than a public endpoint.
|
|
114
|
+
- **MADR decisions** (`.grimoire/decisions/*.md`) — `Security (CIA)` quality-attribute rows and security-relevant decisions (WAF, network isolation, input validation, auth model, tenancy). A documented control that breaks the attack path is a legitimate VEX `inline_mitigations_already_exist` / severity damper.
|
|
115
|
+
- **App config that satisfies (or fails) an advisory's precondition** — many CVEs are conditional ("only when `SESSION_SAVE_EVERY_REQUEST=True`", "only on ASGI", "only if middleware X enabled"). Read the actual setting. A precondition that is **met** raises urgency; one that is **absent** is a clean `not_affected` (`vulnerable_code_cannot_be_controlled_by_adversary` / not in execute path).
|
|
116
|
+
|
|
117
|
+
If a control that *would* change the verdict is assumed but **not recorded anywhere**, do not credit it silently. Flag the gap and recommend recording it as a MADR via `grimoire-draft` — an undocumented control is not defensible in an audit.
|
|
118
|
+
|
|
119
|
+
## VEX verdict — the per-advisory status
|
|
120
|
+
|
|
121
|
+
Assign each surviving advisory a [VEX](https://www.cisa.gov/sites/default/files/2023-04/minimum-requirements-for-vex-508c.pdf) status. This is the noise-suppression layer: only `affected` items become dev work.
|
|
122
|
+
|
|
123
|
+
| Status | Meaning | Justification codes (for `not_affected`) |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `fixed` | Already remediated — current tree resolves ≥ the fixed version (from Step 2). | — |
|
|
126
|
+
| `not_affected` | We are not exploitable. **No dev work.** | `component_not_present`, `vulnerable_code_not_present`, `vulnerable_code_not_in_execute_path`, `vulnerable_code_cannot_be_controlled_by_adversary`, `inline_mitigations_already_exist` |
|
|
127
|
+
| `affected` | Exploitable in our deployment. **Needs a remediation action.** | — (carries an urgency, see below) |
|
|
128
|
+
| `under_investigation` | Can't determine yet (no graph, ambiguous advisory). Time-box it. | — |
|
|
129
|
+
|
|
130
|
+
Every verdict records *why*. A `not_affected` with a justification code is the auditor-defensible way to dismiss noise; a bare "looks fine" is not.
|
|
131
|
+
|
|
132
|
+
## Urgency — the decision that matters (for `affected` items only)
|
|
133
|
+
|
|
134
|
+
| Urgency | Trigger | Action |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| **hotfix-now** | In **KEV** AND reachable AND exposed; **or** EPSS high + reachable + internet-exposed + no mitigating control; **or** active exploitation against a high-impact surface (auth, PII, RCE on a public endpoint). | Drop everything. Expedited fix branch, out-of-band release. Notify security owner. |
|
|
137
|
+
| **next-release** | Reachable but exposure is damped (internal-only / behind a control), **or** no KEV and low/elevated EPSS, **or** fix requires a non-trivial upgrade / image rebuild with no active-exploitation pressure. | File remediation for the normal testing / release cycle. |
|
|
138
|
+
| **accept (risk-accepted)** | `affected` but low real risk and **no fix available** (no patched version / no newer base image yet). | Record justification + an **expiry / revisit date**. Re-triage on expiry or when a fix ships. Don't let it become permanent. |
|
|
139
|
+
|
|
140
|
+
Decision tree, in order:
|
|
141
|
+
|
|
142
|
+
1. Already `fixed` (Step 2)? → done, drop. No enrichment needed.
|
|
143
|
+
2. `not_affected` (reachability / precondition absent)? → done, it's noise. No urgency.
|
|
144
|
+
3. Reachable + (KEV **or** high EPSS) + internet-exposed + no breaking control? → **hotfix-now**.
|
|
145
|
+
4. `affected` but damped (not exposed / control breaks the path / low EPSS / dev-or-build-time only) → **next-release**.
|
|
146
|
+
5. `affected`, no fix exists, low risk → **accept** with expiry.
|
|
147
|
+
|
|
148
|
+
Default bias: unknown reachability + internet-facing + KEV → **hotfix-now** (fail safe). Unknown reachability + no KEV + low EPSS → **under_investigation** + next-release — don't manufacture an emergency.
|
|
149
|
+
|
|
150
|
+
## The Contrarian pass — calibrate before you escalate
|
|
151
|
+
|
|
152
|
+
Before finalizing **any** `hotfix-now` or `affected` verdict, run the **Contrarian calibration pass** (`./review-personas.md` §4.8) over the escalated findings. The Contrarian adds no findings; it challenges the ones we're about to act on. For each escalation ask:
|
|
153
|
+
|
|
154
|
+
1. **Steel-man "we are not affected."** Strongest case this doesn't matter here — dev/build-only, function never called, base-OS cruft, behind auth + network isolation, precondition absent, input never attacker-controlled. If it holds, drop the urgency (often to `not_affected`).
|
|
155
|
+
2. **Name the assumption.** "Assumes the parser is fed untrusted input." "Assumes this endpoint is public." If it contradicts `context.yml`/a MADR/the actual config, the finding is mis-calibrated.
|
|
156
|
+
3. **Inversion.** If we hotfixed this, what *new* risk ships — a rushed major bump, an untested base-image swap, a breaking transitive change? Is the cure riskier than the disease before the next release window?
|
|
157
|
+
4. **Is doing-nothing-until-release right?** Symptom vs root cause; will it actually trigger; cost of "fix now" vs "fix when it hurts".
|
|
158
|
+
5. **Is severity calibrated?** A `hotfix-now` must clear all of: reachable, exploitable-as-deployed, real blast radius.
|
|
159
|
+
|
|
160
|
+
Emit per escalation: `[hotfix upheld]` / `[hotfix → next-release]` / `[finding dropped]` with one line of evidence. Summary counts are **post-Contrarian**. Calibration, not veto — a surviving harm path tied to `context.yml`/KEV stands.
|
|
161
|
+
|
|
162
|
+
## Triage record format
|
|
163
|
+
|
|
164
|
+
Write `.grimoire/security/vulns/<run-date>/triage.md`:
|
|
165
|
+
|
|
166
|
+
```markdown
|
|
167
|
+
---
|
|
168
|
+
scanners: [<npm-audit|pip-audit|osv-scanner|trivy|grype|snyk|sarif|other>]
|
|
169
|
+
scan_dates: [<YYYY-MM-DD per source>]
|
|
170
|
+
triaged_date: <YYYY-MM-DD>
|
|
171
|
+
reconciled_against: <lockfile/manifest/image checked>
|
|
172
|
+
kev-feed: <date fetched, or "offline">
|
|
173
|
+
epss-fetched: <true|false>
|
|
174
|
+
reachability: <graph-verified|grep-asserted|image-layer|unknown>
|
|
175
|
+
totals: { raw_findings: N, unique_advisories: N, fixed: N, not_affected: N, affected: N, hotfix_now: N, accepted: N, under_investigation: N }
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
# Vulnerability Triage — <run-date>
|
|
179
|
+
|
|
180
|
+
## Hotfix now (drop everything)
|
|
181
|
+
<!-- omit section if empty -->
|
|
182
|
+
### <id> — <component> <version> (<component_type>)
|
|
183
|
+
- **VEX**: affected · **Urgency**: hotfix-now
|
|
184
|
+
- **KEV**: yes/no · **EPSS**: 0.NN · **CVSS**: N.N (<severity>) · **Scanner**: <tool>
|
|
185
|
+
- **Reachable**: <yes — calls X / image-layer / no / unknown> (<provenance>)
|
|
186
|
+
- **Exposure**: <internet-facing endpoint / internal-only / dev/build-only / base-OS>
|
|
187
|
+
- **Controls**: <none that break the path / WAF per ADR-00NN / behind auth gateway>
|
|
188
|
+
- **Precondition**: <CVE condition — met / absent, with the setting checked>
|
|
189
|
+
- **Blast radius**: <RCE / PII read / DoS / info disclosure>
|
|
190
|
+
- **Contrarian**: [hotfix upheld] <one line>
|
|
191
|
+
- **Fix**: upgrade <component> <from> → <to> / rebuild on <base> (or: no fix — mitigation: <...>)
|
|
192
|
+
|
|
193
|
+
## Next release cycle
|
|
194
|
+
### <id> — <component> (<component_type>)
|
|
195
|
+
- (same fields) · **Contrarian**: [hotfix → next-release] <why damped>
|
|
196
|
+
|
|
197
|
+
## Risk-accepted (revisit by <date>)
|
|
198
|
+
### <id> — <component>
|
|
199
|
+
- **VEX**: affected · no fix available · **Expiry**: <YYYY-MM-DD> · **Justification**: <...>
|
|
200
|
+
|
|
201
|
+
## Already fixed (reconciled out — scan was stale)
|
|
202
|
+
<!-- audit trail: dropped before enrichment because current tree is past the fix -->
|
|
203
|
+
- <id> — <component>: current tree resolves <ver> ≥ fixed <ver>
|
|
204
|
+
- <id> — <component>: dismissed in manifest (<comment/prior triage>)
|
|
205
|
+
|
|
206
|
+
## Not affected (suppressed noise)
|
|
207
|
+
- <id> — <component>: not_affected (`vulnerable_code_not_in_execute_path` — dev-only / build-time / base-OS not invoked)
|
|
208
|
+
- <id> — <component>: not_affected (`vulnerable_code_cannot_be_controlled_by_adversary` — precondition absent: <setting>)
|
|
209
|
+
|
|
210
|
+
## Under investigation (time-boxed to <date>)
|
|
211
|
+
- <id> — <component>: <what's blocking the call>
|
|
212
|
+
|
|
213
|
+
## Control gaps surfaced
|
|
214
|
+
- <control> assumed for <id> but not in any decision record → suggest `grimoire-draft`.
|
|
215
|
+
|
|
216
|
+
## Infra follow-ups (root-cause, not per-CVE)
|
|
217
|
+
<!-- container/IaC hygiene — route to infra/grimoire-draft, not app remediation. Each must state how-installed + post-change test, per container-scan-triage.md. -->
|
|
218
|
+
- <package> (CVE <id>): installed via <explicit line N / transitive via PARENT / base image / builder-only>; depends: <evidence>; removable: <yes-safe / yes-after-removing-PARENT / no-base-OS / no-required-by-Y>; recommendation: <Dockerfile edit / bump-base / accept+document> — test: <build / import / DB connect>.
|
|
219
|
+
- Secret/config result: <e.g. `.env` baked into image layer> → remove from image, route to infra.
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Supply-chain note (separate from CVE triage)
|
|
223
|
+
|
|
224
|
+
A *known CVE* in a component is what this reference triages. A **dependency add/upgrade** (new package, version bump, floating range, missing lockfile/integrity hashes) is a different risk class — covered by `security-compliance.md` § Supply Chain Defense, a review-time blocker, not a CVE-triage output. Keep them distinct: triage answers "is this known CVE a hotfix?"; supply-chain defense answers "should this change merge at all?".
|
|
225
|
+
|
|
226
|
+
## Principles
|
|
227
|
+
|
|
228
|
+
- **Reconcile first.** A stale scan is mostly already-fixed findings. Confirm each advisory still exists in the current tree before any other work — it's the cheapest, highest-leverage pass.
|
|
229
|
+
- **Scanner-agnostic by construction.** Normalize any tool's output into the canonical model, then triage that. Never couple the verdict logic to npm's or pip's field names.
|
|
230
|
+
- **CVSS ranks the world; we triage our deployment.** The whole job is that gap. 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.
|
|
231
|
+
- **Reachability is type-aware.** Library imports, OS-package usage, runtime-vs-build, and IaC misconfig are judged differently. A flagged base-image OS lib the app never calls is not a prod emergency.
|
|
232
|
+
- **`not_affected` needs a justification code, not a vibe.** That line is the audit trail.
|
|
233
|
+
- **Controls must be recorded to count.** Undocumented WAF/auth/isolation can't damp a verdict — flag the gap.
|
|
234
|
+
- **Fail safe on unknowns, but don't manufacture emergencies.**
|
|
235
|
+
- **The Contrarian calibrates escalations; it does not suppress real signal.**
|
|
236
|
+
- **`accept` is not `ignore`.** It carries an expiry and gets re-triaged.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Grimoire Design Principles
|
|
2
|
+
|
|
3
|
+
Four principles govern every grimoire artifact and every change. `grimoire-draft`,
|
|
4
|
+
`grimoire-plan`, and `grimoire-review` each enforce them at their own stage. They
|
|
5
|
+
are not style preferences — they are gates. A draft, plan, or design that violates
|
|
6
|
+
one without a stated reason is rejected, not merged.
|
|
7
|
+
|
|
8
|
+
This file is the single home for the principles (it practices what it preaches —
|
|
9
|
+
the skills cite it rather than restating it).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. One right way to do a thing
|
|
14
|
+
|
|
15
|
+
There is exactly **one** sanctioned way to do each thing in the codebase, and one
|
|
16
|
+
authoritative home for each fact. Two ways to do the same thing is a defect, even
|
|
17
|
+
if both work.
|
|
18
|
+
|
|
19
|
+
- One capability → one feature spec. One decision → one MADR. One constraint → one
|
|
20
|
+
register entry. One fact → one home. No capability described in three places.
|
|
21
|
+
- When a second mechanism appears for an existing job, the right move is to delete
|
|
22
|
+
one and converge — never to keep both "for flexibility."
|
|
23
|
+
- **Tell:** "we could do it this way *or* that way" in a spec/plan. Pick one. Record
|
|
24
|
+
why in a MADR if the choice is non-obvious; don't leave both paths in the code.
|
|
25
|
+
|
|
26
|
+
## 2. DRY — don't repeat yourself
|
|
27
|
+
|
|
28
|
+
Every piece of knowledge has a single, unambiguous representation.
|
|
29
|
+
|
|
30
|
+
- Don't store what's derivable. Code structure comes from codebase-memory-mcp on
|
|
31
|
+
demand — never freeze it into a doc that drifts. Generated overviews regenerate;
|
|
32
|
+
they are not hand-edited.
|
|
33
|
+
- Reuse before write: search the graph for an existing function/utility before
|
|
34
|
+
writing a new one. Three near-identical copies is the trigger to converge — but
|
|
35
|
+
do not abstract before the third (see KISS).
|
|
36
|
+
- Duplication of *content* (the same rule in three skill files, the same constant in
|
|
37
|
+
three modules, the same scenario in feature + MADR) is the target. Eliminate it.
|
|
38
|
+
|
|
39
|
+
## 3. Don't reinvent the wheel — use existing tools
|
|
40
|
+
|
|
41
|
+
If an established tool already does a job well, use it. Do not build a parallel
|
|
42
|
+
grimoire mechanism that duplicates it.
|
|
43
|
+
|
|
44
|
+
- **git** is the wheel for change processes: branches = isolation, `git diff` =
|
|
45
|
+
staging, `git log` + PR + commit trailers = history and change identity. Do not
|
|
46
|
+
build change-folder copies, promote/sync steps, or bespoke archive/changelog trees.
|
|
47
|
+
- For auth, crypto, parsing, HTTP, queues, etc. — adopt the battle-tested library.
|
|
48
|
+
Never roll custom crypto, custom session management, custom auth tokens.
|
|
49
|
+
- Before building any tracking/versioning/state/diff mechanism, ask: does a standard
|
|
50
|
+
tool already in the stack do this? If yes, wire to it.
|
|
51
|
+
- **Exception that proves the rule:** when no single standard tool exists (e.g. issue
|
|
52
|
+
tracking is a fractured landscape), don't force-adopt one *and* don't build a
|
|
53
|
+
general-purpose clone. Keep any local mechanism narrow and purpose-scoped.
|
|
54
|
+
|
|
55
|
+
## 4. Keep it simple (KISS)
|
|
56
|
+
|
|
57
|
+
The simplest thing that fully solves the *stated* problem wins.
|
|
58
|
+
|
|
59
|
+
- Least code, fewest new files, smallest surface area. A few lines in an existing
|
|
60
|
+
file beats a new module. A standard-library call beats a new dependency. Inline
|
|
61
|
+
beats a one-line wrapper.
|
|
62
|
+
- No premature abstraction. No `BaseX`/factory/strategy/config-object for a single
|
|
63
|
+
caller. No speculative generality "for a future second caller" that doesn't exist.
|
|
64
|
+
- Solve the problem in front of you, not the imagined one. Non-goals are real scope
|
|
65
|
+
boundaries — do not plan or build past them.
|
|
66
|
+
- **Tell:** an abstraction, indirection, or dependency whose only justification is a
|
|
67
|
+
hypothetical. Cut it.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## How the stages apply these
|
|
72
|
+
|
|
73
|
+
- **draft** — admission-test every artifact: does this fact already have a home
|
|
74
|
+
(one-right-way/DRY)? Is it behavior (→ feature) or a constraint/decision/structure
|
|
75
|
+
(→ its own home, not a feature)? Is there an existing tool/library for it
|
|
76
|
+
(don't-reinvent)? Is the scope the stated problem only (KISS)?
|
|
77
|
+
- **plan** — every task names the single approach (one-right-way), reuses before
|
|
78
|
+
writing (DRY), follows a proven pattern / existing tool rather than a bespoke one
|
|
79
|
+
(don't-reinvent), and chooses the least-code option within non-goals (KISS). Flag
|
|
80
|
+
any task that adds an abstraction, dependency, or second mechanism.
|
|
81
|
+
- **review** — a dedicated principles pass: hunt for duplicate homes, derivable-but-
|
|
82
|
+
stored facts, reinvented wheels, and speculative complexity. Each is a finding.
|
|
@@ -91,8 +91,8 @@ TODO/FIXME/HACK/XXX comments that have aged.
|
|
|
91
91
|
## 2g. Duplication
|
|
92
92
|
|
|
93
93
|
**How to scan:**
|
|
94
|
-
-
|
|
95
|
-
-
|
|
94
|
+
- Run `config.tools.duplicates` if configured (e.g., jscpd), or `grimoire health` (config-driven `duplicates` metric)
|
|
95
|
+
- Plus semantic clones via `search_graph(semantic_query=[...])` (requires codebase-memory-mcp) to catch re-implementations under different names
|
|
96
96
|
- Group by area — within-area dupes are easy to consolidate
|
|
97
97
|
|
|
98
98
|
**Severity:** high = >30 lines or >3 copies, medium = 10-30 lines or 2 copies, low = <10 lines
|
|
@@ -26,13 +26,13 @@ Build once, inject as preface to every persona. Findings that don't threaten any
|
|
|
26
26
|
- `.grimoire/brand/tokens.json` and `.grimoire/brand/voice.md` — brand axis (if exist; see `./brand-tokens-format.md`)
|
|
27
27
|
- `.grimoire/changes/<id>/consult.md` — pre-design consult assumptions + givens (if exists)
|
|
28
28
|
- `.grimoire/changes/<id>/designs/problem.md` — design problem statement (if exists)
|
|
29
|
-
- Tag histogram across
|
|
29
|
+
- Tag histogram across `features/**/*.feature` (the live baseline; features are edited in place, so this covers both the change and prior work)
|
|
30
30
|
- All `.grimoire/decisions/*.md` with `status: accepted` — extract ID, title, top Decision Driver
|
|
31
31
|
- Linked manifest's `Why` and `Non-goals` (if a Change trailer / active manifest exists); else PR body or commit messages
|
|
32
32
|
|
|
33
33
|
### Feature inventory
|
|
34
34
|
|
|
35
|
-
- Glob
|
|
35
|
+
- Glob `features/**/*.feature` (the live baseline, including this change's edits)
|
|
36
36
|
- Parse: `Feature:` line, first description line, `@tags`
|
|
37
37
|
- Bucket by path prefix (area)
|
|
38
38
|
- If total >80, emit area-level summary only (count + capability one-liner)
|
|
@@ -274,7 +274,8 @@ If none of the above pin a rule, **don't invent one**. Style preferences without
|
|
|
274
274
|
- **Imports**: Order, grouping, and form match the project (relative vs absolute, `.js` extension policy, type-only imports)?
|
|
275
275
|
- **Formatting**: Diff respects `.editorconfig` and formatter rules (indentation, line endings, trailing newline, max line length)? Any formatter-noisy hunks unrelated to the change?
|
|
276
276
|
- **Comments — presence**: Is there a comment whose WHAT is already obvious from the code? Per most projects' comment policies (and grimoire's `AGENTS.md`), explanatory-of-what comments are noise — **suggestion** to remove.
|
|
277
|
-
- **Comments —
|
|
277
|
+
- **Comments — self-contained**: Does a comment name an external artifact that moves independently — a feature/scenario/`.feature`, MADR/ADR number, change-id, ticket/PR, specific test, or tag code (`LOG-OBS-003`)? These orphan — **suggestion** to rewrite in terms of the code itself (`code-quality.md` §7). Generic descriptive words (test, feature) are fine; identifiers pointing elsewhere are not.
|
|
278
|
+
- **Comments — no essays**: Does a docstring lead with a prose paragraph before its params, or restate types in prose? Keep the summary to 1–2 terse lines — **suggestion** to compress; design rationale belongs in a decision record.
|
|
278
279
|
- **Comments — style**: Match the project's comment form (`//` vs `/* */` vs `#`, JSDoc/TSDoc/docstring conventions)?
|
|
279
280
|
- **Docstrings**: New public functions/classes — does the project require docstrings? If yes (per `comment_style` or visible convention), missing docstring = **suggestion**. If no, added boilerplate docstrings = **suggestion** to remove.
|
|
280
281
|
- **Dead comments**: Commented-out code in the diff = **suggestion** to delete.
|
|
@@ -41,7 +41,7 @@ For contract regression tests: if the client starts reading a new field or stops
|
|
|
41
41
|
Before importing a module, calling a function, or adding a dependency — confirm it exists.
|
|
42
42
|
|
|
43
43
|
**Imports and functions:**
|
|
44
|
-
-
|
|
44
|
+
- Query the graph first (`search_graph` / `get_code_snippet`) for the exact symbol, path, and signature
|
|
45
45
|
- If importing from a file you haven't read, read it first
|
|
46
46
|
- If an import fails, don't guess — read the actual module for the real export name
|
|
47
47
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Grimoire Risk-Accepted Vulnerabilities
|
|
2
|
+
# Vulnerabilities triaged as `affected` but consciously accepted — because no fix
|
|
3
|
+
# exists yet, the residual risk is low, or the fix isn't worth it right now.
|
|
4
|
+
#
|
|
5
|
+
# Written by: grimoire-vuln-remediate (from a grimoire-vuln-triage `accept` verdict)
|
|
6
|
+
# Read by: grimoire-vuln-triage — an UNEXPIRED entry auto-suppresses that CVE as
|
|
7
|
+
# known-accepted so it doesn't re-flood the queue. An EXPIRED entry is
|
|
8
|
+
# re-surfaced for re-triage.
|
|
9
|
+
#
|
|
10
|
+
# Each entry requires: a CVE, the component, the VEX justification, a reason, an
|
|
11
|
+
# owner, an accepted date, and an expiry (when to re-evaluate).
|
|
12
|
+
#
|
|
13
|
+
# An accepted risk is NOT closed — it is scheduled for review. Don't accept the same
|
|
14
|
+
# CVE twice; update the existing entry.
|
|
15
|
+
#
|
|
16
|
+
# Created by: grimoire init
|
|
17
|
+
# Last updated: YYYY-MM-DD
|
|
18
|
+
|
|
19
|
+
accepted: []
|
|
20
|
+
|
|
21
|
+
# --- Examples ---
|
|
22
|
+
#
|
|
23
|
+
# No upstream fix available — low residual risk, reachable but damped:
|
|
24
|
+
#
|
|
25
|
+
# - cve: CVE-2026-44405
|
|
26
|
+
# component: paramiko
|
|
27
|
+
# component_type: library
|
|
28
|
+
# vex_justification: inline_mitigations_already_exist
|
|
29
|
+
# reason: "SHA-1 accepted in rsakey; outbound SFTP only, to tenant-configured
|
|
30
|
+
# trusted hosts. No upstream fix. EPSS ~0, not in KEV. Blast radius:
|
|
31
|
+
# low (integration, not the public API)."
|
|
32
|
+
# owner: fred
|
|
33
|
+
# accepted: 2026-06-04
|
|
34
|
+
# expires: 2026-09-04
|
|
35
|
+
#
|
|
36
|
+
# Base-image OS package with no patched version yet (not removable — transitive):
|
|
37
|
+
#
|
|
38
|
+
# - cve: CVE-2026-40356
|
|
39
|
+
# component: krb5 (libgssapi-krb5-2, libk5crypto3, libkrb5-3, libkrb5support0)
|
|
40
|
+
# component_type: os-package
|
|
41
|
+
# vex_justification: vulnerable_code_not_in_execute_path
|
|
42
|
+
# reason: "Kerberos DoS. No gssapi/kerberos usage in app; pulled transitively
|
|
43
|
+
# via libpq5 (Postgres). Not removable without breaking libpq.
|
|
44
|
+
# status=affected, no FixedVersion. Re-check on base-image bump."
|
|
45
|
+
# owner: fred
|
|
46
|
+
# accepted: 2026-06-04
|
|
47
|
+
# expires: 2026-09-04
|