@perrylink/dsh-skill-pack-security-provider 2.1.4 → 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 +2 -1
- package/lib/types/vet/checks.d.ts +3 -0
- package/lib/types/vet/config.d.ts +3 -0
- package/lib/types/vet/scanners.d.ts +46 -0
- package/lib/types/vet/vocabulary.d.ts +18 -0
- package/lib/vet/checks.js +46 -2
- package/lib/vet/config.js +3 -1
- package/lib/vet/engine.js +12 -1
- package/lib/vet/report.js +3 -1
- package/lib/vet/scanners.js +198 -0
- package/lib/vet/tool.js +14 -0
- package/pack/skills/dependency-audit/SKILL.md +1 -1
- package/pack/skills/incident-response/SKILL.md +1 -1
- package/pack/skills/prompt-injection-review/SKILL.md +1 -1
- package/pack/skills/secret-scan/SKILL.md +1 -1
- package/pack/skills/security-audit/SKILL.md +1 -1
- package/pack/skills/supply-chain-review/SKILL.md +1 -1
- package/pack/skills/threat-model/SKILL.md +1 -1
- package/pack/skills/vuln-intel/SKILL.md +1 -1
- package/pack/skills-en/dependency-audit/SKILL.md +1 -1
- package/pack/skills-en/incident-response/SKILL.md +1 -1
- package/pack/skills-en/prompt-injection-review/SKILL.md +1 -1
- package/pack/skills-en/secret-scan/SKILL.md +1 -1
- package/pack/skills-en/security-audit/SKILL.md +1 -1
- package/pack/skills-en/supply-chain-review/SKILL.md +1 -1
- package/pack/skills-en/threat-model/SKILL.md +1 -1
- package/pack/skills-en/vuln-intel/SKILL.md +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
7
|
+
version: '2.2.0'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# 提示注入面审查(prompt-injection-review)
|
|
@@ -4,7 +4,7 @@ description: '仓库/软件安全审计总览:范围界定→资产清单→
|
|
|
4
4
|
whenToUse: '用户要求对代码仓库或项目做安全审计、制定审计计划、划分审计阶段、汇总多类发现成报告,或不确定该从哪个专项技能开始时使用;单一主题任务(只查密钥、只查依赖、只评审一个 PR、只查注入面)直接加载对应专项技能,不触发本技能。'
|
|
5
5
|
metadata:
|
|
6
6
|
pack: dsh-skill-pack-security
|
|
7
|
-
version: '2.
|
|
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.
|
|
7
|
+
version: '2.2.0'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# 新增依赖快速评审(supply-chain-review)
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
7
|
+
version: '2.2.0'
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Vulnerability intelligence (vuln-intel)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perrylink/dsh-skill-pack-security-provider",
|
|
3
|
-
"version": "2.
|
|
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",
|