@veraxhq/verax 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * PHASE 21.8 — Supply-Chain Policy
3
+ *
4
+ * Enforces supply-chain security policies:
5
+ * - License allowlist/denylist
6
+ * - Integrity hash requirements
7
+ * - Postinstall script restrictions
8
+ */
9
+
10
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { resolve } from 'path';
12
+ import DEFAULT_POLICY from './supplychain.defaults.json' with { type: 'json' };
13
+
14
+ /**
15
+ * Load supply-chain policy
16
+ *
17
+ * @param {string} projectDir - Project directory
18
+ * @returns {Object} Policy object
19
+ */
20
+ export function loadSupplyChainPolicy(projectDir) {
21
+ const customPath = resolve(projectDir, 'supplychain.policy.json');
22
+
23
+ if (existsSync(customPath)) {
24
+ try {
25
+ const custom = JSON.parse(readFileSync(customPath, 'utf-8'));
26
+ // Merge with defaults (custom overrides)
27
+ return {
28
+ ...DEFAULT_POLICY,
29
+ ...custom,
30
+ licensePolicy: {
31
+ ...DEFAULT_POLICY.licensePolicy,
32
+ ...(custom.licensePolicy || {})
33
+ },
34
+ integrityPolicy: {
35
+ ...DEFAULT_POLICY.integrityPolicy,
36
+ ...(custom.integrityPolicy || {})
37
+ },
38
+ scriptPolicy: {
39
+ ...DEFAULT_POLICY.scriptPolicy,
40
+ ...(custom.scriptPolicy || {})
41
+ },
42
+ sourcePolicy: {
43
+ ...DEFAULT_POLICY.sourcePolicy,
44
+ ...(custom.sourcePolicy || {})
45
+ }
46
+ };
47
+ } catch {
48
+ // Invalid custom policy, use defaults
49
+ }
50
+ }
51
+
52
+ return DEFAULT_POLICY;
53
+ }
54
+
55
+ /**
56
+ * Get package.json dependencies
57
+ *
58
+ * @param {string} projectDir - Project directory
59
+ * @returns {Object} Dependencies
60
+ */
61
+ function getDependencies(projectDir) {
62
+ try {
63
+ const pkgPath = resolve(projectDir, 'package.json');
64
+ if (!existsSync(pkgPath)) {
65
+ return { dependencies: {}, devDependencies: {} };
66
+ }
67
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
68
+ return {
69
+ dependencies: pkg.dependencies || {},
70
+ devDependencies: pkg.devDependencies || {}
71
+ };
72
+ } catch {
73
+ return { dependencies: {}, devDependencies: {} };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Get package license from node_modules
79
+ *
80
+ * @param {string} projectDir - Project directory
81
+ * @param {string} packageName - Package name
82
+ * @returns {string|null} License or null
83
+ */
84
+ function getPackageLicense(projectDir, packageName) {
85
+ try {
86
+ const pkgPath = resolve(projectDir, 'node_modules', packageName, 'package.json');
87
+ if (!existsSync(pkgPath)) {
88
+ return null;
89
+ }
90
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
91
+
92
+ if (typeof pkg.license === 'string') {
93
+ return pkg.license;
94
+ } else if (pkg.license && pkg.license.type) {
95
+ return pkg.license.type;
96
+ } else if (Array.isArray(pkg.licenses) && pkg.licenses.length > 0) {
97
+ return pkg.licenses[0].type || pkg.licenses[0];
98
+ }
99
+
100
+ return null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check package-lock.json for integrity hashes
108
+ *
109
+ * @param {string} projectDir - Project directory
110
+ * @returns {Object} Integrity check results
111
+ */
112
+ function checkIntegrityHashes(projectDir) {
113
+ const lockPath = resolve(projectDir, 'package-lock.json');
114
+ const missing = [];
115
+
116
+ if (!existsSync(lockPath)) {
117
+ return { missing: [], total: 0 };
118
+ }
119
+
120
+ try {
121
+ const lock = JSON.parse(readFileSync(lockPath, 'utf-8'));
122
+ const packages = lock.packages || {};
123
+ let total = 0;
124
+
125
+ for (const [path, pkg] of Object.entries(packages)) {
126
+ if (path && pkg && pkg.version) {
127
+ total++;
128
+ if (!pkg.integrity && !pkg.resolved?.includes('file:')) {
129
+ missing.push({
130
+ package: pkg.name || path,
131
+ path: path,
132
+ version: pkg.version
133
+ });
134
+ }
135
+ }
136
+ }
137
+
138
+ return { missing, total };
139
+ } catch {
140
+ return { missing: [], total: 0 };
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Check for postinstall scripts
146
+ *
147
+ * @param {string} projectDir - Project directory
148
+ * @returns {Array} Packages with postinstall scripts
149
+ */
150
+ function checkPostinstallScripts(projectDir) {
151
+ const packages = [];
152
+ const nodeModulesPath = resolve(projectDir, 'node_modules');
153
+
154
+ if (!existsSync(nodeModulesPath)) {
155
+ return packages;
156
+ }
157
+
158
+ try {
159
+ const { readdirSync } = require('fs');
160
+ const entries = readdirSync(nodeModulesPath, { withFileTypes: true });
161
+
162
+ for (const entry of entries) {
163
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
164
+ const pkgPath = resolve(nodeModulesPath, entry.name, 'package.json');
165
+ if (existsSync(pkgPath)) {
166
+ try {
167
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
168
+ if (pkg.scripts) {
169
+ if (pkg.scripts.postinstall || pkg.scripts.preinstall || pkg.scripts.install) {
170
+ packages.push({
171
+ name: pkg.name || entry.name,
172
+ version: pkg.version || 'unknown',
173
+ scripts: Object.keys(pkg.scripts).filter(s =>
174
+ ['postinstall', 'preinstall', 'install'].includes(s)
175
+ )
176
+ });
177
+ }
178
+ }
179
+ } catch {
180
+ // Skip invalid packages
181
+ }
182
+ }
183
+ }
184
+ }
185
+ } catch {
186
+ // If scanning fails, return empty
187
+ }
188
+
189
+ return packages;
190
+ }
191
+
192
+ /**
193
+ * Evaluate supply-chain policy
194
+ *
195
+ * @param {string} projectDir - Project directory
196
+ * @returns {Promise<Object>} Evaluation results
197
+ */
198
+ export async function evaluateSupplyChainPolicy(projectDir) {
199
+ const policy = loadSupplyChainPolicy(projectDir);
200
+ const deps = getDependencies(projectDir);
201
+ const violations = [];
202
+ const warnings = [];
203
+
204
+ // Check licenses
205
+ const allDeps = { ...deps.dependencies, ...deps.devDependencies };
206
+ for (const [packageName] of Object.entries(allDeps)) {
207
+ const license = getPackageLicense(projectDir, packageName);
208
+
209
+ if (license) {
210
+ // Check denylist
211
+ if (policy.licensePolicy.denylist.includes(license)) {
212
+ violations.push({
213
+ type: 'FORBIDDEN_LICENSE',
214
+ package: packageName,
215
+ license: license,
216
+ severity: 'BLOCKING',
217
+ message: `Package ${packageName} uses forbidden license: ${license}`
218
+ });
219
+ }
220
+
221
+ // Check allowlist (if strict mode)
222
+ if (policy.licensePolicy.strictMode && !policy.licensePolicy.allowlist.includes(license)) {
223
+ violations.push({
224
+ type: 'UNALLOWED_LICENSE',
225
+ package: packageName,
226
+ license: license,
227
+ severity: 'BLOCKING',
228
+ message: `Package ${packageName} uses unallowed license: ${license}`
229
+ });
230
+ }
231
+ } else {
232
+ warnings.push({
233
+ type: 'MISSING_LICENSE',
234
+ package: packageName,
235
+ message: `Package ${packageName} has no license information`
236
+ });
237
+ }
238
+ }
239
+
240
+ // Check integrity hashes
241
+ if (policy.integrityPolicy.requireIntegrityHash) {
242
+ const integrityCheck = checkIntegrityHashes(projectDir);
243
+ for (const missing of integrityCheck.missing) {
244
+ if (!policy.integrityPolicy.allowedMissingIntegrity.includes(missing.package)) {
245
+ violations.push({
246
+ type: 'MISSING_INTEGRITY',
247
+ package: missing.package,
248
+ version: missing.version,
249
+ severity: 'BLOCKING',
250
+ message: `Package ${missing.package}@${missing.version} missing integrity hash`
251
+ });
252
+ }
253
+ }
254
+ }
255
+
256
+ // Check postinstall scripts
257
+ if (policy.scriptPolicy.forbidPostinstall ||
258
+ policy.scriptPolicy.forbidPreinstall ||
259
+ policy.scriptPolicy.forbidInstall) {
260
+ const scripts = checkPostinstallScripts(projectDir);
261
+ for (const pkg of scripts) {
262
+ const forbiddenScripts = pkg.scripts.filter(s => {
263
+ if (s === 'postinstall' && policy.scriptPolicy.forbidPostinstall) return true;
264
+ if (s === 'preinstall' && policy.scriptPolicy.forbidPreinstall) return true;
265
+ if (s === 'install' && policy.scriptPolicy.forbidInstall) return true;
266
+ return false;
267
+ });
268
+
269
+ if (forbiddenScripts.length > 0 && !policy.scriptPolicy.allowlist.includes(pkg.name)) {
270
+ violations.push({
271
+ type: 'FORBIDDEN_SCRIPT',
272
+ package: pkg.name,
273
+ version: pkg.version,
274
+ scripts: forbiddenScripts,
275
+ severity: 'BLOCKING',
276
+ message: `Package ${pkg.name} has forbidden scripts: ${forbiddenScripts.join(', ')}`
277
+ });
278
+ }
279
+ }
280
+ }
281
+
282
+ const ok = violations.length === 0;
283
+
284
+ return {
285
+ ok,
286
+ violations,
287
+ warnings,
288
+ summary: {
289
+ totalViolations: violations.length,
290
+ totalWarnings: warnings.length,
291
+ byType: violations.reduce((acc, v) => {
292
+ acc[v.type] = (acc[v.type] || 0) + 1;
293
+ return acc;
294
+ }, {}),
295
+ evaluatedAt: new Date().toISOString()
296
+ },
297
+ policy: {
298
+ version: policy.version,
299
+ licensePolicy: {
300
+ allowlist: policy.licensePolicy.allowlist,
301
+ denylist: policy.licensePolicy.denylist,
302
+ strictMode: policy.licensePolicy.strictMode
303
+ }
304
+ }
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Write supply-chain report
310
+ *
311
+ * @param {string} projectDir - Project directory
312
+ * @param {Object} report - Evaluation results
313
+ * @returns {string} Path to written file
314
+ */
315
+ export function writeSupplyChainReport(projectDir, report) {
316
+ const outputDir = resolve(projectDir, 'release');
317
+ if (!existsSync(outputDir)) {
318
+ mkdirSync(outputDir, { recursive: true });
319
+ }
320
+
321
+ const outputPath = resolve(outputDir, 'security.supplychain.report.json');
322
+ writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
323
+
324
+ return outputPath;
325
+ }
326
+
@@ -0,0 +1,265 @@
1
+ /**
2
+ * PHASE 21.8 — Vulnerability Scanner
3
+ *
4
+ * Scans dependencies for vulnerabilities using npm audit.
5
+ * HIGH/CRITICAL = BLOCKING, MEDIUM = WARNING (configurable).
6
+ */
7
+
8
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ import { execSync } from 'child_process';
11
+
12
+ /**
13
+ * Check if OSV scanner is available
14
+ *
15
+ * @returns {boolean} Whether osv-scanner is available
16
+ */
17
+ function checkOSVAvailable() {
18
+ try {
19
+ execSync('osv-scanner --version', {
20
+ stdio: ['ignore', 'pipe', 'pipe'],
21
+ timeout: 5000
22
+ });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Run npm audit
31
+ *
32
+ * @param {string} projectDir - Project directory
33
+ * @returns {Object|null} Audit results or null
34
+ */
35
+ function runNpmAudit(projectDir) {
36
+ try {
37
+ const result = execSync('npm audit --json', {
38
+ cwd: projectDir,
39
+ encoding: 'utf-8',
40
+ stdio: ['ignore', 'pipe', 'pipe']
41
+ });
42
+
43
+ return JSON.parse(result);
44
+ } catch (error) {
45
+ // npm audit exits with non-zero on vulnerabilities
46
+ try {
47
+ const stderr = error.stderr?.toString() || '';
48
+ const stdout = error.stdout?.toString() || '';
49
+ const output = stdout || stderr;
50
+
51
+ if (output) {
52
+ return JSON.parse(output);
53
+ }
54
+ } catch {
55
+ // Failed to parse
56
+ }
57
+
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Parse vulnerabilities from audit results
64
+ *
65
+ * @param {Object} auditResults - npm audit JSON output
66
+ * @returns {Array} Array of vulnerabilities
67
+ */
68
+ function parseVulnerabilities(auditResults) {
69
+ const vulnerabilities = [];
70
+
71
+ if (!auditResults || !auditResults.vulnerabilities) {
72
+ return vulnerabilities;
73
+ }
74
+
75
+ for (const [packageName, vulnData] of Object.entries(auditResults.vulnerabilities)) {
76
+ if (Array.isArray(vulnData)) {
77
+ for (const vuln of vulnData) {
78
+ vulnerabilities.push({
79
+ package: packageName,
80
+ severity: vuln.severity?.toUpperCase() || 'UNKNOWN',
81
+ title: vuln.title || vuln.name || 'Unknown vulnerability',
82
+ url: vuln.url || null,
83
+ dependencyOf: vuln.dependencyOf || null,
84
+ via: vuln.via || null
85
+ });
86
+ }
87
+ } else if (vulnData.vulnerabilities) {
88
+ for (const vuln of vulnData.vulnerabilities) {
89
+ vulnerabilities.push({
90
+ package: packageName,
91
+ severity: vuln.severity?.toUpperCase() || 'UNKNOWN',
92
+ title: vuln.title || vuln.name || 'Unknown vulnerability',
93
+ url: vuln.url || null,
94
+ dependencyOf: vulnData.dependencyOf || null,
95
+ via: vuln.via || null
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ return vulnerabilities;
102
+ }
103
+
104
+ /**
105
+ * Scan for vulnerabilities
106
+ *
107
+ * @param {string} projectDir - Project directory
108
+ * @param {Object} options - Options
109
+ * @param {boolean} options.blockMedium - Block MEDIUM severity (default: false)
110
+ * @param {boolean} options.requireOSV - Require OSV scanner (default: false)
111
+ * @returns {Promise<Object>} Scan results
112
+ */
113
+ export async function scanVulnerabilities(projectDir, options = {}) {
114
+ const { blockMedium = false, requireOSV = false } = options;
115
+
116
+ // Check OSV availability
117
+ const osvAvailable = checkOSVAvailable();
118
+ let osvResults = null;
119
+
120
+ if (osvAvailable) {
121
+ try {
122
+ // Run OSV scanner
123
+ const osvOutput = execSync('osv-scanner --format=json .', {
124
+ cwd: projectDir,
125
+ encoding: 'utf-8',
126
+ stdio: ['ignore', 'pipe', 'pipe'],
127
+ timeout: 60000
128
+ });
129
+ try {
130
+ osvResults = JSON.parse(osvOutput);
131
+ } catch {
132
+ // OSV scanner may output non-JSON on errors
133
+ }
134
+ } catch {
135
+ // OSV scanner failed, fall back to npm audit
136
+ }
137
+ }
138
+
139
+ // Always try npm audit as fallback
140
+ const auditResults = runNpmAudit(projectDir);
141
+
142
+ if (!auditResults && !osvResults) {
143
+ if (requireOSV && !osvAvailable) {
144
+ return {
145
+ ok: false,
146
+ error: 'OSV scanner not available',
147
+ availability: 'NOT_AVAILABLE',
148
+ tool: null,
149
+ vulnerabilities: [],
150
+ summary: {
151
+ total: 0,
152
+ critical: 0,
153
+ high: 0,
154
+ medium: 0,
155
+ low: 0,
156
+ scannedAt: new Date().toISOString()
157
+ }
158
+ };
159
+ }
160
+
161
+ return {
162
+ ok: false,
163
+ error: 'Failed to run npm audit',
164
+ availability: 'FAILED',
165
+ tool: 'NPM_AUDIT',
166
+ vulnerabilities: [],
167
+ summary: {
168
+ total: 0,
169
+ critical: 0,
170
+ high: 0,
171
+ medium: 0,
172
+ low: 0,
173
+ scannedAt: new Date().toISOString()
174
+ }
175
+ };
176
+ }
177
+
178
+ // Parse vulnerabilities from npm audit (primary source)
179
+ let vulnerabilities = [];
180
+ if (auditResults) {
181
+ vulnerabilities = parseVulnerabilities(auditResults);
182
+ }
183
+
184
+ // Merge OSV results if available
185
+ if (osvResults && osvResults.results) {
186
+ for (const result of osvResults.results) {
187
+ if (result.packages && Array.isArray(result.packages)) {
188
+ for (const pkg of result.packages) {
189
+ if (result.vulnerabilities && Array.isArray(result.vulnerabilities)) {
190
+ for (const vuln of result.vulnerabilities) {
191
+ // Avoid duplicates (merge by package + ID)
192
+ const existing = vulnerabilities.find(v =>
193
+ v.package === pkg.package?.name &&
194
+ v.osvId === vuln.id
195
+ );
196
+ if (!existing) {
197
+ vulnerabilities.push({
198
+ package: pkg.package?.name || 'unknown',
199
+ severity: vuln.severity?.toUpperCase() || 'UNKNOWN',
200
+ title: vuln.summary || vuln.id || 'Unknown vulnerability',
201
+ url: vuln.database_specific?.url || vuln.id ? `https://osv.dev/vulnerability/${vuln.id}` : null,
202
+ osvId: vuln.id,
203
+ source: 'OSV'
204
+ });
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ const critical = vulnerabilities.filter(v => v.severity === 'CRITICAL');
214
+ const high = vulnerabilities.filter(v => v.severity === 'HIGH');
215
+ const medium = vulnerabilities.filter(v => v.severity === 'MEDIUM');
216
+ const low = vulnerabilities.filter(v => v.severity === 'LOW');
217
+
218
+ // BLOCKING: CRITICAL or HIGH
219
+ // WARNING: MEDIUM (blocking if blockMedium=true)
220
+ const blocking = critical.length > 0 || high.length > 0 || (blockMedium && medium.length > 0);
221
+
222
+ return {
223
+ ok: !blocking,
224
+ blocking,
225
+ availability: osvAvailable ? 'AVAILABLE' : 'NOT_AVAILABLE',
226
+ tool: osvAvailable ? 'OSV_SCANNER' : (auditResults ? 'NPM_AUDIT' : null),
227
+ osvAvailable,
228
+ vulnerabilities,
229
+ summary: {
230
+ total: vulnerabilities.length,
231
+ critical: critical.length,
232
+ high: high.length,
233
+ medium: medium.length,
234
+ low: low.length,
235
+ blocking,
236
+ warnings: blockMedium ? 0 : medium.length,
237
+ scannedAt: new Date().toISOString()
238
+ },
239
+ metadata: {
240
+ auditVersion: auditResults?.auditReportVersion || null,
241
+ npmVersion: auditResults?.npmVersion || null,
242
+ osvVersion: osvAvailable ? 'detected' : null
243
+ }
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Write vulnerability report
249
+ *
250
+ * @param {string} projectDir - Project directory
251
+ * @param {Object} report - Scan results
252
+ * @returns {string} Path to written file
253
+ */
254
+ export function writeVulnReport(projectDir, report) {
255
+ const outputDir = resolve(projectDir, 'release');
256
+ if (!existsSync(outputDir)) {
257
+ mkdirSync(outputDir, { recursive: true });
258
+ }
259
+
260
+ const outputPath = resolve(outputDir, 'security.vuln.report.json');
261
+ writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
262
+
263
+ return outputPath;
264
+ }
265
+