Skip to content

Commit c5739af

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor/cli-commands
# Conflicts: # CHANGELOG.md
2 parents e5d3cbe + 328cf05 commit c5739af

8 files changed

Lines changed: 119 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Split the `omv` CLI dispatcher (`omv.ts`) into one module per command under `commands/`, collapsing 12 duplicated error handlers into one.
66
- Wired three commands that were validated and advertised but previously unreachable: `omv repro init`, `omv report artifacts`, and `omv findings doctor`. They now dispatch to the existing domain logic (`initReproArtifacts`, `checkReportArtifacts`, `doctorFinding`).
7+
- `omv-find` now excludes packages that already exist in `.omv/findings/` or `.omv/archive/findings/`; pass `--include-known` to override. Added a behavior eval + golden output for local dedup.
78

89
## v0.8.0 - Workflow readiness gates
910

package-lock.json

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
},
6464
"license": "MIT",
6565
"devDependencies": {
66-
"@types/node": "^25.6.0",
66+
"@types/node": "^25.6.2",
6767
"typescript": "^6.0.3"
6868
},
6969
"dependencies": {

skills/omv-find/SKILL.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Stay in passive research mode: inspect public metadata and public source code on
1313
## Invocation
1414

1515
```text
16-
/omv-find [--lang npm|python|go|rust|java|ruby|php|csharp|swift|dart|elixir|perl|r|lua|all] [--vuln VULN_TYPE] [--count N] [keyword ...]
16+
/omv-find [--lang npm|python|go|rust|java|ruby|php|csharp|swift|dart|elixir|perl|r|lua|all] [--vuln VULN_TYPE] [--count N] [--include-known] [keyword ...]
1717
```
1818

1919
Defaults:
@@ -22,6 +22,7 @@ Defaults:
2222
- `--vuln all`
2323
- `--count 15`
2424
- maximum `--count 20`
25+
- `--include-known` off (local findings are excluded by default)
2526

2627
Valid vulnerability aliases: `proto`, `traversal`, `ssrf`, `injection`, `xss`, `redos`, `yaml`, `unsafe`, `deser`, `race`, `overflow`, `auth`, `csrf`, `xxe`, `sql`, `ssti`, `sandbox`, `redirect`, `upload`, `crypto`, `infoleak`.
2728

@@ -54,19 +55,26 @@ Load `references/research-radar.md` only when the user asks for creative/radar/p
5455
- Use the current date to compute freshness.
5556
- Repositories with no default-branch commit in the last 12 months are stale unless the user asks for abandoned targets.
5657

57-
2. **Select maturity lane**
58+
2. **Exclude local findings (dedup)**
59+
- Read `.omv/index.json` in the workspace root. If it exists, collect all `findings[].id` entries regardless of status (`candidate`, `confirmed`, `blocked`) or `archived` flag.
60+
- Also scan `.omv/findings/*.yaml` and `.omv/archive/findings/*.yaml` — extract `package.registry_name` and `package.ecosystem` from each file.
61+
- Build an exclusion set of `(ecosystem, registry_name)` pairs.
62+
- During candidate discovery (step 4), silently skip any package whose `(ecosystem, registry_name)` matches the exclusion set. Do not mention excluded packages in the output unless the user passes `--include-known`.
63+
- If `.omv/index.json` does not exist or is empty, proceed normally with no exclusions.
64+
65+
3. **Select maturity lane**
5866
- Core lanes: npm, Python, Go, Rust, Java, Ruby.
5967
- Extended lanes: PHP, C#, Swift, Dart, Elixir, Perl, R, Lua.
6068
- Extended lanes are supported, but be more conservative: return fewer results when primary metadata or code evidence is weak.
6169

62-
3. **Discover candidates**
70+
4. **Discover candidates**
6371
- Collect 20-40 raw candidates before deep inspection.
6472
- Prefer GitHub repository search plus package registry primary pages/APIs.
6573
- Avoid flagship or heavily audited framework cores unless the user explicitly asks for them.
6674
- Record project name, repo URL, ecosystem, registry URL/package name, short purpose, and discovery source.
6775
- For playbook requests, map the request to one or more pattern packs and keep the pack tag separate from `vuln_direction`.
6876

69-
4. **Verify metadata**
77+
5. **Verify metadata**
7078
- Use primary pages or APIs, not memory.
7179
- Collect GitHub URL, default branch, archived/fork status, stars, last commit date, registry identity, downloads/dependents/importers when visible, release recency, and code-size estimate.
7280
- Use `未确认` for unverified values and lower confidence.
@@ -75,15 +83,15 @@ Load `references/research-radar.md` only when the user asks for creative/radar/p
7583
- `python scripts/collect_metadata.py --repo <github-url> [--registry npm:pkg]`
7684
- `scripts/estimate_loc.sh <github-url-or-local-path>`
7785

78-
5. **Scan source risk** — see `## Source File Discovery` for how to locate files before fetching.
86+
6. **Scan source risk** — see `## Source File Discovery` for how to locate files before fetching.
7987
- Inspect 2-5 relevant source files per surviving candidate.
8088
- Build a concise source -> sink -> guard note.
8189
- Keyword hits alone are low confidence.
8290
- High-ranked projects need at least one exact file/function path or link.
8391
- For radar requests, run only bounded passive diff checks: at most 3 recent commits/releases and 5 changed file names per candidate, then stop or mark `diff_signal: 未确认`.
8492
- Check duplicate risk through passive public advisory/release/issue sources. Mark likely duplicate only when package, vulnerability class, affected behavior, sink, and version context strongly match.
8593

86-
6. **Score and filter**
94+
7. **Score and filter**
8795
- Score out of 100 using `references/scoring.md`.
8896
- If `--vuln` is set, at least 70% of returned projects must be relevant to that class.
8997
- If a narrow ecosystem/vulnerability combination has too few strong candidates, say so and return fewer results instead of padding.
@@ -92,7 +100,7 @@ Load `references/research-radar.md` only when the user asks for creative/radar/p
92100
- Add concise audit-readiness notes for high-ranked candidates: entry file/function, local test or harness idea, expected guard, and blocker.
93101
- Use sanitized examples in explanations unless the user supplied a real target as the audit subject.
94102

95-
7. **Output**
103+
8. **Output**
96104
- Use the table and follow-up sections in `references/output-contract.md`.
97105
- Sort by score descending.
98106
- Include data freshness, sources used, and uncertainty.

skills/omv-find/evals/evals.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,48 @@
11881188
"type": "freshness_and_uncertainty_present"
11891189
}
11901190
]
1191+
},
1192+
{
1193+
"id": 31,
1194+
"prompt": "/omv-find --lang npm --vuln traversal --count 10 已知工作区 .omv/findings 已含 npm 包 [synthetic-tar-safe, synthetic-zip-worker]",
1195+
"expected_output": "输出约 10 个 npm 生态的路径穿越/Zip Slip 候选项目,但工作区 .omv/findings 中已存在的包(synthetic-tar-safe、synthetic-zip-worker)应被默认排除,不出现在候选表格中。表格后应附一条本地去重提示,说明已排除若干本地已有 finding 的包,并提示可用 --include-known 取消排除。",
1196+
"files": [
1197+
"evals/golden/local-dedup.md"
1198+
],
1199+
"assertions": [
1200+
{
1201+
"text": "输出数量在 8-12 个项目之间,尊重 --count 10",
1202+
"type": "count_matches_flag"
1203+
},
1204+
{
1205+
"text": "所有项目都是 npm/Node.js 生态",
1206+
"type": "ecosystem_correct"
1207+
},
1208+
{
1209+
"text": "至少 70% 的项目漏洞方向涉及 Path Traversal 或 Zip Slip",
1210+
"type": "vuln_focus_relevant"
1211+
},
1212+
{
1213+
"text": "每个高排名项目说明 source、sink 和 guard 是否缺失或待确认",
1214+
"type": "source_sink_guard_present"
1215+
},
1216+
{
1217+
"text": "输出包含本地去重提示,并提到 --include-known 可取消排除",
1218+
"type": "local_dedup_footer_present"
1219+
},
1220+
{
1221+
"text": "工作区已有的包(synthetic-tar-safe、synthetic-zip-worker)未出现在候选表格中",
1222+
"type": "excluded_package_not_listed"
1223+
},
1224+
{
1225+
"text": "包含数据新鲜度与限制说明",
1226+
"type": "freshness_and_uncertainty_present"
1227+
},
1228+
{
1229+
"text": "没有编造任何项目、仓库链接或 stars 数据",
1230+
"type": "no_fabricated_data"
1231+
}
1232+
]
11911233
}
11921234
]
11931235
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
| 排名 | 项目 | 生态 | 仓库 | Registry | Stars | 最近维护 | 规模估计 | 输入面 | 风险证据 | 可能方向 | 推荐切入 | 评分 |
2+
|---|---|---|---|---|---:|---|---|---|---|---|---|---|
3+
| 1 | synthetic-zip-stream | npm | https://github.com/example/synthetic-zip-stream | npmjs.com/package/synthetic-zip-stream | 420 | 2026-04 | cloc: 9k LOC | archive entry names | source zip entry name -> sink fs.createWriteStream in lib/extract.js#writeStream -> guard missing base-prefix check | Path Traversal / Zip Slip | lib/extract.js#writeStream | 79/100 |
4+
| 2 | synthetic-archive-guard | npm | https://github.com/example/synthetic-archive-guard | npmjs.com/package/synthetic-archive-guard | 300 | 2026-03 | tokei: 6k LOC | tar/zip entry path | source entry.path -> sink outputFile() in src/io.js#saveEntry -> guard normalize() present but order 未确认 | Path Traversal | src/io.js#saveEntry | 74/100 |
5+
| 3 | synthetic-tar-entry | npm | https://github.com/example/synthetic-tar-entry | npmjs.com/package/synthetic-tar-entry | 180 | 2026-02 | find/wc: 4k LOC | tar header name | source header.name -> sink fs.writeFile in extract.js#unpack -> guard sanitize() called after join | Path Traversal / Zip Slip | extract.js#unpack | 70/100 |
6+
| 4 | synthetic-untar-cli | npm | https://github.com/example/synthetic-untar-cli | npmjs.com/package/synthetic-untar-cli | 95 | 2026-01 | GitHub estimate: 3k LOC | CLI archive path | source argv path -> sink createWriteStream in cli/extract.js -> guard 未确认 | Path Traversal | cli/extract.js | 63/100 |
7+
| 5 | synthetic-zip-async | npm | https://github.com/example/synthetic-zip-async | npmjs.com/package/synthetic-zip-async | 70 | 2025-12 | cloc: 5k LOC | zip entry name | source entry.fileName -> sink writeEntry in lib/zip.js#writeEntry -> guard checkPath() bypassable on absolute path | Zip Slip | lib/zip.js#writeEntry | 61/100 |
8+
| 6 | synthetic-gzip-pipe | npm | https://github.com/example/synthetic-gzip-pipe | npmjs.com/package/synthetic-gzip-pipe | 55 | 2025-11 | tokei: 2k LOC | gzipped tar name | source stream name -> sink fs.write in src/stream.js -> guard resolve() applied after extraction | Path Traversal | src/stream.js | 58/100 |
9+
| 7 | synthetic-extract-dir | npm | https://github.com/example/synthetic-extract-dir | npmjs.com/package/synthetic-extract-dir | 40 | 2025-10 | find/wc: 2k LOC | archive entry path | source entry.path -> sink mkdirSync/writeFile in lib/dir.js -> guard 未确认 | Path Traversal | lib/dir.js | 52/100 |
10+
| 8 | synthetic-unzip-node | npm | https://github.com/example/synthetic-unzip-node | npmjs.com/package/synthetic-unzip-node | 30 | 2025-09 | GitHub estimate: 1k LOC | zip entry name | source entry.name -> sink writeFile in extract/unzip.js -> guard basename-only check | Zip Slip | extract/unzip.js | 49/100 |
11+
12+
**审计建议**
13+
14+
- synthetic-zip-stream: entry lib/extract.js#writeStream; local test feeds a zip fixture with `../` and absolute path entries, then asserts the resolved output path stays inside the target dir.
15+
- synthetic-archive-guard: entry src/io.js#saveEntry; compare the order of normalize() vs the base-prefix check with a traversal fixture; confirm guard runs before the write.
16+
- synthetic-tar-entry: entry extract.js#unpack; trace whether sanitize() runs before path join; add a unit case with a tar entry containing `../../`.
17+
18+
**数据新鲜度与限制**
19+
20+
验证日期: 2026-06-18. Sources used: sanitized fixture registry/source metadata and local source snippets. 星数与维护时间为 sanitized 测试数据,非真实抓取。`未确认` 字段单独列出,未从记忆推断。
21+
22+
已排除 2 个本地已有 finding 的包(使用 --include-known 可取消排除)。

skills/omv-find/references/output-contract.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,13 @@ Do not describe live exploitation. Phrase next steps as local review:
101101
- add fuzz cases for parser input
102102
- compare safe/unsafe loader options
103103
- review whether a guard applies before the sink
104+
105+
## Local Dedup Footer
106+
107+
When packages were excluded because they already exist in `.omv/findings/` or `.omv/archive/findings/`, append a one-line note after the table:
108+
109+
```text
110+
已排除 N 个本地已有 finding 的包(使用 --include-known 可取消排除)。
111+
```
112+
113+
If no packages were excluded, omit this line.

skills/omv-find/scripts/check_output.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,15 @@ def count_from_prompt(prompt: str) -> int | None:
182182
return int(match.group(1)) if match else None
183183

184184

185+
def excluded_packages_from_prompt(prompt: str) -> list[str]:
186+
"""Package names listed in `[a, b, c]` inside the prompt are local findings
187+
that should be excluded from the candidate table."""
188+
match = re.search(r"\[([^\]]+)\]", prompt)
189+
if not match:
190+
return []
191+
return [p.strip() for p in match.group(1).split(",") if p.strip()]
192+
193+
185194
def expected_range(prompt: str) -> tuple[int, int] | None:
186195
count = count_from_prompt(prompt)
187196
if count is None:
@@ -297,6 +306,14 @@ def check(assertion_type: str, text: str, prompt: str, assertion_text: str = "")
297306
return contains_any(lowered, ["diff-alert", "diff signal", "recent commit", "release", "changelog", "changed file", "近期提交", "变更"])
298307
if assertion_type == "pattern_pack_present":
299308
return contains_any(lowered, ["playbook", "pattern pack", "archive-extractor", "renderer-pipeline", "template-engine", "config-loader", "media-tool", "webhook-client", "upload-handler"])
309+
if assertion_type == "local_dedup_footer_present":
310+
return contains_any(lowered, ["已排除", "本地已有"]) and "--include-known" in lowered
311+
if assertion_type == "excluded_package_not_listed":
312+
rows_text = "\n".join(table_row_lines(text)).lower()
313+
excluded = excluded_packages_from_prompt(prompt)
314+
if not excluded:
315+
return True
316+
return all(pkg.lower() not in rows_text for pkg in excluded)
300317
if assertion_type == "sparse_result_behavior_present":
301318
return rows <= 3 or contains_any(lowered, ["sparse", "不足", "没有硬凑", "few strong candidates"])
302319
if assertion_type in {"non_exploitative_guidance", "no_fabricated_data"}:

0 commit comments

Comments
 (0)