dsh-vet 0.2.6 → 0.3.0

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 CHANGED
@@ -6,10 +6,12 @@ Security vetting for DeepSeek Harness (DSH) plugins: permission & supply-chain
6
6
  audits before install, graded via the open [`dsh-vet/v1`](docs/dsh-vet-v1.md)
7
7
  report standard.
8
8
 
9
- > **Status: v0.1 shipped.** [`dsh-vet@0.1.0` is live on npm](https://www.npmjs.com/package/dsh-vet)
10
- > reference scanner, 15 calibrated rules, public rule rationales, and an
11
- > 11-package ecosystem sweep record. v0.2 (author-side CI + badges) is next
12
- > on the [roadmap](ROADMAP.md).
9
+ > **Status: v0.3 underway.** v0.2 shipped the author side reference
10
+ > scanner ([npm](https://www.npmjs.com/package/dsh-vet), 16 calibrated rules
11
+ > with public rationales), CI Action, and auditable grade badges live in two
12
+ > repos. v0.3 is the ecosystem round: report validation for consumers, the
13
+ > verified-emitter program, and marketplace adoption before the contract's
14
+ > formal freeze ([roadmap](ROADMAP.md)).
13
15
 
14
16
  ## Install
15
17
 
@@ -31,6 +33,7 @@ npx dsh-vet <specifier> # npm package, git URL, or local path
31
33
  npx dsh-vet --json <specifier> # dsh-vet/v1 report on stdout
32
34
  npx dsh-vet --strict <specifier> # exit 1 on findings >= high (confidence >= medium)
33
35
  npx dsh-vet --rules dep.install-scripts <specifier>
36
+ npx dsh-vet validate <report.json> # check a report against the contract
34
37
  ```
35
38
 
36
39
  Any completed report exits `0` — grades describe findings, they do not gate.
@@ -104,8 +107,12 @@ The differentiating piece is not another scanner — it is
104
107
  deterministic JSON report contract (findings with severity **and confidence**,
105
108
  derived A–F grades) that any scanner may emit and any marketplace, CI job, or
106
109
  UI may consume, in the spirit of the community's `dsh-doctor/v1` contract.
107
- The TypeScript reference types ship from this package; third-party emitters
108
- are welcome and listed here once verified.
110
+ The TypeScript reference types and the reference markdown renderer ship from
111
+ this package; `dsh-vet validate` checks any report against the contract —
112
+ including the derived grade, so a report from an emitter you don't know can't
113
+ forge one. Third-party emitters are welcome and
114
+ [listed once verified](docs/emitters.md); marketplaces can start from
115
+ [docs/adopt-marketplace.md](docs/adopt-marketplace.md).
109
116
 
110
117
  ## How it differs
111
118
 
@@ -127,12 +134,13 @@ rules get re-examined and the rule set gets corrected in public.
127
134
 
128
135
  ## Roadmap
129
136
 
130
- - **v0.1** — contract frozen; reference CLI (`dsh vet <pkg>`) with the four
131
- check families above
137
+ - **v0.1** — contract shipped (stable, additive-only since 0.1.0) + reference
138
+ CLI (`dsh-vet <pkg>`) with the four check families above
132
139
  - **v0.2** — GitHub Action + badge so plugin authors self-audit and publish
133
140
  their grade
134
- - **v0.3** — marketplace integrations render `dsh-vet/v1` reports; contract
135
- adopted by at least one third-party emitter
141
+ - **v0.3** — ecosystem round: `dsh-vet validate` + the verified-emitter
142
+ program, marketplace integrations rendering `dsh-vet/v1` reports, and the
143
+ contract's formal freeze after the feedback round
136
144
 
137
145
  The detailed, trackable plan — task breakdowns, recorded decisions,
138
146
  definitions of done, risks, and kill criteria — lives in
@@ -0,0 +1,75 @@
1
+ # Rendering `dsh-vet/v1` reports in your marketplace
2
+
3
+ A one-pager for marketplace and catalog maintainers. Short version: your
4
+ users currently judge DSH plugins by vibes at install time; a committed
5
+ `dsh-vet/v1` report gives you a grade, a findings table, and filters from
6
+ one JSON file — with zero servers to run and no endorsement implied.
7
+
8
+ ## Why render reports
9
+
10
+ - **Demand already exists.** The community's most-upvoted feature request
11
+ ([deepseek-harness#1115](https://github.com/deepseek-ai/deepseek-harness/discussions/1115))
12
+ asks for marketplace standards and review mechanisms. The official
13
+ marketplace will take time.
14
+ - **Authors publish reports already.** The
15
+ [GitHub Action](https://github.com/rogerdigital/dsh-vet/tree/main/action)
16
+ audits a plugin on every push and publishes the report + badge to a
17
+ `dsh-vet/report` branch, so for adopting repos the data is a raw-file URL
18
+ away — nothing for you to run.
19
+ - **Zero lock-in.** The contract is additive-only, consumers must ignore
20
+ unknown fields, and the grade ships inside the report — you render it, you
21
+ never recompute it. Dropping the integration later loses a column, not
22
+ your site.
23
+ - **Display is not endorsement.** Reports are signals, not verdicts; the
24
+ spec says so, and the badge says so. You surface what the audit found.
25
+
26
+ ## Three integration paths
27
+
28
+ **1. The badge (minutes).** Render a shields.io badge from a report:
29
+
30
+ ```sh
31
+ npx dsh-vet badge <report.json> # → shields endpoint JSON
32
+ ```
33
+
34
+ **2. The findings table (an hour).** Import the reference renderer — the
35
+ same one the GitHub Action uses for PR comments:
36
+
37
+ ```ts
38
+ import { renderMarkdown, validateReport } from 'dsh-vet'
39
+
40
+ const report = await fetch(reportUrl).then((r) => r.json())
41
+ const { ok } = validateReport(report) // never trust an unverified emitter
42
+ if (ok) page.add(renderMarkdown(report, { runUrl: reportUrl }))
43
+ ```
44
+
45
+ **3. Ingestion-time validation (for pipelines).** Reject malformed or
46
+ forged reports before they reach your UI — the validator recomputes the
47
+ grade from the findings, so an emitter cannot assert a grade its evidence
48
+ does not support:
49
+
50
+ ```sh
51
+ npx dsh-vet validate report.json && echo trustworthy-shape
52
+ ```
53
+
54
+ ## Consumer rules (from the spec)
55
+
56
+ - **Ignore unknown fields** — emitters may add `x-`-prefixed extras; tolerate
57
+ them.
58
+ - **Read `summary.grade`; never recompute it.** Grades are derived at emit
59
+ time by contract.
60
+ - **Never present grade `X`** as a plugin's grade — it marks an incomplete
61
+ scan.
62
+ - **List findings, don't re-score them.** Severity and confidence are part
63
+ of the data; a low-confidence finding never lowers a grade, by contract.
64
+
65
+ ## Where reports come from
66
+
67
+ Plugin authors commit them via the [Action](https://github.com/rogerdigital/dsh-vet/tree/main/action)
68
+ (`dsh-vet/report` branch in their repo), or you can run the scanner yourself
69
+ on any npm-installable plugin: `npx dsh-vet --json <specifier>`. Emitters
70
+ you didn't write must pass `dsh-vet validate` first; verified emitters are
71
+ listed in [emitters.md](emitters.md).
72
+
73
+ The contract itself: [dsh-vet-v1.md](dsh-vet-v1.md) — stable, additive-only
74
+ since 0.1.0, freezing after this adoption round. Feedback on it is exactly
75
+ what the freeze round is for: [discussions](https://github.com/rogerdigital/dsh-vet/discussions).
@@ -1,7 +1,11 @@
1
1
  # The `dsh-vet/v1` report contract
2
2
 
3
- Status: **draft**open for community input before v0.1 freezes it.
3
+ Status: **stableadditive-only since v0.1.0.** Every report the 0.1.0
4
+ reference scanner emitted still validates today. The formal freeze is
5
+ announced with v0.3, after the marketplace-feedback round; until then,
6
+ changes are limited to new optional fields and new rule ids.
4
7
  Reference TypeScript types: `src/contract.ts` (shipped from this package).
8
+ Conformance checking: `dsh-vet validate <report.json>`.
5
9
 
6
10
  `dsh-vet/v1` defines a machine-readable audit report for a DeepSeek Harness
7
11
  (DSH) plugin. It is implementation-agnostic: any scanner may emit it, and any
@@ -152,8 +156,9 @@ confidence ≥ `medium` exist, for CI gating.
152
156
 
153
157
  ## Versioning
154
158
 
155
- `/v1` freezes at the v0.1 release of this package. Backward-compatible
156
- additions (new optional fields, new rule ids) stay in `/v1`; semantic changes
157
- get `/v2` with a migration note. Discussion happens in
159
+ `/v1` has been additive-only since the v0.1 release of this package — new
160
+ optional fields and new rule ids stay in `/v1`; semantic changes get `/v2`
161
+ with a migration note. The formal freeze follows the v0.3 marketplace-feedback
162
+ round: afterwards even additive changes land only after public discussion in
158
163
  [GitHub Discussions](https://github.com/rogerdigital/dsh-vet/discussions)
159
164
  and the [dsh-plugin topic](https://github.com/topics/dsh-plugin).
@@ -0,0 +1,55 @@
1
+ # Verified emitters of `dsh-vet/v1`
2
+
3
+ An **emitter** is any tool that produces `dsh-vet/v1` reports — a scanner
4
+ like this one, a CI integration, a marketplace's own analysis pipeline. The
5
+ contract is only useful if consumers can trust reports from emitters they
6
+ did not write, so this page defines what *verified* means and lists the
7
+ emitters that made it.
8
+
9
+ ## The checklist
10
+
11
+ An emitter is listed as verified when it meets every item below. The list is
12
+ deliberately short: the contract carries the structure, this carries the
13
+ honesty.
14
+
15
+ 1. **Structure.** Reports are built through `createReport()` from this
16
+ package, or — for non-TypeScript emitters — every published report passes
17
+ `dsh-vet validate` (`npx dsh-vet validate <report.json>`). Either path
18
+ guarantees the derived summary, the deterministic sort, and well-formed
19
+ rule ids; an emitter that hand-assembles reports and skips validation is
20
+ not verified.
21
+ 2. **Determinism.** Two runs over the same artifact with the same emitter
22
+ version produce identical reports, `scanner.ranAt` aside.
23
+ 3. **Conservative severity.** Findings follow the severity ladder in the
24
+ [spec](dsh-vet-v1.md#severity-definitions); anything that depends on
25
+ runtime values is emitted at `low` confidence (or reduced severity), never
26
+ the reverse. The cost of a false positive is paid by a plugin author.
27
+ 4. **Evidence.** Every finding carries at least one evidence item (`file`,
28
+ and `line` when the emitter has it); snippets are minimal and never
29
+ include secrets.
30
+ 5. **Vendor-prefixed, documented rules.** Third-party rule ids start with
31
+ the vendor's own segment (`acme.eval-detect`) and each rule has a public
32
+ rationale page — a rule nobody can dispute in public is a rule nobody
33
+ should trust.
34
+ 6. **A public dispute channel.** A place authors can contest findings, with
35
+ a visible record of corrections.
36
+ 7. **Honest `scanner` fields.** `name` and `version` identify the emitting
37
+ tool as it actually ran.
38
+
39
+ ## Verification process
40
+
41
+ 1. The emitter's maintainer opens an issue here linking to the emitter and
42
+ 2–3 sample reports against real plugins.
43
+ 2. We run `dsh-vet validate` on the samples and read the vendor rule docs,
44
+ checking severity calibration against the spec ladder (item 3).
45
+ 3. Both sides record the verified version range; listings link to the
46
+ emitter's repo and rule docs. Breaking the checklist later removes the
47
+ listing, with the reason stated in the issue.
48
+
49
+ ## Registry
50
+
51
+ | Emitter | Verified versions | Rules | Notes |
52
+ |---|---|---|---|
53
+ | [`dsh-vet`](https://github.com/rogerdigital/dsh-vet) | 0.1.0 – | [`docs/rules/`](rules/) | the reference emitter; self-audited in [`examples/`](../examples/) |
54
+
55
+ Third-party emitters: none yet — the slot is open.
@@ -0,0 +1,12 @@
1
+ # Outreach drafts
2
+
3
+ Ready-to-send drafts for the v0.3 announcement round. Each file names its
4
+ destination. None are sent yet — send from the maintainer's account, then
5
+ record the thread link in ROADMAP.md v0.3 so feedback has a traceable home.
6
+
7
+ | File | Destination | Depends on |
8
+ |---|---|---|
9
+ | [deepseek-harness-1115-reply.md](deepseek-harness-1115-reply.md) | reply in [deepseek-harness#1115](https://github.com/deepseek-ai/deepseek-harness/discussions/1115) | npm 0.3.0 published |
10
+ | [show-your-plugins-post.md](show-your-plugins-post.md) | community "Show Your Plugins" thread | 0.3.0 published |
11
+ | [awesome-list-pr.md](awesome-list-pr.md) | PR against a dsh/agent awesome-list | — |
12
+ | [dsh-plugin-audit-collab.md](dsh-plugin-audit-collab.md) | issue/DM to dsh-plugin-audit maintainers | — |
@@ -0,0 +1,39 @@
1
+ <!-- Destination: PR against a dsh / agent-skills / security-tools awesome list.
2
+ Swap in the list's entry format (table vs bullet) and category. -->
3
+
4
+ PR title: `Add dsh-vet — pre-install security audits for DSH plugins`
5
+
6
+ ## What
7
+
8
+ [dsh-vet](https://github.com/rogerdigital/dsh-vet) — static security vetting
9
+ for DeepSeek Harness plugins, built around an open report contract:
10
+
11
+ ```markdown
12
+ - [dsh-vet](https://github.com/rogerdigital/dsh-vet) — pre-install permission & supply-chain audits
13
+ for DSH plugins; emits the open `dsh-vet/v1` report (severity + confidence, derived grades),
14
+ ships a CI Action + auditable badge, and validates/renders reports from any conforming emitter.
15
+ ```
16
+
17
+ or, for table-format lists:
18
+
19
+ ```markdown
20
+ | [dsh-vet](https://github.com/rogerdigital/dsh-vet) | CLI · library · CI Action | Pre-install static audits for DSH plugins; emits and verifies the open `dsh-vet/v1` report |
21
+ ```
22
+
23
+ ## Why it fits
24
+
25
+ - Solves a live pain: the community's top feature request is marketplace
26
+ review standards (deepseek-harness#1115); incidents like the Full Access
27
+ home-directory wipe (#461) show the stakes.
28
+ - Not another closed tool: the differentiating artifact is the
29
+ implementation-agnostic report contract — any scanner may emit it, any
30
+ marketplace/UI may consume it, `dsh-vet validate` verifies conformance.
31
+ - Maintained in the open: public per-rule rationales, public
32
+ false-positive dispute process, calibration record against 11 real
33
+ plugins, self-audited with its own scanner, MIT.
34
+
35
+ ## Checks
36
+
37
+ - [ ] MIT licensed, docs and tests present, CI green
38
+ - [x] Installable via npm (`dsh-vet`), runs via `npx`, zero runtime deps
39
+ beyond the parser
@@ -0,0 +1,38 @@
1
+ <!-- Destination: reply in deepseek-ai/deepseek-harness discussion #1115
2
+ (marketplace standards / review mechanisms). Send after 0.3.0 is on npm. -->
3
+
4
+ Sharing what we built against exactly this problem, in case it's useful
5
+ before an official marketplace lands.
6
+
7
+ The core idea: "should I install this plugin?" needs a **shared,
8
+ machine-readable answer**, not per-tool vibes. So the contract came first —
9
+ [`dsh-vet/v1`](https://github.com/rogerdigital/dsh-vet/blob/main/docs/dsh-vet-v1.md),
10
+ an implementation-agnostic audit report (findings with severity *and*
11
+ confidence, derived A–F grades), following the pattern `dsh-doctor/v1`
12
+ proved: freeze a small boring contract, compete on implementations.
13
+
14
+ On top of it:
15
+
16
+ - **A reference scanner** ([dsh-vet on npm](https://www.npmjs.com/package/dsh-vet)) —
17
+ static, deterministic, never installs or transmits the audited plugin.
18
+ 16 rules across capability seams, supply chain, obfuscation, and data
19
+ egress; every rule has a public rationale page and a public dispute
20
+ template. Calibrated against 11 real ecosystem plugins
21
+ ([sweep record](https://github.com/rogerdigital/dsh-vet/blob/main/docs/calibration-v0.1.md)).
22
+ - **Author-side CI** — a GitHub Action that audits on every push and
23
+ publishes the report + grade badge from the plugin's own repo (zero
24
+ server; the badge value is auditable through git history). Live in two
25
+ repos already, including the scanner's own self-audit.
26
+ - **A consumer story** — `dsh-vet validate` checks any report against the
27
+ contract (grade is recomputed from findings, so an emitter can't forge
28
+ one), and `renderMarkdown` gives marketplaces/UIs the same rendering the
29
+ Action uses: [adopting it](https://github.com/rogerdigital/dsh-vet/blob/main/docs/adopt-marketplace.md).
30
+
31
+ The contract has been additive-only since 0.1.0 — every report the first
32
+ release emitted still validates today. **We're actively seeking feedback
33
+ from marketplace and tool maintainers before formally freezing `/v1`.** If
34
+ you're building a catalog or review layer, this round is exactly for you:
35
+ what would you need the report to carry?
36
+
37
+ Happy to go deeper on any piece — severity calibration, false-positive
38
+ handling, or the freeze criteria.
@@ -0,0 +1,37 @@
1
+ <!-- Destination: issue on dsh-plugin-audit's repo (or DM to its maintainers).
2
+ Tone: peer-to-peer, one concrete ask (a conversation), no commitment demanded. -->
3
+
4
+ Hi — maintainer of [dsh-vet](https://github.com/rogerdigital/dsh-vet) here.
5
+ Your runtime-sentinel work is in our README's comparison table as the
6
+ complementary half of this problem, and I'd like to make that explicit
7
+ instead of just documented on our side.
8
+
9
+ The shape of it:
10
+
11
+ - **dsh-vet** answers *"what does this plugin do?"* **before install** —
12
+ static analysis, emitted as the open
13
+ [`dsh-vet/v1`](https://github.com/rogerdigital/dsh-vet/blob/main/docs/dsh-vet-v1.md)
14
+ report (findings with severity + confidence, derived grades, evidence per
15
+ finding).
16
+ - **dsh-plugin-audit** answers *"what is it doing right now?"* at runtime —
17
+ permission profiling and sentinels.
18
+
19
+ Static-before-install and runtime-during-use cover different failure modes
20
+ (obfuscated payloads vs. behavior that only emerges live), which is why we
21
+ list you as complementary rather than competing — and why we'd rather
22
+ coordinate than FUD.
23
+
24
+ Two things that might be cheap and useful, if you're interested:
25
+
26
+ 1. **Shared vocabulary.** Our report contract deliberately leaves rule ids
27
+ open-ended (`acme.eval-detect` style vendor prefixes). If your runtime
28
+ findings ever want a common shape — severity/confidence semantics,
29
+ evidence, dispute channels — the contract is additive-only and open for
30
+ feedback before its formal freeze. You'd be the most natural co-author
31
+ of whatever `/v1` learns from runtime auditing.
32
+ 2. **Cross-linking.** We already point to you as the runtime half; a link
33
+ back from your side (if you find the pre-install half useful) would let
34
+ users find the full stack.
35
+
36
+ No ask beyond a conversation — if either half sounds useful, I'm happy to
37
+ open a discussion with concrete details.
@@ -0,0 +1,41 @@
1
+ <!-- Destination: a community "Show Your Plugins" / project showcase thread.
2
+ Adjust the opening line to the thread's framing. Send after 0.3.0 is on npm. -->
3
+
4
+ **dsh-vet** — know what a plugin does *before* you install it.
5
+
6
+ DSH's everything-is-a-plugin model is its superpower and its attack surface:
7
+ an installed plugin can touch your filesystem, spawn processes, and open
8
+ network connections. After the incident where a Full Access session wiped a
9
+ user's home directory, "should I install this?" deserved better than vibes.
10
+
11
+ dsh-vet audits any npm-installable DSH plugin — statically, locally, without
12
+ installing or executing it — and emits an open, deterministic
13
+ [`dsh-vet/v1`](https://github.com/rogerdigital/dsh-vet/blob/main/docs/dsh-vet-v1.md)
14
+ report: findings with severity **and** confidence, and a derived A–F grade
15
+ where a low-confidence finding never counts against you.
16
+
17
+ ```sh
18
+ npx dsh-vet <plugin> # human summary
19
+ npx dsh-vet --json <plugin> # full dsh-vet/v1 report
20
+ ```
21
+
22
+ What makes it defensible rather than noisy:
23
+
24
+ - **Every rule has a public rationale** and a public false-positive dispute
25
+ template; disputed rules get corrected in the open (it has happened, and
26
+ the changelog shows it).
27
+ - **Calibrated on real plugins** — an 11-package sweep record is published,
28
+ including grades and the two tunings the sweep forced.
29
+ - **It audits itself** — the repo's own grade badge is generated from the
30
+ exact tarball that ships to npm, and every signal the scanner finds in
31
+ itself is in the published report.
32
+
33
+ For plugin authors: a [GitHub Action](https://github.com/rogerdigital/dsh-vet/tree/main/action)
34
+ audits on every push and publishes your grade badge from your own repo —
35
+ no badge service, the value is auditable through your git history. For
36
+ catalog/UI builders: [`dsh-vet validate`](https://github.com/rogerdigital/dsh-vet/blob/main/docs/adopt-marketplace.md)
37
+ ingests and verifies reports from any conforming emitter.
38
+
39
+ The contract is additive-only since 0.1.0 and open for feedback before the
40
+ formal freeze. Repo: [rogerdigital/dsh-vet](https://github.com/rogerdigital/dsh-vet) —
41
+ criticism on severity calibration is genuinely welcome.
package/lib/index.d.mts CHANGED
@@ -88,6 +88,8 @@ interface VetReport {
88
88
  readonly summary: VetSummary;
89
89
  readonly findings: readonly VetFinding[];
90
90
  }
91
+ /** Sort rank per severity, worst first; the contract's deterministic order. */
92
+ declare const SEVERITY_RANK: Record<VetSeverity, number>;
91
93
  /**
92
94
  * Rule ids are two or more dot-separated lowercase segments
93
95
  * (`perm.broad-fs-write`, `acme.eval-detect`). Deliberately open-ended about
@@ -271,7 +273,7 @@ declare function runRules(analysis: Analysis, only?: string[]): VetFinding[];
271
273
  //#endregion
272
274
  //#region src/scanner.d.ts
273
275
  /** Kept in lockstep with package.json; a test asserts they match. */
274
- declare const SCANNER_VERSION = "0.2.6";
276
+ declare const SCANNER_VERSION = "0.3.0";
275
277
  interface ScanOptions extends ResolveOptions {
276
278
  /** Injectable clock for deterministic tests/reports. */
277
279
  now?: () => string;
@@ -291,6 +293,43 @@ interface ShieldsEndpointBadge {
291
293
  }
292
294
  declare function renderBadge(report: VetReport): ShieldsEndpointBadge;
293
295
  //#endregion
296
+ //#region src/render.d.ts
297
+ interface RenderMarkdownOptions {
298
+ /** Link to the CI run or page hosting the full report. */
299
+ runUrl: string;
300
+ }
301
+ declare function renderMarkdown(report: VetReport, options: RenderMarkdownOptions): string;
302
+ //#endregion
303
+ //#region src/validate.d.ts
304
+ /**
305
+ * Structural validation for `dsh-vet/v1` reports (ROADMAP v0.3).
306
+ *
307
+ * Marketplaces and CI jobs receive reports from scanners they did not write.
308
+ * This module checks a report against the contract without trusting the
309
+ * emitter: every normative statement in `docs/dsh-vet-v1.md` is enforced —
310
+ * field types, enums, rule-id shape, evidence presence, the derived summary,
311
+ * and the deterministic sort order. Unknown fields are ignored (design
312
+ * rule 4). The derived-summary check is the load-bearing one: an emitter
313
+ * cannot assert a flattering grade that its findings do not support.
314
+ *
315
+ * @module dsh-vet/validate
316
+ */
317
+ interface ValidationIssue {
318
+ /** Dotted path into the report, e.g. `findings[2].evidence[0].line`. */
319
+ path: string;
320
+ message: string;
321
+ }
322
+ interface ValidationResult {
323
+ ok: boolean;
324
+ issues: ValidationIssue[];
325
+ }
326
+ /**
327
+ * Validate a parsed report against the `dsh-vet/v1` contract. Collects every
328
+ * issue instead of failing on the first, so an emitter sees its full repair
329
+ * list in one pass.
330
+ */
331
+ declare function validateReport(report: unknown): ValidationResult;
332
+ //#endregion
294
333
  //#region src/cli.d.ts
295
334
  /**
296
335
  * CLI (ROADMAP T4). Exit semantics per the dsh-vet/v1 spec: `0` for any
@@ -304,4 +343,4 @@ interface CliIo {
304
343
  /** Parse and run; returns the process exit code. */
305
344
  declare function runCli(argv: string[], io: CliIo): Promise<number>;
306
345
  //#endregion
307
- export { type Analysis, type Capability, type CapabilityUse, type CharcodeCall, type CliIo, type CreateReportInput, type DynamicImportUse, type EncodedLiteral, type EvalUse, type FindingInit, type ImportRef, type NetUse, type PkgJson, RULES, RULE_ID_PATTERN, type ResolveOptions, type ResolvedTarget, type Rule, type RuleContext, SCANNER_VERSION, SCHEMA_ID, type ScanOptions, type ShieldsEndpointBadge, type SourceFile, type SpecifierKind, type VetConfidence, VetError, type VetEvidence, type VetFinding, type VetGrade, type VetReport, type VetScanner, type VetSeverity, type VetSummary, type VetSummaryCounts, type VetTarget, type VetTargetKind, analyze, classifySpecifier, countFindings, createReport, gradeFor, isGraded, parseNpmSpecifier, reachableFrom, renderBadge, resolveTarget, ruleIds, runCli, runRules, scan, scanDirectory };
346
+ export { type Analysis, type Capability, type CapabilityUse, type CharcodeCall, type CliIo, type CreateReportInput, type DynamicImportUse, type EncodedLiteral, type EvalUse, type FindingInit, type ImportRef, type NetUse, type PkgJson, RULES, RULE_ID_PATTERN, type RenderMarkdownOptions, type ResolveOptions, type ResolvedTarget, type Rule, type RuleContext, SCANNER_VERSION, SCHEMA_ID, SEVERITY_RANK, type ScanOptions, type ShieldsEndpointBadge, type SourceFile, type SpecifierKind, type ValidationIssue, type ValidationResult, type VetConfidence, VetError, type VetEvidence, type VetFinding, type VetGrade, type VetReport, type VetScanner, type VetSeverity, type VetSummary, type VetSummaryCounts, type VetTarget, type VetTargetKind, analyze, classifySpecifier, countFindings, createReport, gradeFor, isGraded, parseNpmSpecifier, reachableFrom, renderBadge, renderMarkdown, resolveTarget, ruleIds, runCli, runRules, scan, scanDirectory, validateReport };
package/lib/index.mjs CHANGED
@@ -23,6 +23,7 @@ import { parseArgs } from "node:util";
23
23
  */
24
24
  /** Literal `schema` value every dsh-vet/v1 report carries. */
25
25
  const SCHEMA_ID = "dsh-vet/v1";
26
+ /** Sort rank per severity, worst first; the contract's deterministic order. */
26
27
  const SEVERITY_RANK = {
27
28
  critical: 0,
28
29
  high: 1,
@@ -1603,7 +1604,7 @@ function runRules(analysis, only) {
1603
1604
  * over the same artifact with the same version are identical.
1604
1605
  */
1605
1606
  /** Kept in lockstep with package.json; a test asserts they match. */
1606
- const SCANNER_VERSION = "0.2.6";
1607
+ const SCANNER_VERSION = "0.3.0";
1607
1608
  /**
1608
1609
  * A scan that audited zero JavaScript files must not read as a clean pass —
1609
1610
  * a TypeScript source tree scanned as a local path has no `.js` to analyze,
@@ -1681,6 +1682,218 @@ function renderBadge(report) {
1681
1682
  };
1682
1683
  }
1683
1684
  //#endregion
1685
+ //#region src/render.ts
1686
+ function renderMarkdown(report, options) {
1687
+ const c = report.summary.counts;
1688
+ const lines = [
1689
+ "<!-- dsh-vet:pr-comment -->",
1690
+ "## dsh-vet report",
1691
+ "",
1692
+ `**Grade: ${report.summary.grade}** · audited \`${report.target.specifier}\` · [run](${options.runUrl}) · report uploaded as the \`dsh-vet-report\` artifact`,
1693
+ "",
1694
+ `| critical | high | medium | low | info |`,
1695
+ `| --- | --- | --- | --- | --- |`,
1696
+ `| ${c.critical} | ${c.high} | ${c.medium} | ${c.low} | ${c.info} |`,
1697
+ ""
1698
+ ];
1699
+ const graded = report.findings.filter((f) => f.severity !== "info");
1700
+ const info = report.findings.filter((f) => f.severity === "info");
1701
+ if (report.findings.length === 0) lines.push("No findings.");
1702
+ if (graded.length > 0) {
1703
+ lines.push("### Findings", "");
1704
+ for (const f of graded.slice(0, 10)) {
1705
+ lines.push(`- **[${f.severity[0].toUpperCase()}] ${f.id}** — ${f.title} (\`${f.confidence}\` confidence)`);
1706
+ for (const e of f.evidence.slice(0, 3)) {
1707
+ const where = e.line ? `${e.file}:${e.line}` : e.file;
1708
+ lines.push(` - \`${where}\`${e.snippet ? ` — \`${e.snippet}\`` : ""}`);
1709
+ }
1710
+ }
1711
+ if (graded.length > 10) lines.push(`- …and ${graded.length - 10} more in the report artifact`);
1712
+ lines.push("");
1713
+ }
1714
+ if (info.length > 0) {
1715
+ lines.push(`<details><summary>${info.length} info findings (never affect the grade)</summary>`, "");
1716
+ for (const f of info.slice(0, 8)) lines.push(`- **${f.id}** — ${f.title}`);
1717
+ if (info.length > 8) lines.push(`- …and ${info.length - 8} more`);
1718
+ lines.push("</details>", "");
1719
+ }
1720
+ lines.push("---", "Findings are signals, not verdicts — a low-confidence finding never lowers a grade. A finding you believe is wrong? [Open a public dispute](https://github.com/rogerdigital/dsh-vet/issues/new?template=false-positive.md).");
1721
+ return lines.join("\n");
1722
+ }
1723
+ //#endregion
1724
+ //#region src/validate.ts
1725
+ /**
1726
+ * Structural validation for `dsh-vet/v1` reports (ROADMAP v0.3).
1727
+ *
1728
+ * Marketplaces and CI jobs receive reports from scanners they did not write.
1729
+ * This module checks a report against the contract without trusting the
1730
+ * emitter: every normative statement in `docs/dsh-vet-v1.md` is enforced —
1731
+ * field types, enums, rule-id shape, evidence presence, the derived summary,
1732
+ * and the deterministic sort order. Unknown fields are ignored (design
1733
+ * rule 4). The derived-summary check is the load-bearing one: an emitter
1734
+ * cannot assert a flattering grade that its findings do not support.
1735
+ *
1736
+ * @module dsh-vet/validate
1737
+ */
1738
+ const SEVERITIES = [
1739
+ "critical",
1740
+ "high",
1741
+ "medium",
1742
+ "low",
1743
+ "info"
1744
+ ];
1745
+ const CONFIDENCES = [
1746
+ "high",
1747
+ "medium",
1748
+ "low"
1749
+ ];
1750
+ const GRADES = [
1751
+ "A",
1752
+ "B",
1753
+ "C",
1754
+ "D",
1755
+ "F",
1756
+ "X"
1757
+ ];
1758
+ const TARGET_KINDS = [
1759
+ "npm-package",
1760
+ "git-repo",
1761
+ "local-path"
1762
+ ];
1763
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
1764
+ function isObject(value) {
1765
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1766
+ }
1767
+ function show(value) {
1768
+ if (typeof value === "string") return JSON.stringify(value);
1769
+ if (Array.isArray(value)) return "an array";
1770
+ if (isObject(value)) return "an object";
1771
+ return String(value);
1772
+ }
1773
+ function isInt(value) {
1774
+ return typeof value === "number" && Number.isInteger(value);
1775
+ }
1776
+ /**
1777
+ * Validate a parsed report against the `dsh-vet/v1` contract. Collects every
1778
+ * issue instead of failing on the first, so an emitter sees its full repair
1779
+ * list in one pass.
1780
+ */
1781
+ function validateReport(report) {
1782
+ const issues = [];
1783
+ const fail = (path, message) => {
1784
+ issues.push({
1785
+ path,
1786
+ message
1787
+ });
1788
+ };
1789
+ if (!isObject(report)) return {
1790
+ ok: false,
1791
+ issues: [{
1792
+ path: "report",
1793
+ message: `must be an object, got ${show(report)}`
1794
+ }]
1795
+ };
1796
+ if (report.schema !== "dsh-vet/v1") fail("schema", `must be ${JSON.stringify(SCHEMA_ID)}, got ${show(report.schema)}`);
1797
+ if (!isObject(report.target)) fail("target", `must be an object, got ${show(report.target)}`);
1798
+ else {
1799
+ const target = report.target;
1800
+ if (!TARGET_KINDS.includes(target.kind)) fail("target.kind", `must be one of ${TARGET_KINDS.map((k) => JSON.stringify(k)).join(" | ")}, got ${show(target.kind)}`);
1801
+ if (typeof target.specifier !== "string") fail("target.specifier", `must be a string, got ${show(target.specifier)}`);
1802
+ if (target.resolved !== void 0) {
1803
+ if (!isObject(target.resolved)) fail("target.resolved", `must be an object, got ${show(target.resolved)}`);
1804
+ else for (const key of [
1805
+ "version",
1806
+ "commit",
1807
+ "integrity"
1808
+ ]) {
1809
+ const value = target.resolved[key];
1810
+ if (value !== void 0 && typeof value !== "string") fail(`target.resolved.${key}`, `must be a string, got ${show(value)}`);
1811
+ }
1812
+ }
1813
+ }
1814
+ if (!isObject(report.scanner)) fail("scanner", `must be an object, got ${show(report.scanner)}`);
1815
+ else {
1816
+ const scanner = report.scanner;
1817
+ if (typeof scanner.name !== "string") fail("scanner.name", `must be a string, got ${show(scanner.name)}`);
1818
+ if (typeof scanner.version !== "string") fail("scanner.version", `must be a string, got ${show(scanner.version)}`);
1819
+ if (typeof scanner.ranAt !== "string" || !RFC3339.test(scanner.ranAt) || Number.isNaN(Date.parse(scanner.ranAt))) fail("scanner.ranAt", `must be an RFC 3339 timestamp, got ${show(scanner.ranAt)}`);
1820
+ }
1821
+ let findingsShapeOk = false;
1822
+ if (!Array.isArray(report.findings)) fail("findings", `must be an array, got ${show(report.findings)}`);
1823
+ else {
1824
+ findingsShapeOk = true;
1825
+ report.findings.forEach((raw, i) => {
1826
+ const at = `findings[${i}]`;
1827
+ if (!isObject(raw)) {
1828
+ fail(at, `must be an object, got ${show(raw)}`);
1829
+ findingsShapeOk = false;
1830
+ return;
1831
+ }
1832
+ const finding = raw;
1833
+ if (typeof finding.id !== "string" || !RULE_ID_PATTERN.test(finding.id)) fail(`${at}.id`, `must match ${RULE_ID_PATTERN} (vendor rule sets prefix their own segment, e.g. acme.eval-detect), got ${show(finding.id)}`);
1834
+ if (typeof finding.title !== "string") fail(`${at}.title`, `must be a string, got ${show(finding.title)}`);
1835
+ if (!SEVERITIES.includes(finding.severity)) {
1836
+ fail(`${at}.severity`, `must be one of ${SEVERITIES.map((s) => JSON.stringify(s)).join(" | ")}, got ${show(finding.severity)}`);
1837
+ findingsShapeOk = false;
1838
+ }
1839
+ if (!CONFIDENCES.includes(finding.confidence)) {
1840
+ fail(`${at}.confidence`, `must be one of ${CONFIDENCES.map((c) => JSON.stringify(c)).join(" | ")}, got ${show(finding.confidence)}`);
1841
+ findingsShapeOk = false;
1842
+ }
1843
+ if (!Array.isArray(finding.evidence) || finding.evidence.length === 0) fail(`${at}.evidence`, "must be a non-empty array");
1844
+ else finding.evidence.forEach((ev, j) => {
1845
+ const evAt = `${at}.evidence[${j}]`;
1846
+ if (!isObject(ev)) {
1847
+ fail(evAt, `must be an object, got ${show(ev)}`);
1848
+ return;
1849
+ }
1850
+ if (typeof ev.file !== "string") fail(`${evAt}.file`, `must be a string, got ${show(ev.file)}`);
1851
+ if (ev.line !== void 0 && (!isInt(ev.line) || ev.line < 1)) fail(`${evAt}.line`, `must be a positive integer, got ${show(ev.line)}`);
1852
+ if (ev.snippet !== void 0 && typeof ev.snippet !== "string") fail(`${evAt}.snippet`, `must be a string, got ${show(ev.snippet)}`);
1853
+ if (ev.note !== void 0 && typeof ev.note !== "string") fail(`${evAt}.note`, `must be a string, got ${show(ev.note)}`);
1854
+ });
1855
+ if (finding.remediation !== void 0 && typeof finding.remediation !== "string") fail(`${at}.remediation`, `must be a string, got ${show(finding.remediation)}`);
1856
+ if (finding.references !== void 0) {
1857
+ if (!Array.isArray(finding.references) || finding.references.some((ref) => typeof ref !== "string")) fail(`${at}.references`, `must be an array of strings, got ${show(finding.references)}`);
1858
+ }
1859
+ });
1860
+ }
1861
+ if (!isObject(report.summary)) fail("summary", `must be an object, got ${show(report.summary)}`);
1862
+ else {
1863
+ const summary = report.summary;
1864
+ if (!GRADES.includes(summary.grade)) fail("summary.grade", `must be one of ${GRADES.join(" | ")}, got ${show(summary.grade)}`);
1865
+ if (!isObject(summary.counts)) fail("summary.counts", `must be an object, got ${show(summary.counts)}`);
1866
+ else for (const severity of SEVERITIES) {
1867
+ const count = summary.counts[severity];
1868
+ if (!isInt(count) || count < 0) fail(`summary.counts.${severity}`, `must be a non-negative integer, got ${show(count)}`);
1869
+ }
1870
+ }
1871
+ if (findingsShapeOk && Array.isArray(report.findings) && isObject(report.summary) && isObject(report.summary.counts)) {
1872
+ const findings = report.findings;
1873
+ const derivedCounts = countFindings(findings);
1874
+ for (const severity of SEVERITIES) if (report.summary.counts[severity] !== derivedCounts[severity]) fail(`summary.counts.${severity}`, `must match the findings (${report.summary.counts[severity]} asserted, ${derivedCounts[severity]} derived) — the summary is always derived, never asserted`);
1875
+ const grade = report.summary.grade;
1876
+ if (grade !== "X") {
1877
+ const derivedGrade = gradeFor(findings);
1878
+ if (grade !== derivedGrade) fail("summary.grade", `${show(grade)} is asserted but the findings derive ${JSON.stringify(derivedGrade)} — the summary is always derived, never asserted`);
1879
+ }
1880
+ for (let i = 1; i < report.findings.length; i++) {
1881
+ const prev = report.findings[i - 1];
1882
+ const cur = report.findings[i];
1883
+ const prevRank = SEVERITY_RANK[prev.severity];
1884
+ const curRank = SEVERITY_RANK[cur.severity];
1885
+ if (prevRank > curRank || prevRank === curRank && String(prev.id).localeCompare(String(cur.id)) > 0) {
1886
+ fail("findings", `must be sorted worst severity first then id ascending; ${show(cur.id)} follows ${show(prev.id)}`);
1887
+ break;
1888
+ }
1889
+ }
1890
+ }
1891
+ return {
1892
+ ok: issues.length === 0,
1893
+ issues
1894
+ };
1895
+ }
1896
+ //#endregion
1684
1897
  //#region src/cli.ts
1685
1898
  /**
1686
1899
  * CLI (ROADMAP T4). Exit semantics per the dsh-vet/v1 spec: `0` for any
@@ -1689,6 +1902,7 @@ function renderBadge(report) {
1689
1902
  */
1690
1903
  const USAGE = `usage: dsh-vet <specifier> [options]
1691
1904
  dsh-vet badge <report.json>
1905
+ dsh-vet validate <report.json> [<report.json> ...]
1692
1906
 
1693
1907
  specifier npm package (name[@version]), git URL, or local path
1694
1908
 
@@ -1701,7 +1915,11 @@ const USAGE = `usage: dsh-vet <specifier> [options]
1701
1915
 
1702
1916
  badge render a shields.io endpoint badge (JSON) from a
1703
1917
  dsh-vet/v1 report file; used by CI to publish a grade
1704
- badge whose value is the committed report`;
1918
+ badge whose value is the committed report
1919
+
1920
+ validate check report file(s) against the dsh-vet/v1 contract;
1921
+ the entry point for marketplaces and CI jobs consuming
1922
+ reports from scanners they did not write`;
1705
1923
  const SEVERITY_ORDER = [
1706
1924
  "critical",
1707
1925
  "high",
@@ -1737,6 +1955,7 @@ function humanSummary(report) {
1737
1955
  /** Parse and run; returns the process exit code. */
1738
1956
  async function runCli(argv, io) {
1739
1957
  if (argv[0] === "badge") return runBadge(argv.slice(1), io);
1958
+ if (argv[0] === "validate") return runValidate(argv.slice(1), io);
1740
1959
  let args;
1741
1960
  try {
1742
1961
  args = parseArgs({
@@ -1809,5 +2028,36 @@ function runBadge(argv, io) {
1809
2028
  io.stdout(JSON.stringify(renderBadge(report)));
1810
2029
  return 0;
1811
2030
  }
2031
+ /**
2032
+ * `dsh-vet validate <report.json> [...]` — check reports against the
2033
+ * contract. Exit 0 when every file is valid, 1 when any is invalid (the
2034
+ * issues are data, printed for repair), 2 on usage or unreadable files.
2035
+ */
2036
+ function runValidate(argv, io) {
2037
+ const paths = argv.filter((arg) => arg !== "--help");
2038
+ if (paths.length === 0 || argv.includes("--help")) {
2039
+ io.stderr(`usage: dsh-vet validate <report.json> [<report.json> ...]\n\nCheck report file(s) against the dsh-vet/v1 contract.`);
2040
+ return 2;
2041
+ }
2042
+ let worst = 0;
2043
+ for (const path of paths) {
2044
+ let parsed;
2045
+ try {
2046
+ parsed = JSON.parse(readFileSync(path, "utf8"));
2047
+ } catch (err) {
2048
+ io.stderr(`dsh-vet validate: cannot read report: ${err.message}`);
2049
+ return 2;
2050
+ }
2051
+ const { ok, issues } = validateReport(parsed);
2052
+ if (ok) {
2053
+ io.stdout(`ok ${path}`);
2054
+ continue;
2055
+ }
2056
+ worst = 1;
2057
+ io.stdout(`invalid ${path}`);
2058
+ for (const issue of issues) io.stdout(` ${issue.path}: ${issue.message}`);
2059
+ }
2060
+ return worst;
2061
+ }
1812
2062
  //#endregion
1813
- export { RULES, RULE_ID_PATTERN, SCANNER_VERSION, SCHEMA_ID, VetError, analyze, classifySpecifier, countFindings, createReport, gradeFor, isGraded, parseNpmSpecifier, reachableFrom, renderBadge, resolveTarget, ruleIds, runCli, runRules, scan, scanDirectory };
2063
+ export { RULES, RULE_ID_PATTERN, SCANNER_VERSION, SCHEMA_ID, SEVERITY_RANK, VetError, analyze, classifySpecifier, countFindings, createReport, gradeFor, isGraded, parseNpmSpecifier, reachableFrom, renderBadge, renderMarkdown, resolveTarget, ruleIds, runCli, runRules, scan, scanDirectory, validateReport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vet",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Security vetting for DeepSeek Harness (DSH) plugins: permission & supply-chain audits before install, graded via the open dsh-vet/v1 report standard.",
5
5
  "dsh": {
6
6
  "seams": ["fs", "shell", "web"]