@perrylink/dsh-skill-pack-security-provider 2.1.3 → 2.2.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/lib/index.js CHANGED
@@ -38,8 +38,9 @@ export const Config = z.object({
38
38
  maxExtractBytes: z.natural().min(1024).max(512 * 1024 * 1024).default(64 * 1024 * 1024),
39
39
  maxDepNodes: z.natural().min(1).max(10000).default(600),
40
40
  maxFindingsPerCheck: z.natural().min(1).max(100).default(12),
41
- userAgent: z.string().max(200).default('dsh-skill-pack-security/2.1.3 (+https://github.com/PerryLink/dsh-skill-pack-security)'),
41
+ userAgent: z.string().max(200).default('dsh-skill-pack-security/2.2.0 (+https://github.com/PerryLink/dsh-skill-pack-security)'),
42
42
  dataResponsibility: z.boolean().default(true),
43
+ externalScanners: z.boolean().default(true),
43
44
  gate: z.object({
44
45
  policy: z.union(['warn', 'deny']).default('warn'),
45
46
  }),
@@ -12,6 +12,7 @@ import { type Lang } from './skills.js';
12
12
  import type { VetConfig } from './config.js';
13
13
  import type { ScannedFile } from './walk.js';
14
14
  import type { CheckId, VetCheck, VetSbom } from './vocabulary.js';
15
+ import type { ScannerResult } from './scanners.js';
15
16
  /** Shared inputs every check reads. */
16
17
  export interface CheckInputs {
17
18
  readonly files: ScannedFile[];
@@ -25,6 +26,8 @@ export interface CheckInputs {
25
26
  /** 40-hex HEAD of a local git target, when readable without spawning git. */
26
27
  readonly localHead: string;
27
28
  readonly now: number;
29
+ /** Dependency-scanner result (builtin or an external CLI), when computed. */
30
+ readonly scanner?: ScannerResult;
28
31
  }
29
32
  /** One check run: the check plus its optional SBOM payload. */
30
33
  export interface CheckResult {
@@ -19,6 +19,8 @@ export interface VetConfigInput {
19
19
  readonly maxFindingsPerCheck?: number;
20
20
  readonly userAgent?: string;
21
21
  readonly dataResponsibility?: boolean;
22
+ /** Orchestrate osv-scanner/npm audit when their CLIs are present (default true). */
23
+ readonly externalScanners?: boolean;
22
24
  readonly gate?: {
23
25
  readonly policy?: GatePolicy;
24
26
  };
@@ -34,6 +36,7 @@ export interface VetConfig {
34
36
  readonly maxFindingsPerCheck: number;
35
37
  readonly userAgent: string;
36
38
  readonly dataResponsibility: boolean;
39
+ readonly externalScanners: boolean;
37
40
  readonly gate: {
38
41
  readonly policy: GatePolicy;
39
42
  };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Dependency-vulnerability scanner Provider seam.
3
+ *
4
+ * The `sbom` check can hand its dependency evidence to an external scanner
5
+ * (`osv-scanner` or `npm audit`) when that CLI is present, and degrade to the
6
+ * built-in zero-dependency tree scan otherwise. Every scanner declares its
7
+ * `source`, so the report never misattributes findings. Probing is
8
+ * "detect → use → graceful degrade": any spawn failure (CLI absent, sandbox
9
+ * restriction, non-zero exit, unparseable output) resolves to an unavailable
10
+ * scanner instead of throwing.
11
+ *
12
+ * @module dsh-skill-pack-security/vet/scanners
13
+ */
14
+ import type { ScannerSource, VulnIssue } from './vocabulary.js';
15
+ /** Result of probing and (optionally) running one scanner. */
16
+ export interface ScannerResult {
17
+ readonly source: ScannerSource;
18
+ /** Whether the scanner could run against `dir`. */
19
+ readonly available: boolean;
20
+ /** Why it was unavailable (CLI missing, no lockfile, timeout, parse error). */
21
+ readonly reason?: string;
22
+ /** Vulnerabilities found; empty on a clean scan or when unavailable. */
23
+ readonly issues: VulnIssue[];
24
+ }
25
+ /** The Provider contract a dependency scanner implements. */
26
+ export interface DependencyScanner {
27
+ readonly source: ScannerSource;
28
+ readonly label: string;
29
+ /** Whether the scanner can run against `dir` (CLI present + target suitable). */
30
+ probe(dir: string, timeoutMs: number, signal?: AbortSignal): Promise<boolean>;
31
+ /** Run the scanner; never throws — failures degrade to an empty issue list. */
32
+ scan(dir: string, timeoutMs: number, signal?: AbortSignal): Promise<VulnIssue[]>;
33
+ }
34
+ /** osv-scanner adapter: detects the CLI, scans the directory. */
35
+ export declare const osvScanner: DependencyScanner;
36
+ /** npm-audit adapter: detects a lockfile + the npm CLI, runs `npm audit`. */
37
+ export declare const npmAuditScanner: DependencyScanner;
38
+ /**
39
+ * Probe the external scanners in preference order and return the first that is
40
+ * available, or a `builtin` result (always available) when none is.
41
+ * @param dir - the local target directory to scan (external scanners need it).
42
+ * @param timeoutMs - per-CLI probe/scan timeout.
43
+ * @param signal - caller abort signal (the tool-call signal).
44
+ * @returns a result whose `source` names the effective scanner.
45
+ */
46
+ export declare function probeDependencyScanner(dir: string, timeoutMs: number, signal?: AbortSignal): Promise<ScannerResult>;
@@ -61,6 +61,20 @@ export interface VetPackage {
61
61
  /** Whether the entry came from devDependencies only. */
62
62
  readonly dev: boolean;
63
63
  }
64
+ /** Where the dependency-scan evidence came from. */
65
+ export type ScannerSource = 'builtin' | 'osv-scanner' | 'npm-audit';
66
+ /** One vulnerability reported by a dependency scanner. */
67
+ export interface VulnIssue {
68
+ /** Advisory id when known (GHSA-…, CVE-…, or npm advisory id). */
69
+ readonly id?: string;
70
+ /** Affected package name. */
71
+ readonly package: string;
72
+ /** Affected version range, when the scanner reported one. */
73
+ readonly version?: string;
74
+ readonly severity: 'critical' | 'high' | 'moderate' | 'low' | 'unknown';
75
+ /** Short human-readable summary. */
76
+ readonly title: string;
77
+ }
64
78
  /** SBOM summary produced by the `sbom` check. */
65
79
  export interface VetSbom {
66
80
  readonly lockfile: string | null;
@@ -72,6 +86,10 @@ export interface VetSbom {
72
86
  readonly totalPackages: number;
73
87
  /** Direct dependency specs that are not pinned to an exact version. */
74
88
  readonly unpinned: string[];
89
+ /** Which scanner produced the dependency evidence (`builtin` = self-computed). */
90
+ readonly source: ScannerSource;
91
+ /** Vulnerabilities reported by an external scanner; empty for `builtin`. */
92
+ readonly vulnerabilities: VulnIssue[];
75
93
  }
76
94
  /** Scorecard: the five dimensions plus the overall weighted score. */
77
95
  export interface VetScores {
package/lib/vet/checks.js CHANGED
@@ -168,17 +168,19 @@ export function licenseCheck(inputs) {
168
168
  }
169
169
  // --- sbom --------------------------------------------------------------------
170
170
  export function sbomCheck(inputs) {
171
- const { manifest, lock, config, lang } = inputs;
171
+ const { manifest, lock, config, lang, scanner } = inputs;
172
172
  const zh = lang === 'zh';
173
173
  const findings = [];
174
174
  let score = 100;
175
175
  const direct = Object.keys(manifest.dependencies).length;
176
176
  const dev = Object.keys(manifest.devDependencies).length;
177
177
  const unpinned = unpinnedSpecs(manifest);
178
+ const source = scanner?.source ?? 'builtin';
179
+ const vulnerabilities = scanner?.issues ?? [];
178
180
  if (!manifest.present) {
179
181
  return {
180
182
  check: makeCheck('sbom', 0, [], lang, config, zh ? '无 package.json,无法生成依赖树' : 'no package.json, cannot build a dependency tree'),
181
- sbom: { lockfile: null, directDependencies: 0, directDevDependencies: 0, packages: [], truncated: false, totalPackages: 0, unpinned: [] },
183
+ sbom: { lockfile: null, directDependencies: 0, directDevDependencies: 0, packages: [], truncated: false, totalPackages: 0, unpinned: [], source, vulnerabilities },
182
184
  };
183
185
  }
184
186
  if (lock.lockfile === null) {
@@ -225,6 +227,8 @@ export function sbomCheck(inputs) {
225
227
  truncated: tree.truncated,
226
228
  totalPackages: tree.total,
227
229
  unpinned,
230
+ source,
231
+ vulnerabilities,
228
232
  };
229
233
  if (tree.truncated) {
230
234
  findings.push({
@@ -238,8 +242,48 @@ export function sbomCheck(inputs) {
238
242
  message: zh ? `依赖树:${tree.packages.length} 个唯一包(直接 ${direct} + dev ${dev})${lock.lockfile !== null ? `,锁文件 ${lock.lockfile}${lock.lockfileVersion !== '' ? ` v${lock.lockfileVersion}` : ''}` : ''}` : `dependency tree: ${tree.packages.length} unique packages (direct ${direct} + dev ${dev})${lock.lockfile !== null ? `, lockfile ${lock.lockfile}${lock.lockfileVersion !== '' ? ` v${lock.lockfileVersion}` : ''}` : ''}`,
239
243
  skill: SKILL_REF.sbom,
240
244
  });
245
+ // External scanner evidence replaces the self-computed vulnerability view;
246
+ // its source is annotated so the report never misattributes findings.
247
+ if (source !== 'builtin') {
248
+ if (vulnerabilities.length === 0) {
249
+ findings.push({
250
+ level: 'info',
251
+ message: zh ? `外部扫描器(${scannerLabel(source, zh)})未发现已知漏洞` : `external scanner (${scannerLabel(source, zh)}) found no known vulnerabilities`,
252
+ skill: SKILL_REF.sbom,
253
+ });
254
+ }
255
+ else {
256
+ for (const vuln of vulnerabilities) {
257
+ const severity = vuln.severity;
258
+ const level = severity === 'critical' || severity === 'high' ? 'fail' : 'warn';
259
+ findings.push({
260
+ level,
261
+ message: zh
262
+ ? `[${source}] ${vuln.package}${vuln.version !== undefined ? `@${vuln.version}` : ''}:${vuln.title}(${severity}${vuln.id !== undefined ? `, ${vuln.id}` : ''})`
263
+ : `[${source}] ${vuln.package}${vuln.version !== undefined ? `@${vuln.version}` : ''}: ${vuln.title} (${severity}${vuln.id !== undefined ? `, ${vuln.id}` : ''})`,
264
+ skill: SKILL_REF.sbom,
265
+ });
266
+ score -= level === 'fail' ? 30 : 15;
267
+ }
268
+ }
269
+ }
270
+ else {
271
+ findings.push({
272
+ level: 'info',
273
+ message: zh ? '依赖扫描来源:内置自算(未探测到 osv-scanner/npm audit CLI)' : 'dependency scan source: builtin (no osv-scanner/npm audit CLI detected)',
274
+ skill: SKILL_REF.sbom,
275
+ });
276
+ }
241
277
  return { check: makeCheck('sbom', score, findings, lang, config), sbom };
242
278
  }
279
+ /** Human label for a scanner source. */
280
+ function scannerLabel(source, zh) {
281
+ if (source === 'osv-scanner')
282
+ return zh ? 'OSV-Scanner' : 'OSV-Scanner';
283
+ if (source === 'npm-audit')
284
+ return zh ? 'npm audit' : 'npm audit';
285
+ return zh ? '内置自算' : 'builtin';
286
+ }
243
287
  /** Scan the collected files for git/action refs that must be 40-hex commits. */
244
288
  function collectRefHits(files, manifest) {
245
289
  const hits = [];
package/lib/vet/config.js CHANGED
@@ -15,8 +15,9 @@ export const VET_DEFAULTS = {
15
15
  maxExtractBytes: 64 * 1024 * 1024,
16
16
  maxDepNodes: 600,
17
17
  maxFindingsPerCheck: 12,
18
- userAgent: 'dsh-skill-pack-security/2.1.3 (+https://github.com/PerryLink/dsh-skill-pack-security)',
18
+ userAgent: 'dsh-skill-pack-security/2.2.0 (+https://github.com/PerryLink/dsh-skill-pack-security)',
19
19
  dataResponsibility: true,
20
+ externalScanners: true,
20
21
  gate: { policy: 'warn' },
21
22
  };
22
23
  /** Merge raw config over the defaults (the schema already validated shape/ranges). */
@@ -33,6 +34,7 @@ export function resolveVetConfig(raw) {
33
34
  maxFindingsPerCheck: raw.maxFindingsPerCheck ?? VET_DEFAULTS.maxFindingsPerCheck,
34
35
  userAgent: raw.userAgent ?? VET_DEFAULTS.userAgent,
35
36
  dataResponsibility: raw.dataResponsibility ?? VET_DEFAULTS.dataResponsibility,
37
+ externalScanners: raw.externalScanners ?? VET_DEFAULTS.externalScanners,
36
38
  gate: { policy: raw.gate?.policy ?? VET_DEFAULTS.gate.policy },
37
39
  };
38
40
  }
package/lib/vet/engine.js CHANGED
@@ -13,6 +13,7 @@ import { runChecks } from './checks.js';
13
13
  import { parseLockfile, parseManifest } from './manifest.js';
14
14
  import { resolveTarget } from './source.js';
15
15
  import { VetFetchError } from './fetch.js';
16
+ import { probeDependencyScanner } from './scanners.js';
16
17
  import { CHECK_NAME, SKILL_REF, T } from './skills.js';
17
18
  /** Engine-level failure: target unusable (not found, offline, budget). */
18
19
  export class VetTargetError extends Error {
@@ -159,7 +160,7 @@ export async function runVet(args, config, lang, signal) {
159
160
  scores: scoreDimensions(checks),
160
161
  verdict: 'skip',
161
162
  gate: { policy, applied: false, blocked: false },
162
- sbom: { lockfile: null, directDependencies: 0, directDevDependencies: 0, packages: [], truncated: false, totalPackages: 0, unpinned: [] },
163
+ sbom: { lockfile: null, directDependencies: 0, directDevDependencies: 0, packages: [], truncated: false, totalPackages: 0, unpinned: [], source: 'builtin', vulnerabilities: [] },
163
164
  budget: { filesScanned: 0, filesSkipped: 0, bytesScanned: 0, truncated: false, truncatedReason: skipReason },
164
165
  followupSkills: ['security-audit'],
165
166
  });
@@ -172,6 +173,13 @@ export async function runVet(args, config, lang, signal) {
172
173
  const manifest = parseManifest(resolved.files);
173
174
  const lock = parseLockfile(resolved.files);
174
175
  const localHead = resolved.kind === 'local-path' ? await readLocalHead(resolved.resolved) : '';
176
+ // External scanners (osv-scanner/npm audit) need a real on-disk project, so
177
+ // they are orchestrated only for local targets; remote targets degrade to
178
+ // the built-in tree scan with an explicit `builtin` source annotation.
179
+ let scanner;
180
+ if (config.externalScanners && resolved.kind === 'local-path') {
181
+ scanner = await probeDependencyScanner(resolved.resolved, config.timeoutMs, signal);
182
+ }
175
183
  const results = runChecks({
176
184
  files: resolved.files,
177
185
  manifest,
@@ -183,6 +191,7 @@ export async function runVet(args, config, lang, signal) {
183
191
  lang,
184
192
  localHead,
185
193
  now,
194
+ scanner,
186
195
  }, effectiveIds);
187
196
  const checks = results.map(result => result.check);
188
197
  const sbom = results.find(result => result.sbom !== undefined)?.sbom;
@@ -238,6 +247,8 @@ export async function runVet(args, config, lang, signal) {
238
247
  truncated: false,
239
248
  totalPackages: 0,
240
249
  unpinned: [],
250
+ source: 'builtin',
251
+ vulnerabilities: [],
241
252
  },
242
253
  budget,
243
254
  followupSkills: [...followupSkills],
package/lib/vet/report.js CHANGED
@@ -69,7 +69,9 @@ export function renderReport(report, lang) {
69
69
  parts.push(renderCheck(check, lang));
70
70
  }
71
71
  parts.push('');
72
- parts.push(`**SBOM** (${report.sbom.lockfile ?? (lang === 'zh' ? '无锁文件' : 'no lockfile')}) — ${lang === 'zh' ? '直接依赖' : 'direct'} ${report.sbom.directDependencies} + dev ${report.sbom.directDevDependencies}, ${lang === 'zh' ? '唯一包' : 'unique packages'} ${report.sbom.packages.length}${report.sbom.totalPackages > report.sbom.packages.length ? ` (${lang === 'zh' ? '总计' : 'total'} ${report.sbom.totalPackages})` : ''}`);
72
+ const sbomSource = report.sbom.source === 'osv-scanner' ? 'OSV-Scanner' : report.sbom.source === 'npm-audit' ? 'npm audit' : (lang === 'zh' ? '内置自算' : 'builtin');
73
+ const vulnCount = report.sbom.vulnerabilities.length;
74
+ parts.push(`**SBOM** (${report.sbom.lockfile ?? (lang === 'zh' ? '无锁文件' : 'no lockfile')}) — ${lang === 'zh' ? '直接依赖' : 'direct'} ${report.sbom.directDependencies} + dev ${report.sbom.directDevDependencies}, ${lang === 'zh' ? '唯一包' : 'unique packages'} ${report.sbom.packages.length}${report.sbom.totalPackages > report.sbom.packages.length ? ` (${lang === 'zh' ? '总计' : 'total'} ${report.sbom.totalPackages})` : ''} · ${lang === 'zh' ? '扫描来源' : 'scan source'}: ${sbomSource}${vulnCount > 0 ? ` · ${lang === 'zh' ? '已知漏洞' : 'known vulns'}: ${vulnCount}` : ''}`);
73
75
  if (report.sbom.packages.length > 0) {
74
76
  const tree = report.sbom.packages.slice(0, TREE_CAP).map(pkg => `${' '.repeat(Math.min(pkg.depth, 8))}${pkg.name}@${pkg.version}`).join('\n');
75
77
  parts.push('```text');
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Dependency-vulnerability scanner Provider seam.
3
+ *
4
+ * The `sbom` check can hand its dependency evidence to an external scanner
5
+ * (`osv-scanner` or `npm audit`) when that CLI is present, and degrade to the
6
+ * built-in zero-dependency tree scan otherwise. Every scanner declares its
7
+ * `source`, so the report never misattributes findings. Probing is
8
+ * "detect → use → graceful degrade": any spawn failure (CLI absent, sandbox
9
+ * restriction, non-zero exit, unparseable output) resolves to an unavailable
10
+ * scanner instead of throwing.
11
+ *
12
+ * @module dsh-skill-pack-security/vet/scanners
13
+ */
14
+ import { spawn } from 'node:child_process';
15
+ import { existsSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ /** Spawn a CLI, capture output, and always settle (error/timeout → null code). */
18
+ function runCommand(bin, args, cwd, timeoutMs, signal) {
19
+ return new Promise((resolve) => {
20
+ let child;
21
+ try {
22
+ child = spawn(bin, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
23
+ }
24
+ catch {
25
+ resolve({ code: null, stdout: '', stderr: 'spawn threw' });
26
+ return;
27
+ }
28
+ let stdout = '';
29
+ let stderr = '';
30
+ let settled = false;
31
+ const finish = (code) => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ clearTimeout(timer);
36
+ resolve({ code, stdout, stderr });
37
+ };
38
+ const timer = setTimeout(() => {
39
+ try {
40
+ child.kill();
41
+ }
42
+ catch { /* already exited */ }
43
+ finish(null);
44
+ }, timeoutMs);
45
+ const onAbort = () => { try {
46
+ child.kill();
47
+ }
48
+ catch { /* already exited */ } };
49
+ signal?.addEventListener('abort', onAbort, { once: true });
50
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
51
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
52
+ child.on('error', () => finish(null));
53
+ child.on('close', (code) => finish(code));
54
+ });
55
+ }
56
+ /** Parse osv-scanner's `--format json` output into issues. */
57
+ function parseOsvJson(text) {
58
+ try {
59
+ const parsed = JSON.parse(text);
60
+ const issues = [];
61
+ for (const result of parsed.results ?? []) {
62
+ for (const pkg of result.packages ?? []) {
63
+ const name = pkg.package?.name ?? 'unknown';
64
+ const version = pkg.package?.version;
65
+ for (const vuln of pkg.vulnerabilities ?? []) {
66
+ issues.push({
67
+ id: vuln.id,
68
+ package: name,
69
+ version,
70
+ severity: osvSeverity(vuln),
71
+ title: vuln.summary ?? vuln.id ?? 'vulnerability',
72
+ });
73
+ }
74
+ }
75
+ }
76
+ return issues;
77
+ }
78
+ catch {
79
+ return [];
80
+ }
81
+ }
82
+ /** Map osv-scanner severity (a CVSS score object) to our severity vocabulary. */
83
+ function osvSeverity(vuln) {
84
+ const score = Number.parseFloat(vuln.severity?.[0]?.score ?? '');
85
+ if (Number.isNaN(score))
86
+ return 'unknown';
87
+ if (score >= 9)
88
+ return 'critical';
89
+ if (score >= 7)
90
+ return 'high';
91
+ if (score >= 4)
92
+ return 'moderate';
93
+ return 'low';
94
+ }
95
+ /** Parse `npm audit --json` output into issues. */
96
+ function parseNpmAuditJson(text) {
97
+ try {
98
+ const parsed = JSON.parse(text);
99
+ const issues = [];
100
+ for (const [key, vuln] of Object.entries(parsed.vulnerabilities ?? {})) {
101
+ const via = vuln.via ?? [];
102
+ const advisory = via.find((entry) => typeof entry === 'object' && entry !== null);
103
+ const title = advisory?.title ?? (typeof via[0] === 'string' ? via[0] : key);
104
+ issues.push({
105
+ id: typeof advisory?.url === 'string' ? advisory.url.split('/').pop() : undefined,
106
+ package: vuln.name ?? key,
107
+ version: vuln.range,
108
+ severity: npmSeverity(vuln.severity),
109
+ title,
110
+ });
111
+ }
112
+ return issues;
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ }
118
+ /** Normalize npm audit severity strings. */
119
+ function npmSeverity(severity) {
120
+ if (severity === 'critical')
121
+ return 'critical';
122
+ if (severity === 'high')
123
+ return 'high';
124
+ if (severity === 'moderate')
125
+ return 'moderate';
126
+ if (severity === 'low')
127
+ return 'low';
128
+ return 'unknown';
129
+ }
130
+ /** Lockfile filenames that external scanners can operate on. */
131
+ const LOCKFILES = ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock'];
132
+ function hasLockfile(dir) {
133
+ return LOCKFILES.some(name => existsSync(join(dir, name)));
134
+ }
135
+ /** osv-scanner adapter: detects the CLI, scans the directory. */
136
+ export const osvScanner = {
137
+ source: 'osv-scanner',
138
+ label: 'OSV-Scanner',
139
+ async probe(dir, timeoutMs, signal) {
140
+ if (!hasLockfile(dir))
141
+ return false;
142
+ const result = await runCommand('osv-scanner', ['--version'], dir, timeoutMs, signal);
143
+ return result.code === 0 && result.stdout.length > 0;
144
+ },
145
+ async scan(dir, timeoutMs, signal) {
146
+ // `osv-scanner scan` (newer) falls back to the legacy flag-less form.
147
+ const attempts = [
148
+ ['scan', '--format', 'json', '--recursive', '.'],
149
+ ['--format', 'json', '--recursive', '.'],
150
+ ];
151
+ for (const args of attempts) {
152
+ const result = await runCommand('osv-scanner', args, dir, timeoutMs, signal);
153
+ if (result.code === 0 || result.stdout.trim() !== '') {
154
+ return parseOsvJson(result.stdout);
155
+ }
156
+ }
157
+ return [];
158
+ },
159
+ };
160
+ /** npm-audit adapter: detects a lockfile + the npm CLI, runs `npm audit`. */
161
+ export const npmAuditScanner = {
162
+ source: 'npm-audit',
163
+ label: 'npm audit',
164
+ async probe(dir, timeoutMs, signal) {
165
+ if (!hasLockfile(dir))
166
+ return false;
167
+ const result = await runCommand('npm', ['--version'], dir, timeoutMs, signal);
168
+ return result.code === 0 && result.stdout.length > 0;
169
+ },
170
+ async scan(dir, timeoutMs, signal) {
171
+ const result = await runCommand('npm', ['audit', '--json'], dir, timeoutMs, signal);
172
+ if (result.stdout.trim() === '')
173
+ return [];
174
+ return parseNpmAuditJson(result.stdout);
175
+ },
176
+ };
177
+ /**
178
+ * Probe the external scanners in preference order and return the first that is
179
+ * available, or a `builtin` result (always available) when none is.
180
+ * @param dir - the local target directory to scan (external scanners need it).
181
+ * @param timeoutMs - per-CLI probe/scan timeout.
182
+ * @param signal - caller abort signal (the tool-call signal).
183
+ * @returns a result whose `source` names the effective scanner.
184
+ */
185
+ export async function probeDependencyScanner(dir, timeoutMs, signal) {
186
+ for (const scanner of [osvScanner, npmAuditScanner]) {
187
+ try {
188
+ if (await scanner.probe(dir, timeoutMs, signal)) {
189
+ const issues = await scanner.scan(dir, timeoutMs, signal);
190
+ return { source: scanner.source, available: true, issues };
191
+ }
192
+ }
193
+ catch {
194
+ // Any probe/scan failure degrades to the next scanner (then builtin).
195
+ }
196
+ }
197
+ return { source: 'builtin', available: true, issues: [] };
198
+ }
package/lib/vet/tool.js CHANGED
@@ -95,6 +95,20 @@ const REPORT_SCHEMA = {
95
95
  truncated: { type: 'boolean', required: true },
96
96
  totalPackages: { type: 'integer', required: true },
97
97
  unpinned: { type: 'array', required: true, items: { type: 'string' } },
98
+ source: { type: 'string', required: true, enum: ['builtin', 'osv-scanner', 'npm-audit'] },
99
+ vulnerabilities: {
100
+ type: 'array', required: true,
101
+ items: {
102
+ type: 'object', additionalProperties: false,
103
+ properties: {
104
+ id: { type: 'string' },
105
+ package: { type: 'string', required: true },
106
+ version: { type: 'string' },
107
+ severity: { type: 'string', required: true, enum: ['critical', 'high', 'moderate', 'low', 'unknown'] },
108
+ title: { type: 'string', required: true },
109
+ },
110
+ },
111
+ },
98
112
  },
99
113
  },
100
114
  budget: {
@@ -4,7 +4,7 @@ description: '依赖供应链审计:pnpm/npm audit 输出与退出码解读、
4
4
  whenToUse: '用户要求审计或盘点项目依赖安全(漏洞、license、投毒、锁文件漂移)、解读 audit 报告、判断某个依赖能否引入,或写依赖审计结论时使用;单个依赖的普通升级与纯功能开发不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 依赖审计(dependency-audit)
@@ -4,7 +4,7 @@ description: 'agent 环境安全事件响应:分类→控制蔓延→取证留
4
4
  whenToUse: 'agent 环境(DSH 会话、插件、MCP、CI)出现疑似安全事件——密钥泄露、被注入执行了未授权操作、依赖投毒、权限异常——需要响应、留证与复盘时使用;没有事件迹象的日常开发不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 事件响应(incident-response)
@@ -4,7 +4,7 @@ description: '面向 agent 项目的提示注入面审查:AGENTS.md、技能
4
4
  whenToUse: '审查 agent 项目的上下文注入面(AGENTS.md/CLAUDE.md、.agents/skills、工具描述、MCP server 来源、web 抓取链路)、评估间接注入风险或对 agent 项目做安全评审时使用;与模型上下文无关的普通代码评审不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 提示注入面审查(prompt-injection-review)
@@ -4,7 +4,7 @@ description: '凭据/密钥暴露审计:gitleaks、trivy 全历史扫描命令
4
4
  whenToUse: '用户要求扫描或检查仓库的密钥泄露、排查某提交或某文件中的 token、给扫描告警定真伪、写脱敏泄露报告或规划密钥轮换时使用;纯功能开发与常规代码审查不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 凭据扫描(secret-scan)
@@ -4,7 +4,7 @@ description: '仓库/软件安全审计总览:范围界定→资产清单→
4
4
  whenToUse: '用户要求对代码仓库或项目做安全审计、制定审计计划、划分审计阶段、汇总多类发现成报告,或不确定该从哪个专项技能开始时使用;单一主题任务(只查密钥、只查依赖、只评审一个 PR、只查注入面)直接加载对应专项技能,不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 安全审计总览(security-audit)
@@ -4,7 +4,7 @@ description: 'PR/新依赖快速供应链评审:危险 install/postinstall 脚
4
4
  whenToUse: '评审含新依赖(package.json/锁文件变更)的 PR、审查某包的 install 脚本行为、判断疑似 typosquat 包或验证构建可复现性时使用;纯业务代码、与新增依赖无关的 PR 评审不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 新增依赖快速评审(supply-chain-review)
@@ -4,7 +4,7 @@ description: '新功能/新系统的轻量威胁建模:固定对象→划定
4
4
  whenToUse: '用户要求对新功能/新系统做威胁建模、设计阶段安全评审、STRIDE 分析、攻击树分析,或要求把安全考虑前置到设计阶段时使用;纯实现细节讨论、与信任边界无关的改动不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 威胁建模(threat-model)
@@ -4,7 +4,7 @@ description: '漏洞情报检索与判定:NVD/CISA-KEV/GHSA/OSV 四处权威
4
4
  whenToUse: '用户给出 CVE/GHSA 编号要求查详情与影响、判断漏洞是否被在野利用(KEV)、评估漏洞对当前项目/依赖的适用性或汇总漏洞情报简报时使用;没有具体编号的通用安全学习、与特定漏洞无关的讨论不触发本技能。'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # 漏洞情报(vuln-intel)
@@ -4,7 +4,7 @@ description: 'Dependency supply-chain audit: reading pnpm/npm audit output and e
4
4
  whenToUse: 'Use when the user asks to audit or inventory project dependency security (vulnerabilities, licenses, poisoning, lockfile drift), to interpret an audit report, to judge whether a dependency may be introduced, or to write a dependency-audit conclusion. Upgrading a single dependency and plain feature development do not trigger this skill.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
  # Dependency audit (dependency-audit)
10
10
 
@@ -4,7 +4,7 @@ description: 'Security incident response for agent environments: a staged flow o
4
4
  whenToUse: 'Use when an agent environment (DSH sessions, plugins, MCP, CI) shows a suspected security incident — secret leak, injected execution of unauthorized actions, dependency poisoning, permission anomalies — and it needs response, evidence, and a postmortem. Day-to-day development without incident indicators does not trigger this skill.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # Incident response (incident-response)
@@ -4,7 +4,7 @@ description: 'Injection-surface review for agent projects: a checklist covering
4
4
  whenToUse: 'Use when reviewing the context injection surfaces of an agent project (AGENTS.md/CLAUDE.md, .agents/skills, tool descriptions, MCP server sources, web-fetch chains), assessing indirect-injection risk, or doing a security review of an agent project. Ordinary code review unrelated to model context does not trigger this skill.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
  # Prompt-injection surface review (prompt-injection-review)
10
10
 
@@ -4,7 +4,7 @@ description: 'Credential/secret exposure audit: gitleaks and trivy full-history
4
4
  whenToUse: 'Use when the user asks to scan or inspect a repository for secret leaks, to hunt tokens in a commit or file, to tier scan alerts as real or false, to write a redacted leak report, or to plan secret rotation. Plain feature development and ordinary code review do not trigger this skill.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
  # Secret scanning (secret-scan)
10
10
 
@@ -4,7 +4,7 @@ description: 'Repository/software security audit overview: a staged flow of scop
4
4
  whenToUse: 'Use when the user asks for a security audit of a code repository or project, an audit plan, staged audit steps, a consolidated findings report, or is unsure which specialist skill to start with. Single-topic tasks (only secrets, only dependencies, only one PR, only injection surfaces) load the matching specialist skill directly and do not trigger this overview.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
  # Security audit overview (security-audit)
10
10
 
@@ -4,7 +4,7 @@ description: 'Quick PR/new-dependency supply-chain review: dangerous install/pos
4
4
  whenToUse: 'Use when reviewing a PR that adds new dependencies (package.json/lockfile changes), inspecting a package install-script behavior, judging a suspected typosquat package, or verifying build reproducibility. Plain business-code PR reviews unrelated to new dependencies do not trigger this skill.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
  # New-dependency quick review (supply-chain-review)
10
10
 
@@ -4,7 +4,7 @@ description: 'Lightweight threat modeling for new features/systems: fix the targ
4
4
  whenToUse: 'Use when the user asks for threat modeling of a new feature/system, design-stage security review, STRIDE analysis, attack-tree analysis, or wants security considered up front at design time. Pure implementation detail discussions and changes unrelated to trust boundaries do not trigger this skill.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # Threat modeling (threat-model)
@@ -4,7 +4,7 @@ description: 'Vulnerability intelligence lookup and triage: query commands for t
4
4
  whenToUse: 'Use when the user gives a CVE/GHSA id and asks for details and impact, whether a vulnerability is actively exploited (KEV), its applicability to the current project/dependencies, or a vulnerability intelligence brief. General security learning without a specific id, and discussions unrelated to a specific vulnerability, do not trigger this skill.'
5
5
  metadata:
6
6
  pack: dsh-skill-pack-security
7
- version: '2.1.3'
7
+ version: '2.2.0'
8
8
  ---
9
9
 
10
10
  # Vulnerability intelligence (vuln-intel)
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@perrylink/dsh-skill-pack-security-provider",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "Provider plugin for dsh-skill-pack-security: registers the pack's skills/ (zh) or skills-en/ (en) edition on ctx.skills AND the plugin_vet supply-chain gate tool on ctx.tools (license/SBOM/commit-lock/malware scans + five-dimension risk card). Ships both skill editions embedded in pack/.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
8
+ "engines": {
9
+ "node": "^22.19.0 || >=24.0.0"
10
+ },
11
+ "packageManager": "pnpm@11.7.0",
8
12
  "repository": {
9
13
  "type": "git",
10
14
  "url": "git+https://github.com/PerryLink/dsh-skill-pack-security.git",