@perrylink/dsh-skill-pack-security-provider 2.1.4 → 2.2.3

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/LICENSE ADDED
@@ -0,0 +1,190 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ Copyright 2026 dsh-skill-pack-security contributors
179
+
180
+ Licensed under the Apache License, Version 2.0 (the "License");
181
+ you may not use this file except in compliance with the License.
182
+ You may obtain a copy of the License at
183
+
184
+ http://www.apache.org/licenses/LICENSE-2.0
185
+
186
+ Unless required by applicable law or agreed to in writing, software
187
+ distributed under the License is distributed on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
+ See the License for the specific language governing permissions and
190
+ limitations under the License.
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.4 (+https://github.com/PerryLink/dsh-skill-pack-security)'),
41
+ userAgent: z.string().max(200).default('dsh-skill-pack-security/2.2.3 (+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.4 (+https://github.com/PerryLink/dsh-skill-pack-security)',
18
+ userAgent: 'dsh-skill-pack-security/2.2.3 (+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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.4'
7
+ version: '2.2.3'
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.1.4",
3
+ "version": "2.2.3",
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",
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "compatibility": {
51
51
  "dshVersions": [
52
- "0.1.1-rc.2"
52
+ "0.1.2-alpha.3"
53
53
  ]
54
54
  },
55
55
  "capability": {
@@ -77,18 +77,18 @@
77
77
  "supply-chain"
78
78
  ],
79
79
  "peerDependencies": {
80
- "@deepseek-ai/cordis": "^4.0.1",
80
+ "@deepseek-ai/cordis": "^4.0.2",
81
81
  "@deepseek-ai/dsh-skill-filesystem": ">=0.1.0-rc.8 <0.2.0",
82
82
  "@deepseek-ai/dsh-tools": ">=0.1.0-rc.8 <0.2.0",
83
- "@deepseek-ai/schemastery": "^3.18.1"
83
+ "@deepseek-ai/schemastery": "^3.18.2"
84
84
  },
85
85
  "devDependencies": {
86
- "@deepseek-ai/cordis": "^4.0.1",
87
- "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
88
- "@deepseek-ai/dsh-skill-filesystem": "0.1.1-rc.2",
89
- "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
90
- "@deepseek-ai/schemastery": "^3.18.1",
91
- "@types/node": "^26.2.0",
86
+ "@deepseek-ai/cordis": "^4.0.2",
87
+ "@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
88
+ "@deepseek-ai/dsh-skill-filesystem": "0.1.2-alpha.3",
89
+ "@deepseek-ai/dsh-tools": "0.1.2-alpha.3",
90
+ "@deepseek-ai/schemastery": "^3.18.2",
91
+ "@types/node": "^26.4.0",
92
92
  "typescript": "^7.0.2"
93
93
  },
94
94
  "scripts": {