Skip to content

Commit 3e0d7be

Browse files
committed
feat: add deterministic report skeleton renderer (v0.9.0)
Add render_template.py to omv-report — takes an Evidence.v1 YAML and renders a pre-filled report skeleton for vuldb, ghsa, osv, or md formats. Structural fields (package, versions, CVSS, CWE, source→sink→guard, reproducer, dedup checklist) are filled deterministically; prose sections carry [DRAFT: ...] markers for Claude to complete. - Gates on status:confirmed + submission_score≥75 (--force bypasses) - OSV emits pure JSON; VulDB/GHSA/MD emit Markdown with score footer - Score computation mirrors contracts/evidence.v1.yaml scoring guide - Adds confirmed-prototype-pollution.yaml fixture (100/100 scores) - Adds four golden files (vuldb/ghsa/osv/md) for exact-match regression - release_check.py: validate_renderer() runs all four formats at release time - SKILL.md: confirmed path now documents running the renderer for a skeleton
1 parent d338483 commit 3e0d7be

8 files changed

Lines changed: 984 additions & 0 deletions

File tree

scripts/release_check.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
("skills/omv-repro/scripts/check_output.py", "2", "skills/omv-repro/evals/golden/blocked-repro-failure.md"),
2929
]
3030

31+
RENDERER_FIXTURE = "skills/omv-report/evals/fixtures/confirmed-prototype-pollution.yaml"
32+
RENDERER_FORMATS = ["vuldb", "ghsa", "osv", "md"]
33+
3134

3235
def run(args: list[str]) -> None:
3336
subprocess.run(args, cwd=REPO_ROOT, check=True)
@@ -113,6 +116,39 @@ def validate_stable_evals() -> None:
113116
])
114117

115118

119+
def validate_renderer() -> None:
120+
renderer = REPO_ROOT / "skills/omv-report/scripts/render_template.py"
121+
fixture = REPO_ROOT / RENDERER_FIXTURE
122+
golden_dir = REPO_ROOT / "skills/omv-report/evals/golden"
123+
124+
for fmt in RENDERER_FORMATS:
125+
ext = "json" if fmt == "osv" else "md"
126+
golden = golden_dir / f"render-{fmt}.{ext}"
127+
if not golden.exists():
128+
raise SystemExit(f"missing renderer golden: {golden}")
129+
130+
result = subprocess.run(
131+
[sys.executable, str(renderer), "--finding", str(fixture), "--format", fmt],
132+
capture_output=True,
133+
text=True,
134+
cwd=REPO_ROOT,
135+
)
136+
if result.returncode != 0:
137+
raise SystemExit(
138+
f"renderer failed for --format {fmt}:\n{result.stderr.strip()}"
139+
)
140+
141+
actual = result.stdout
142+
expected = golden.read_text(encoding="utf-8")
143+
if actual != expected:
144+
raise SystemExit(
145+
f"renderer golden mismatch for --format {fmt}\n"
146+
f"Re-run: python3 {renderer} --finding {fixture} --format {fmt}"
147+
)
148+
149+
print(f"OK: renderer golden tests passed ({', '.join(RENDERER_FORMATS)})", flush=True)
150+
151+
116152
def main() -> None:
117153
parser = argparse.ArgumentParser()
118154
parser.add_argument(
@@ -136,6 +172,7 @@ def main() -> None:
136172
validate_versions()
137173
run([sys.executable, str(VALIDATE_SCRIPT)])
138174
validate_stable_evals()
175+
validate_renderer()
139176

140177
artifacts: list[Path] = []
141178
if args.write_artifacts:

skills/omv-report/SKILL.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ Use the validation result to choose output mode:
5959
- `submissionScore` below 75 or `verdict.exploitability` not `proven`: do not produce a submission-ready report; explain what evidence or reproduction artifact is missing.
6060
- `evidence.repro_artifacts` present: reference the artifacts as local reviewer evidence. If absent, warn that the report depends only on inline reproducer text.
6161

62+
### Deterministic Skeleton Renderer
63+
64+
For confirmed findings with submission score ≥ 75, run the deterministic renderer first to get a pre-filled skeleton:
65+
66+
```bash
67+
python3 "$(omv doctor --json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('skillsDir',''))")/omv-report/scripts/render_template.py" \
68+
--finding .omv/findings/<id>.yaml --format vuldb|ghsa|osv|md
69+
```
70+
71+
Or if the skill directory is known (e.g. `~/.claude/skills/omv-report/`):
72+
73+
```bash
74+
python3 ~/.claude/skills/omv-report/scripts/render_template.py \
75+
--finding .omv/findings/<id>.yaml --format vuldb
76+
```
77+
78+
The renderer fills all structural fields (package, versions, CVSS, CWE, source→sink→guard, reproducer, dedup checklist) and leaves `[DRAFT: ...]` markers for prose sections. Fill in every `[DRAFT: ...]` before submitting. Do not submit placeholders.
79+
6280
After producing a submission-ready report for a confirmed finding, suggest removing it from the active local queue:
6381

6482
```bash
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
schema_version: "1"
2+
handoff_version: "1.0"
3+
status: confirmed
4+
researcher_goal: VulDB
5+
6+
package:
7+
ecosystem: npm
8+
registry_name: demo-merge
9+
repository_url: https://github.com/example/demo-merge
10+
vendor: example-utils project
11+
product: demo-merge
12+
13+
versions:
14+
tested: "2.4.1"
15+
affected_range: "up to and including 2.4.1"
16+
fixed: "2.4.2"
17+
18+
vulnerability:
19+
class: Prototype Pollution
20+
cwe: CWE-1321
21+
affected_component: src/merge.js
22+
affected_function: mergeDeep()
23+
24+
evidence:
25+
source: "JSON.parse(req.body) passed to mergeDeep()"
26+
sink: "target[key] = value in mergeDeep() at src/merge.js:42"
27+
guard: "no blocklist for __proto__, constructor, prototype"
28+
reproducer: |
29+
node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}'));" \
30+
&& node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}')); console.log({}.polluted);"
31+
observed_result: "prints 'yes' confirming Object.prototype mutation"
32+
repro_artifacts:
33+
- .omv/repro/demo-merge-pp/commands.sh
34+
- .omv/repro/demo-merge-pp/observed.txt
35+
36+
cvss:
37+
vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N"
38+
score: "6.4"
39+
severity: Medium
40+
41+
impact:
42+
attack_vector: Network
43+
authentication_required: "true"
44+
user_interaction_required: "false"
45+
scope_changed: "true"
46+
confidentiality: Low
47+
integrity: Low
48+
availability: None
49+
50+
dedup:
51+
nvd_searched: true
52+
ghsa_searched: true
53+
ecosystem_db_searched: true
54+
existing_cve: none
55+
notes: "Searched NVD, GHSA, npmjs advisory DB — no existing entry found"
56+
57+
disclosure:
58+
vendor_contacted: true
59+
contact_date: "2026-04-20"
60+
vendor_response: acknowledged
61+
planned_disclosure_date: "2026-05-20"
62+
63+
blockers: []
64+
65+
verdict:
66+
exploitability: proven
67+
confidence: high
68+
reason: "Local reproducer confirmed Object.prototype mutation via __proto__ injection"
69+
70+
provenance:
71+
verification_date: "2026-04-28"
72+
researcher: researcher
73+
unverified_fields: []
74+
tool_versions:
75+
omv-find: "0.8.0"
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## GitHub Security Advisory
2+
3+
**Ecosystem**
4+
npm
5+
6+
**Package name**
7+
demo-merge
8+
9+
**Affected versions**
10+
>= 0, < 2.4.2
11+
12+
**Patched versions**
13+
2.4.2
14+
15+
**Severity**
16+
Medium
17+
18+
**CWE IDs**
19+
CWE-1321
20+
21+
**Title**
22+
demo-merge: Prototype Pollution in mergeDeep()
23+
24+
**Description**
25+
### Summary
26+
[DRAFT: One paragraph: Prototype Pollution in demo-merge up to and including 2.4.1.]
27+
28+
### Details
29+
30+
- Source: JSON.parse(req.body) passed to mergeDeep()
31+
- Sink: target[key] = value in mergeDeep() at src/merge.js:42
32+
- Guard: no blocklist for __proto__, constructor, prototype
33+
34+
Affected file: `src/merge.js`
35+
Affected function: `mergeDeep()`
36+
37+
### Impact
38+
[DRAFT: Attacker control, required auth (true), user interaction (false), scope (true). CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N]
39+
40+
### Proof of Concept
41+
```
42+
node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}'));" \
43+
&& node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}')); console.log({}.polluted);"
44+
```
45+
Expected output: prints 'yes' confirming Object.prototype mutation
46+
47+
### Recommended Fix
48+
[DRAFT: Specific code fix — reject prototype-mutating keys, validate input, add output encoding, etc.]
49+
50+
---
51+
Rendered by omv render_template | evidence: 100/100 | submission: 100/100 | status: confirmed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# demo-merge: Prototype Pollution in mergeDeep()
2+
3+
## Summary
4+
[DRAFT: One paragraph: Prototype Pollution in demo-merge up to and including 2.4.1, classified as CWE-1321. Local reproducer confirmed Object.prototype mutation via __proto__ injection]
5+
6+
## Affected Versions
7+
- Package: `npm:demo-merge`
8+
- Tested version: `2.4.1`
9+
- Affected range: `up to and including 2.4.1`
10+
- Fixed version: `2.4.2`
11+
12+
## Technical Details
13+
14+
- Source: JSON.parse(req.body) passed to mergeDeep()
15+
- Sink: target[key] = value in mergeDeep() at src/merge.js:42
16+
- Guard: no blocklist for __proto__, constructor, prototype
17+
18+
Affected file: `src/merge.js`
19+
Affected function: `mergeDeep()`
20+
21+
[DRAFT: Explain the root cause and why the guard is insufficient or absent.]
22+
23+
## Impact
24+
[DRAFT: Attacker control, required auth (true), user interaction (false), scope (true), security consequences (C:Low I:Low A:None).]
25+
26+
CVSS v3.1: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N` — 6.4 Medium
27+
28+
## Proof of Concept
29+
30+
```
31+
node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}'));" \
32+
&& node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}')); console.log({}.polluted);"
33+
```
34+
Expected output: prints 'yes' confirming Object.prototype mutation
35+
36+
## Remediation
37+
[DRAFT: Specific fix — e.g. reject prototype-mutating keys, validate input range, add output encoding.]
38+
39+
## Disclosure Timeline
40+
41+
- 2026-04-20: vendor contacted
42+
- response: acknowledged
43+
- 2026-05-20: planned public disclosure
44+
45+
## References
46+
47+
- Repository: https://github.com/example/demo-merge
48+
- CWE-1321: https://cwe.mitre.org/data/definitions/1321.html
49+
50+
Reproduction artifacts:
51+
- .omv/repro/demo-merge-pp/commands.sh
52+
- .omv/repro/demo-merge-pp/observed.txt
53+
54+
---
55+
Rendered by omv render_template | evidence: 100/100 | submission: 100/100 | status: confirmed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"schema_version": "1.6.0",
3+
"id": "TBD",
4+
"modified": "2026-04-28T00:00:00Z",
5+
"published": "2026-05-20T00:00:00Z",
6+
"summary": "demo-merge: Prototype Pollution in mergeDeep()",
7+
"details": "[DRAFT: Root cause, affected component (src/merge.js mergeDeep()), impact, and attack requirements.] Source: JSON.parse(req.body) passed to mergeDeep() \u2192 Sink: target[key] = value in mergeDeep() at src/merge.js:42 \u2192 Guard: no blocklist for __proto__, constructor, prototype Observed: prints 'yes' confirming Object.prototype mutation",
8+
"affected": [
9+
{
10+
"package": {
11+
"ecosystem": "npm",
12+
"name": "demo-merge"
13+
},
14+
"ranges": [
15+
{
16+
"type": "SEMVER",
17+
"events": [
18+
{
19+
"introduced": "0"
20+
},
21+
{
22+
"fixed": "2.4.2"
23+
}
24+
]
25+
}
26+
],
27+
"versions": [
28+
"2.4.1"
29+
],
30+
"database_specific": {
31+
"cwe_ids": [
32+
"CWE-1321"
33+
],
34+
"source": "Source: JSON.parse(req.body) passed to mergeDeep() \u2192 Sink: target[key] = value in mergeDeep() at src/merge.js:42 \u2192 Guard: no blocklist for __proto__, constructor, prototype"
35+
}
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/example/demo-merge"
42+
}
43+
],
44+
"severity": [
45+
{
46+
"type": "CVSS_V3",
47+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N"
48+
}
49+
]
50+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## VulDB Form Fields
2+
3+
**Vendor**
4+
example-utils project
5+
6+
**Product**
7+
demo-merge
8+
9+
**Version**
10+
up to and including 2.4.1
11+
12+
**Class**
13+
Prototype Pollution
14+
15+
**Description**
16+
[DRAFT: Write continuous prose: affected component (src/merge.js mergeDeep()), root cause (no blocklist for __proto__, constructor, prototype), attacker action, impact (Medium 6.4), attack requirements (Auth:true UI:false), and suggested fix.]
17+
18+
Source → Sink → Guard:
19+
- Source: JSON.parse(req.body) passed to mergeDeep()
20+
- Sink: target[key] = value in mergeDeep() at src/merge.js:42
21+
- Guard: no blocklist for __proto__, constructor, prototype
22+
23+
Observed result: prints 'yes' confirming Object.prototype mutation
24+
25+
Reproducer:
26+
node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}'));" \
27+
&& node -e "const {mergeDeep}=require('./'); mergeDeep({},JSON.parse('{\"__proto__\":{\"polluted\":\"yes\"}}')); console.log({}.polluted);"
28+
29+
**Advisory / Exploit**
30+
https://github.com/example/demo-merge
31+
32+
**CVE checkbox**
33+
[x] vendor contacted on 2026-04-20 (response: acknowledged)
34+
[x] no existing CVE found (NVD, GHSA, and ecosystem DB searched)
35+
[ ] no other CNA submission in progress — confirm before submitting
36+
37+
---
38+
Rendered by omv render_template | evidence: 100/100 | submission: 100/100 | status: confirmed

0 commit comments

Comments
 (0)