dsh-vet 0.1.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.
@@ -0,0 +1,27 @@
1
+ # perm.network-client
2
+
3
+ Opens network connections.
4
+
5
+ ## What it looks for
6
+
7
+ Client-side network calls: `http(s).request/get`, `net.connect`,
8
+ `net.createConnection`, `tls.connect`, `dns.lookup/resolve`,
9
+ `dgram.createSocket`, global `fetch`, `new WebSocket()`.
10
+
11
+ ## Severity / confidence policy
12
+
13
+ - Client calls present, no `"web"` seam declared: **medium / high.**
14
+ - `"web"` declared in `dsh.seams`: **info / high** — expected capability,
15
+ reported for inventory (the endpoints themselves are listed by
16
+ `egress.outbound-endpoints`).
17
+ - Network modules imported with no observable client call: **info / high.**
18
+
19
+ ## False positives
20
+
21
+ Server-only listeners (`http.createServer`) are intentionally not counted.
22
+ Transitive network use inside dependencies is not attributed to the plugin
23
+ here — dependency scanning is the `dep.*` family's business.
24
+
25
+ ## Remediation
26
+
27
+ Declare the web seam in `dsh.seams`, or drop the network access.
@@ -0,0 +1,34 @@
1
+ # perm.seam-mismatch
2
+
3
+ Capability used by the plugin's code but not declared in `dsh.seams`.
4
+
5
+ ## What it looks for
6
+
7
+ Seams are declared in package.json:
8
+
9
+ ```json
10
+ { "dsh": { "seams": ["fs", "web"] } }
11
+ ```
12
+
13
+ Detected capabilities map to seams: `fs`/`fs-write` → `fs`, `child_process` →
14
+ `shell`, net/http/fetch → `web`, `worker_threads` → `workers`. When a
15
+ declaration exists and a detected capability's seam is missing from it, this
16
+ rule reports the first use site per missing seam.
17
+
18
+ ## Severity / confidence policy
19
+
20
+ - **medium / medium.** The capability demonstrably exists; the declaration
21
+ demonstrably omits it. Both are facts; the mismatch is the interpretation.
22
+ - When **no** `dsh.seams` declaration exists at all, this rule stays silent:
23
+ an absent declaration is not a claim, and per-capability rules
24
+ (`perm.subprocess-spawn`, `perm.network-client`) speak instead.
25
+
26
+ ## False positives
27
+
28
+ A plugin that computes its seam set at runtime or documents capabilities in
29
+ README prose only. Dispute with evidence of the declaration and the rule will
30
+ be taught that form.
31
+
32
+ ## Remediation
33
+
34
+ Add the seam to `dsh.seams`, or drop the capability if it is not needed.
@@ -0,0 +1,28 @@
1
+ # perm.subprocess-spawn
2
+
3
+ Spawns subprocesses.
4
+
5
+ ## What it looks for
6
+
7
+ `child_process` call sites: `spawn`, `exec`, `execFile`, `fork` and their
8
+ `*Sync` variants, reached through any import style (ESM named/default, CJS
9
+ `require`, destructuring).
10
+
11
+ ## Severity / confidence policy
12
+
13
+ - Spawn call sites present, no `"shell"` seam declared: **medium / high.**
14
+ The call is a fact; whether it is justified is the reader's call.
15
+ - `"shell"` declared in `dsh.seams`: **info / high** — expected capability,
16
+ reported for inventory.
17
+ - `child_process` imported but never called: **info / high** (dead import,
18
+ not an exercised capability).
19
+
20
+ ## False positives
21
+
22
+ Plugins whose whole purpose is running commands (a shell helper, a runner).
23
+ Declaring the `shell` seam turns the finding into inventory; the honest fix
24
+ is the declaration, not the report's silence.
25
+
26
+ ## Remediation
27
+
28
+ Declare the shell seam in `dsh.seams`, or avoid spawning processes.
@@ -0,0 +1,32 @@
1
+ # perm.undeclared-fs-write
2
+
3
+ Writes or deletes files outside any plausible plugin scope.
4
+
5
+ ## What it looks for
6
+
7
+ Calls to write/delete filesystem APIs (`writeFile`, `rm`, `unlink`,
8
+ `truncate`, `rename`, …) in shipped JS. Literal absolute paths **outside** the
9
+ plausible scope — the temp directory and the DSH home subtree
10
+ (`~/.dsh`, `~/.config/dsh`, `~/.cache/dsh`) — are flagged. Relative paths and
11
+ in-scope paths are not.
12
+
13
+ ## Severity / confidence policy
14
+
15
+ - Destructive APIs (`rm`, `unlink`, `rmdir`, `truncate`) with a literal
16
+ out-of-scope absolute path: **critical / high** — the spec reserves critical
17
+ for destructive operations outside any declared scope.
18
+ - Non-destructive writes with a literal out-of-scope absolute path:
19
+ **high / high.**
20
+ - Writes whose target is a runtime value: **medium / low** (ROADMAP D2 —
21
+ runtime-dependent findings never lower a grade on their own).
22
+
23
+ ## False positives
24
+
25
+ A plugin that legitimately manages files elsewhere on disk (a backup tool,
26
+ say). The scope list is deliberately tiny; dispute with the use case and the
27
+ scope list gets a documented entry, not a silent exception.
28
+
29
+ ## Remediation
30
+
31
+ Keep writes inside the workspace or the DSH home directory; derive paths from
32
+ configuration instead of literals.
@@ -0,0 +1,26 @@
1
+ # perm.unreachable-files
2
+
3
+ Shipped files not reachable from any declared entry point.
4
+
5
+ ## What it looks for
6
+
7
+ The module graph starts from `main`/`module`/`exports`/`bin` entry points and
8
+ follows static imports and literal `require`/`import()`. Files the graph never
9
+ reaches are listed.
10
+
11
+ ## Severity / confidence policy
12
+
13
+ **info / high** — never affects a grade. Unreachable is not malicious: dead
14
+ code, fixtures, and dynamic-graph plugins all produce it. It is reported
15
+ because shipped-but-unreachable files can still be `require`d at runtime by
16
+ design or by an attacker, and a reviewer should know they exist.
17
+
18
+ ## False positives
19
+
20
+ Plugins that build their module graph from config strings at runtime. This is
21
+ the same population as `obf.dynamic-require`; the fix there (plain
22
+ specifiers) fixes the noise here.
23
+
24
+ ## Remediation
25
+
26
+ Delete dead files or wire them into the import graph.
@@ -0,0 +1,297 @@
1
+ import { existsSync } from "node:fs";
2
+ import { Node } from "acorn";
3
+ //#region src/contract.d.ts
4
+ /**
5
+ * The `dsh-vet/v1` report contract: a machine-readable, implementation-
6
+ * agnostic audit report for DeepSeek Harness (DSH) plugins. Any scanner may
7
+ * emit it; any marketplace, CI job, or UI may consume it. See
8
+ * `docs/dsh-vet-v1.md` for the normative spec.
9
+ *
10
+ * Two design rules are encoded here, not just documented:
11
+ * - Findings are signals, not verdicts: a finding with `low` confidence never
12
+ * lowers a grade, and `info` severity never does either.
13
+ * - Reports are deterministic: findings are sorted, and the summary is always
14
+ * derived, never asserted by the emitter.
15
+ *
16
+ * @module dsh-vet/contract
17
+ */
18
+ /** Literal `schema` value every dsh-vet/v1 report carries. */
19
+ declare const SCHEMA_ID: "dsh-vet/v1";
20
+ /** How bad a finding is. `info` never affects a grade. */
21
+ type VetSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
22
+ /** How sure the scanner is that the finding is real. `low` never affects a grade. */
23
+ type VetConfidence = 'high' | 'medium' | 'low';
24
+ /** Grade derived from graded findings. `X` marks an incomplete or errored scan. */
25
+ type VetGrade = 'A' | 'B' | 'C' | 'D' | 'F' | 'X';
26
+ interface VetEvidence {
27
+ /** Path within the audited package, e.g. `host-half.js`. */
28
+ file: string;
29
+ /** 1-based line of the evidence, when known. */
30
+ line?: number;
31
+ /** Minimal verbatim snippet; never include secrets. */
32
+ snippet?: string;
33
+ /** What the evidence shows, one sentence. */
34
+ note?: string;
35
+ }
36
+ interface VetFinding {
37
+ /**
38
+ * Namespaced rule id, e.g. `perm.broad-fs-write` or a vendor-prefixed
39
+ * `acme.eval-detect`. Must match {@link RULE_ID_PATTERN}; two or more
40
+ * dot-separated segments so third-party rule sets never collide.
41
+ */
42
+ id: string;
43
+ /** One-line human title, e.g. `Writes outside the declared workspace`. */
44
+ title: string;
45
+ severity: VetSeverity;
46
+ confidence: VetConfidence;
47
+ evidence: VetEvidence[];
48
+ /** How to make the finding go away, when actionable. */
49
+ remediation?: string;
50
+ /** URLs to rule documentation. */
51
+ references?: string[];
52
+ }
53
+ type VetTargetKind = 'npm-package' | 'git-repo' | 'local-path';
54
+ interface VetTarget {
55
+ kind: VetTargetKind;
56
+ /** What was audited, e.g. `dsh-vault@1.10.6`, a git URL, or a path. */
57
+ specifier: string;
58
+ /** What the specifier resolved to, when applicable. */
59
+ resolved?: {
60
+ version?: string;
61
+ commit?: string;
62
+ /** Subresource integrity of the exact artifact that was scanned. */
63
+ integrity?: string;
64
+ };
65
+ }
66
+ interface VetScanner {
67
+ /** Emitting implementation, e.g. `dsh-vet`. */
68
+ name: string;
69
+ version: string;
70
+ /** RFC 3339 timestamp of the run. */
71
+ ranAt: string;
72
+ }
73
+ interface VetSummaryCounts {
74
+ critical: number;
75
+ high: number;
76
+ medium: number;
77
+ low: number;
78
+ info: number;
79
+ }
80
+ interface VetSummary {
81
+ grade: VetGrade;
82
+ counts: VetSummaryCounts;
83
+ }
84
+ interface VetReport {
85
+ readonly schema: typeof SCHEMA_ID;
86
+ readonly target: VetTarget;
87
+ readonly scanner: VetScanner;
88
+ readonly summary: VetSummary;
89
+ readonly findings: readonly VetFinding[];
90
+ }
91
+ /**
92
+ * Rule ids are two or more dot-separated lowercase segments
93
+ * (`perm.broad-fs-write`, `acme.eval-detect`). Deliberately open-ended about
94
+ * segment count so vendor-prefixed rule sets work — a closed single-segment
95
+ * pattern is how the dsh-doctor contract initially broke third-party ids.
96
+ */
97
+ declare const RULE_ID_PATTERN: RegExp;
98
+ /** True when a finding participates in grading: not `info`, not low-confidence. */
99
+ declare function isGraded(finding: VetFinding): boolean;
100
+ /** Per-severity finding totals. */
101
+ declare function countFindings(findings: readonly VetFinding[]): VetSummaryCounts;
102
+ /**
103
+ * Derive the grade from graded findings only: worst severity decides.
104
+ * `low`-confidence and `info` findings are reported but never lower a grade.
105
+ */
106
+ declare function gradeFor(findings: readonly VetFinding[]): Exclude<VetGrade, 'X'>;
107
+ interface CreateReportInput {
108
+ target: VetTarget;
109
+ scanner: VetScanner;
110
+ findings: readonly VetFinding[];
111
+ }
112
+ /**
113
+ * Build a normalized report: validates rule ids, sorts findings
114
+ * deterministically (worst severity first, then id ascending), and derives
115
+ * the summary. Emitters must go through this instead of assembling a report
116
+ * by hand — it is what keeps two runs over the same artifact identical.
117
+ */
118
+ declare function createReport(input: CreateReportInput): VetReport;
119
+ //#endregion
120
+ //#region src/analyze.d.ts
121
+ type Capability = 'fs' | 'fs-write' | 'shell' | 'net' | 'workers' | 'crypto' | 'env' | 'homedir' | 'secret-read';
122
+ interface CapabilityUse {
123
+ cap: Capability;
124
+ file: string;
125
+ line: number;
126
+ api: string;
127
+ snippet: string;
128
+ /** Literal argument strings at the call site, when statically known. */
129
+ literals: string[];
130
+ }
131
+ interface NetUse {
132
+ file: string;
133
+ line: number;
134
+ api: string;
135
+ literals: string[];
136
+ snippet: string;
137
+ }
138
+ interface EvalUse {
139
+ file: string;
140
+ line: number;
141
+ kind: 'eval' | 'Function';
142
+ literal: boolean;
143
+ snippet: string;
144
+ }
145
+ interface DynamicImportUse {
146
+ file: string;
147
+ line: number;
148
+ kind: 'require' | 'import';
149
+ /** Statically concatenated argument value, when recoverable. */
150
+ literals: string[] | null;
151
+ snippet: string;
152
+ }
153
+ interface EncodedLiteral {
154
+ file: string;
155
+ line: number;
156
+ value: string;
157
+ charset: 'base64' | 'hex';
158
+ }
159
+ interface CharcodeCall {
160
+ file: string;
161
+ line: number;
162
+ chars: string;
163
+ }
164
+ interface ImportRef {
165
+ specifier: string;
166
+ line: number;
167
+ }
168
+ interface SourceFile {
169
+ path: string;
170
+ code: string;
171
+ sourceType: 'module' | 'script' | null;
172
+ parseError: string | null;
173
+ ast: Node | null;
174
+ imports: ImportRef[];
175
+ /** Bare (non-relative) module specifiers imported by this file. */
176
+ externals: string[];
177
+ }
178
+ interface PkgJson {
179
+ raw: Record<string, unknown>;
180
+ name: string;
181
+ version: string;
182
+ scripts: Record<string, string>;
183
+ dependencies: Record<string, string>;
184
+ devDependencies: Record<string, string>;
185
+ /** Declared Cordis seams, read from `dsh.seams` in package.json. */
186
+ seams: string[] | null;
187
+ entryHints: string[];
188
+ }
189
+ interface Analysis {
190
+ rootDir: string;
191
+ pkg: PkgJson | null;
192
+ files: SourceFile[];
193
+ fileByPath: Map<string, SourceFile>;
194
+ entries: string[];
195
+ reachable: Set<string>;
196
+ unreachable: string[];
197
+ /** Static relative import edges: file → files it imports. */
198
+ edges: Map<string, string[]>;
199
+ capUses: CapabilityUse[];
200
+ netUses: NetUse[];
201
+ evalUses: EvalUse[];
202
+ dynamicImports: DynamicImportUse[];
203
+ encodedLiterals: EncodedLiteral[];
204
+ charcodeCalls: CharcodeCall[];
205
+ }
206
+ /** Analyze a package directory: parse, walk, and build the module graph. */
207
+ declare function analyze(rootDir: string): Analysis;
208
+ /** All files statically reachable from `path` (excluding the path itself). */
209
+ declare function reachableFrom(analysis: Analysis, path: string): Set<string>;
210
+ //#endregion
211
+ //#region src/resolve.d.ts
212
+ type SpecifierKind = VetTarget['kind'];
213
+ interface ResolveOptions {
214
+ /** Injectable fetch for tests; defaults to the global fetch. */
215
+ fetchImpl?: (url: string) => Promise<Response>;
216
+ /** Registry base, default `https://registry.npmjs.org`. */
217
+ registry?: string;
218
+ }
219
+ interface ResolvedTarget {
220
+ target: VetTarget;
221
+ /** Local directory holding the artifact to scan. */
222
+ rootDir: string;
223
+ /** Package-relative paths of files the tarball contained, when known. */
224
+ files?: string[];
225
+ /** Links found in the tarball that were deliberately not materialized. */
226
+ skippedLinks?: Array<{
227
+ path: string;
228
+ type: string;
229
+ target: string;
230
+ }>;
231
+ cleanup: () => void;
232
+ }
233
+ declare class VetError extends Error {}
234
+ /** Classify a specifier without any network access. */
235
+ declare function classifySpecifier(specifier: string, exists?: typeof existsSync): SpecifierKind;
236
+ /** Split `name`, `name@version`, `@scope/name`, `@scope/name@tag|version`. */
237
+ declare function parseNpmSpecifier(specifier: string): {
238
+ name: string;
239
+ spec?: string;
240
+ };
241
+ /** Resolve any accepted specifier into a scannable local directory. */
242
+ declare function resolveTarget(specifier: string, opts?: ResolveOptions): Promise<ResolvedTarget>;
243
+ //#endregion
244
+ //#region src/rule.d.ts
245
+ interface RuleContext {
246
+ analysis: Analysis;
247
+ }
248
+ interface Rule {
249
+ /** Namespaced id matching RULE_ID_PATTERN, e.g. `perm.subprocess-spawn`. */
250
+ id: string;
251
+ /** One-line human title. */
252
+ title: string;
253
+ defaultSeverity: VetSeverity;
254
+ defaultConfidence: VetConfidence;
255
+ check(ctx: RuleContext): VetFinding[];
256
+ }
257
+ interface FindingInit {
258
+ title?: string;
259
+ severity?: VetSeverity;
260
+ confidence?: VetConfidence;
261
+ evidence: VetEvidence[];
262
+ remediation?: string;
263
+ references?: string[];
264
+ }
265
+ //#endregion
266
+ //#region src/rules/index.d.ts
267
+ declare const RULES: Rule[];
268
+ declare function ruleIds(): string[];
269
+ /** Run all rules, or the subset named in `only` (unknown ids throw). */
270
+ declare function runRules(analysis: Analysis, only?: string[]): VetFinding[];
271
+ //#endregion
272
+ //#region src/scanner.d.ts
273
+ /** Kept in lockstep with package.json; a test asserts they match. */
274
+ declare const SCANNER_VERSION = "0.1.0";
275
+ interface ScanOptions extends ResolveOptions {
276
+ /** Injectable clock for deterministic tests/reports. */
277
+ now?: () => string;
278
+ /** Restrict the scan to these rule ids (CLI `--rules`). */
279
+ rules?: string[];
280
+ }
281
+ declare function scanDirectory(dir: string, options?: ScanOptions): Promise<VetReport>;
282
+ declare function scan(specifier: string, options?: ScanOptions): Promise<VetReport>;
283
+ //#endregion
284
+ //#region src/cli.d.ts
285
+ /**
286
+ * CLI (ROADMAP T4). Exit semantics per the dsh-vet/v1 spec: `0` for any
287
+ * completed report (even a graded-F one), non-zero for scanner failure;
288
+ * `--strict`/`--fail-on` turn threshold breaches into exit code 1.
289
+ */
290
+ interface CliIo {
291
+ stdout: (line: string) => void;
292
+ stderr: (line: string) => void;
293
+ }
294
+ /** Parse and run; returns the process exit code. */
295
+ declare function runCli(argv: string[], io: CliIo): Promise<number>;
296
+ //#endregion
297
+ 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 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, resolveTarget, ruleIds, runCli, runRules, scan, scanDirectory };