@veraxhq/verax 0.2.1 → 0.4.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 (213) hide show
  1. package/README.md +10 -6
  2. package/bin/verax.js +11 -11
  3. package/package.json +29 -8
  4. package/src/cli/commands/baseline.js +103 -0
  5. package/src/cli/commands/default.js +51 -6
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +246 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +4 -2
  10. package/src/cli/commands/release-check.js +215 -0
  11. package/src/cli/commands/run.js +45 -6
  12. package/src/cli/commands/security-check.js +212 -0
  13. package/src/cli/commands/truth.js +113 -0
  14. package/src/cli/entry.js +30 -20
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +544 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-promise-extractor.js +581 -0
  22. package/src/cli/util/ast-usestate-detector.js +602 -0
  23. package/src/cli/util/atomic-write.js +12 -1
  24. package/src/cli/util/bootstrap-guard.js +86 -0
  25. package/src/cli/util/console-reporter.js +72 -0
  26. package/src/cli/util/detection-engine.js +105 -41
  27. package/src/cli/util/determinism-runner.js +124 -0
  28. package/src/cli/util/determinism-writer.js +129 -0
  29. package/src/cli/util/digest-engine.js +359 -0
  30. package/src/cli/util/dom-diff.js +226 -0
  31. package/src/cli/util/evidence-engine.js +287 -0
  32. package/src/cli/util/expectation-extractor.js +151 -5
  33. package/src/cli/util/findings-writer.js +3 -0
  34. package/src/cli/util/framework-detector.js +572 -0
  35. package/src/cli/util/idgen.js +1 -1
  36. package/src/cli/util/interaction-planner.js +529 -0
  37. package/src/cli/util/learn-writer.js +2 -0
  38. package/src/cli/util/ledger-writer.js +110 -0
  39. package/src/cli/util/monorepo-resolver.js +162 -0
  40. package/src/cli/util/observation-engine.js +127 -278
  41. package/src/cli/util/observe-writer.js +2 -0
  42. package/src/cli/util/project-discovery.js +284 -0
  43. package/src/cli/util/project-writer.js +2 -0
  44. package/src/cli/util/run-id.js +23 -27
  45. package/src/cli/util/run-resolver.js +64 -0
  46. package/src/cli/util/run-result.js +778 -0
  47. package/src/cli/util/selector-resolver.js +235 -0
  48. package/src/cli/util/source-requirement.js +55 -0
  49. package/src/cli/util/summary-writer.js +2 -0
  50. package/src/cli/util/svelte-navigation-detector.js +163 -0
  51. package/src/cli/util/svelte-network-detector.js +80 -0
  52. package/src/cli/util/svelte-sfc-extractor.js +146 -0
  53. package/src/cli/util/svelte-state-detector.js +242 -0
  54. package/src/cli/util/trust-activation-integration.js +496 -0
  55. package/src/cli/util/trust-activation-wrapper.js +85 -0
  56. package/src/cli/util/trust-integration-hooks.js +164 -0
  57. package/src/cli/util/types.js +153 -0
  58. package/src/cli/util/url-validation.js +40 -0
  59. package/src/cli/util/vue-navigation-detector.js +178 -0
  60. package/src/cli/util/vue-sfc-extractor.js +161 -0
  61. package/src/cli/util/vue-state-detector.js +215 -0
  62. package/src/types/fs-augment.d.ts +23 -0
  63. package/src/types/global.d.ts +137 -0
  64. package/src/types/internal-types.d.ts +35 -0
  65. package/src/verax/cli/init.js +4 -18
  66. package/src/verax/core/action-classifier.js +4 -3
  67. package/src/verax/core/artifacts/registry.js +139 -0
  68. package/src/verax/core/artifacts/verifier.js +990 -0
  69. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  70. package/src/verax/core/baseline/baseline.snapshot.js +233 -0
  71. package/src/verax/core/capabilities/gates.js +505 -0
  72. package/src/verax/core/capabilities/registry.js +475 -0
  73. package/src/verax/core/confidence/confidence-compute.js +144 -0
  74. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  75. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  76. package/src/verax/core/confidence/confidence-weights.js +44 -0
  77. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  78. package/src/verax/core/confidence/confidence.loader.js +80 -0
  79. package/src/verax/core/confidence/confidence.schema.js +94 -0
  80. package/src/verax/core/confidence-engine-refactor.js +489 -0
  81. package/src/verax/core/confidence-engine.js +625 -0
  82. package/src/verax/core/contracts/index.js +29 -0
  83. package/src/verax/core/contracts/types.js +186 -0
  84. package/src/verax/core/contracts/validators.js +456 -0
  85. package/src/verax/core/decisions/decision.trace.js +278 -0
  86. package/src/verax/core/determinism/contract-writer.js +89 -0
  87. package/src/verax/core/determinism/contract.js +139 -0
  88. package/src/verax/core/determinism/diff.js +405 -0
  89. package/src/verax/core/determinism/engine.js +222 -0
  90. package/src/verax/core/determinism/finding-identity.js +149 -0
  91. package/src/verax/core/determinism/normalize.js +466 -0
  92. package/src/verax/core/determinism/report-writer.js +93 -0
  93. package/src/verax/core/determinism/run-fingerprint.js +123 -0
  94. package/src/verax/core/dynamic-route-intelligence.js +529 -0
  95. package/src/verax/core/evidence/evidence-capture-service.js +308 -0
  96. package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
  97. package/src/verax/core/evidence-builder.js +487 -0
  98. package/src/verax/core/execution-mode-context.js +77 -0
  99. package/src/verax/core/execution-mode-detector.js +192 -0
  100. package/src/verax/core/failures/exit-codes.js +88 -0
  101. package/src/verax/core/failures/failure-summary.js +76 -0
  102. package/src/verax/core/failures/failure.factory.js +225 -0
  103. package/src/verax/core/failures/failure.ledger.js +133 -0
  104. package/src/verax/core/failures/failure.types.js +196 -0
  105. package/src/verax/core/failures/index.js +10 -0
  106. package/src/verax/core/ga/ga-report-writer.js +43 -0
  107. package/src/verax/core/ga/ga.artifact.js +49 -0
  108. package/src/verax/core/ga/ga.contract.js +435 -0
  109. package/src/verax/core/ga/ga.enforcer.js +87 -0
  110. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  111. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  112. package/src/verax/core/guardrails/policy.loader.js +84 -0
  113. package/src/verax/core/guardrails/policy.schema.js +110 -0
  114. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  115. package/src/verax/core/guardrails-engine.js +505 -0
  116. package/src/verax/core/incremental-store.js +1 -0
  117. package/src/verax/core/integrity/budget.js +138 -0
  118. package/src/verax/core/integrity/determinism.js +342 -0
  119. package/src/verax/core/integrity/integrity.js +208 -0
  120. package/src/verax/core/integrity/poisoning.js +108 -0
  121. package/src/verax/core/integrity/transaction.js +140 -0
  122. package/src/verax/core/observe/run-timeline.js +318 -0
  123. package/src/verax/core/perf/perf.contract.js +186 -0
  124. package/src/verax/core/perf/perf.display.js +65 -0
  125. package/src/verax/core/perf/perf.enforcer.js +91 -0
  126. package/src/verax/core/perf/perf.monitor.js +209 -0
  127. package/src/verax/core/perf/perf.report.js +200 -0
  128. package/src/verax/core/pipeline-tracker.js +243 -0
  129. package/src/verax/core/product-definition.js +127 -0
  130. package/src/verax/core/release/provenance.builder.js +130 -0
  131. package/src/verax/core/release/release-report-writer.js +40 -0
  132. package/src/verax/core/release/release.enforcer.js +164 -0
  133. package/src/verax/core/release/reproducibility.check.js +222 -0
  134. package/src/verax/core/release/sbom.builder.js +292 -0
  135. package/src/verax/core/replay-validator.js +2 -0
  136. package/src/verax/core/replay.js +4 -0
  137. package/src/verax/core/report/cross-index.js +195 -0
  138. package/src/verax/core/report/human-summary.js +362 -0
  139. package/src/verax/core/route-intelligence.js +420 -0
  140. package/src/verax/core/run-id.js +6 -3
  141. package/src/verax/core/run-manifest.js +4 -3
  142. package/src/verax/core/security/secrets.scan.js +329 -0
  143. package/src/verax/core/security/security-report.js +50 -0
  144. package/src/verax/core/security/security.enforcer.js +128 -0
  145. package/src/verax/core/security/supplychain.defaults.json +38 -0
  146. package/src/verax/core/security/supplychain.policy.js +334 -0
  147. package/src/verax/core/security/vuln.scan.js +265 -0
  148. package/src/verax/core/truth/truth.certificate.js +252 -0
  149. package/src/verax/core/ui-feedback-intelligence.js +481 -0
  150. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  151. package/src/verax/detect/confidence-engine.js +62 -34
  152. package/src/verax/detect/confidence-helper.js +34 -0
  153. package/src/verax/detect/dynamic-route-findings.js +338 -0
  154. package/src/verax/detect/expectation-chain-detector.js +417 -0
  155. package/src/verax/detect/expectation-model.js +2 -2
  156. package/src/verax/detect/failure-cause-inference.js +293 -0
  157. package/src/verax/detect/findings-writer.js +131 -35
  158. package/src/verax/detect/flow-detector.js +2 -2
  159. package/src/verax/detect/form-silent-failure.js +98 -0
  160. package/src/verax/detect/index.js +46 -5
  161. package/src/verax/detect/invariants-enforcer.js +147 -0
  162. package/src/verax/detect/journey-stall-detector.js +558 -0
  163. package/src/verax/detect/navigation-silent-failure.js +82 -0
  164. package/src/verax/detect/problem-aggregator.js +361 -0
  165. package/src/verax/detect/route-findings.js +219 -0
  166. package/src/verax/detect/summary-writer.js +477 -0
  167. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  168. package/src/verax/detect/ui-feedback-findings.js +207 -0
  169. package/src/verax/detect/view-switch-correlator.js +242 -0
  170. package/src/verax/flow/flow-engine.js +2 -1
  171. package/src/verax/flow/flow-spec.js +0 -6
  172. package/src/verax/index.js +4 -0
  173. package/src/verax/intel/ts-program.js +1 -0
  174. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  175. package/src/verax/learn/action-contract-extractor.js +3 -0
  176. package/src/verax/learn/ast-contract-extractor.js +1 -1
  177. package/src/verax/learn/flow-extractor.js +1 -0
  178. package/src/verax/learn/project-detector.js +5 -0
  179. package/src/verax/learn/react-router-extractor.js +2 -0
  180. package/src/verax/learn/source-instrumenter.js +1 -0
  181. package/src/verax/learn/state-extractor.js +2 -1
  182. package/src/verax/learn/static-extractor.js +1 -0
  183. package/src/verax/observe/coverage-gaps.js +132 -0
  184. package/src/verax/observe/expectation-handler.js +126 -0
  185. package/src/verax/observe/incremental-skip.js +46 -0
  186. package/src/verax/observe/index.js +51 -155
  187. package/src/verax/observe/interaction-executor.js +192 -0
  188. package/src/verax/observe/interaction-runner.js +782 -513
  189. package/src/verax/observe/network-firewall.js +86 -0
  190. package/src/verax/observe/observation-builder.js +169 -0
  191. package/src/verax/observe/observe-context.js +205 -0
  192. package/src/verax/observe/observe-helpers.js +192 -0
  193. package/src/verax/observe/observe-runner.js +230 -0
  194. package/src/verax/observe/observers/budget-observer.js +185 -0
  195. package/src/verax/observe/observers/console-observer.js +102 -0
  196. package/src/verax/observe/observers/coverage-observer.js +107 -0
  197. package/src/verax/observe/observers/interaction-observer.js +471 -0
  198. package/src/verax/observe/observers/navigation-observer.js +132 -0
  199. package/src/verax/observe/observers/network-observer.js +87 -0
  200. package/src/verax/observe/observers/safety-observer.js +82 -0
  201. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  202. package/src/verax/observe/page-traversal.js +138 -0
  203. package/src/verax/observe/snapshot-ops.js +94 -0
  204. package/src/verax/observe/ui-feedback-detector.js +742 -0
  205. package/src/verax/scan-summary-writer.js +2 -0
  206. package/src/verax/shared/artifact-manager.js +25 -5
  207. package/src/verax/shared/caching.js +1 -0
  208. package/src/verax/shared/css-spinner-rules.js +204 -0
  209. package/src/verax/shared/expectation-tracker.js +1 -0
  210. package/src/verax/shared/view-switch-rules.js +208 -0
  211. package/src/verax/shared/zip-artifacts.js +6 -0
  212. package/src/verax/shared/config-loader.js +0 -169
  213. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -0,0 +1,334 @@
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
+
13
+ const DEFAULT_POLICY = JSON.parse(
14
+ readFileSync(new URL('./supplychain.defaults.json', import.meta.url), 'utf-8')
15
+ );
16
+
17
+ /**
18
+ * Load supply-chain policy
19
+ *
20
+ * @param {string} projectDir - Project directory
21
+ * @returns {Object} Policy object
22
+ */
23
+ export function loadSupplyChainPolicy(projectDir) {
24
+ const customPath = resolve(projectDir, 'supplychain.policy.json');
25
+
26
+ if (existsSync(customPath)) {
27
+ try {
28
+ // @ts-expect-error - readFileSync with encoding returns string
29
+ const custom = JSON.parse(readFileSync(customPath, 'utf-8'));
30
+ // Merge with defaults (custom overrides)
31
+ return {
32
+ ...DEFAULT_POLICY,
33
+ ...custom,
34
+ licensePolicy: {
35
+ ...DEFAULT_POLICY.licensePolicy,
36
+ ...(custom.licensePolicy || {})
37
+ },
38
+ integrityPolicy: {
39
+ ...DEFAULT_POLICY.integrityPolicy,
40
+ ...(custom.integrityPolicy || {})
41
+ },
42
+ scriptPolicy: {
43
+ ...DEFAULT_POLICY.scriptPolicy,
44
+ ...(custom.scriptPolicy || {})
45
+ },
46
+ sourcePolicy: {
47
+ ...DEFAULT_POLICY.sourcePolicy,
48
+ ...(custom.sourcePolicy || {})
49
+ }
50
+ };
51
+ } catch {
52
+ // Invalid custom policy, use defaults
53
+ }
54
+ }
55
+
56
+ return DEFAULT_POLICY;
57
+ }
58
+
59
+ /**
60
+ * Get package.json dependencies
61
+ *
62
+ * @param {string} projectDir - Project directory
63
+ * @returns {Object} Dependencies
64
+ */
65
+ function getDependencies(projectDir) {
66
+ try {
67
+ const pkgPath = resolve(projectDir, 'package.json');
68
+ if (!existsSync(pkgPath)) {
69
+ return { dependencies: {}, devDependencies: {} };
70
+ }
71
+ // @ts-expect-error - readFileSync with encoding returns string
72
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
73
+ return {
74
+ dependencies: pkg.dependencies || {},
75
+ devDependencies: pkg.devDependencies || {}
76
+ };
77
+ } catch {
78
+ return { dependencies: {}, devDependencies: {} };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get package license from node_modules
84
+ *
85
+ * @param {string} projectDir - Project directory
86
+ * @param {string} packageName - Package name
87
+ * @returns {string|null} License or null
88
+ */
89
+ function getPackageLicense(projectDir, packageName) {
90
+ try {
91
+ const pkgPath = resolve(projectDir, 'node_modules', packageName, 'package.json');
92
+ if (!existsSync(pkgPath)) {
93
+ return null;
94
+ }
95
+ // @ts-expect-error - readFileSync with encoding returns string
96
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
97
+
98
+ if (typeof pkg.license === 'string') {
99
+ return pkg.license;
100
+ } else if (pkg.license && pkg.license.type) {
101
+ return pkg.license.type;
102
+ } else if (Array.isArray(pkg.licenses) && pkg.licenses.length > 0) {
103
+ return pkg.licenses[0].type || pkg.licenses[0];
104
+ }
105
+
106
+ return null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check package-lock.json for integrity hashes
114
+ *
115
+ * @param {string} projectDir - Project directory
116
+ * @returns {Object} Integrity check results
117
+ */
118
+ function checkIntegrityHashes(projectDir) {
119
+ const lockPath = resolve(projectDir, 'package-lock.json');
120
+ const missing = [];
121
+
122
+ if (!existsSync(lockPath)) {
123
+ return { missing: [], total: 0 };
124
+ }
125
+
126
+ try {
127
+ // @ts-expect-error - readFileSync with encoding returns string
128
+ const lock = JSON.parse(readFileSync(lockPath, 'utf-8'));
129
+ const packages = lock.packages || {};
130
+ let total = 0;
131
+
132
+ for (const [path, pkg] of Object.entries(packages)) {
133
+ if (path && pkg && pkg.version) {
134
+ total++;
135
+ if (!pkg.integrity && !pkg.resolved?.includes('file:')) {
136
+ missing.push({
137
+ package: pkg.name || path,
138
+ path: path,
139
+ version: pkg.version
140
+ });
141
+ }
142
+ }
143
+ }
144
+
145
+ return { missing, total };
146
+ } catch {
147
+ return { missing: [], total: 0 };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Check for postinstall scripts
153
+ *
154
+ * @param {string} projectDir - Project directory
155
+ * @returns {Array} Packages with postinstall scripts
156
+ */
157
+ function checkPostinstallScripts(projectDir) {
158
+ const packages = [];
159
+ const nodeModulesPath = resolve(projectDir, 'node_modules');
160
+
161
+ if (!existsSync(nodeModulesPath)) {
162
+ return packages;
163
+ }
164
+
165
+ try {
166
+ const { readdirSync } = require('fs');
167
+ const entries = readdirSync(nodeModulesPath, { withFileTypes: true });
168
+
169
+ for (const entry of entries) {
170
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
171
+ const pkgPath = resolve(nodeModulesPath, entry.name, 'package.json');
172
+ if (existsSync(pkgPath)) {
173
+ try {
174
+ // @ts-expect-error - readFileSync with encoding returns string
175
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
176
+ if (pkg.scripts) {
177
+ if (pkg.scripts.postinstall || pkg.scripts.preinstall || pkg.scripts.install) {
178
+ packages.push({
179
+ name: pkg.name || entry.name,
180
+ version: pkg.version || 'unknown',
181
+ scripts: Object.keys(pkg.scripts).filter(s =>
182
+ ['postinstall', 'preinstall', 'install'].includes(s)
183
+ )
184
+ });
185
+ }
186
+ }
187
+ } catch {
188
+ // Skip invalid packages
189
+ }
190
+ }
191
+ }
192
+ }
193
+ } catch {
194
+ // If scanning fails, return empty
195
+ }
196
+
197
+ return packages;
198
+ }
199
+
200
+ /**
201
+ * Evaluate supply-chain policy
202
+ *
203
+ * @param {string} projectDir - Project directory
204
+ * @returns {Promise<Object>} Evaluation results
205
+ */
206
+ export async function evaluateSupplyChainPolicy(projectDir) {
207
+ const policy = loadSupplyChainPolicy(projectDir);
208
+ const deps = getDependencies(projectDir);
209
+ const violations = [];
210
+ const warnings = [];
211
+
212
+ // Check licenses
213
+ const allDeps = { ...deps.dependencies, ...deps.devDependencies };
214
+ for (const [packageName] of Object.entries(allDeps)) {
215
+ const license = getPackageLicense(projectDir, packageName);
216
+
217
+ if (license) {
218
+ // Check denylist
219
+ if (policy.licensePolicy.denylist.includes(license)) {
220
+ violations.push({
221
+ type: 'FORBIDDEN_LICENSE',
222
+ package: packageName,
223
+ license: license,
224
+ severity: 'BLOCKING',
225
+ message: `Package ${packageName} uses forbidden license: ${license}`
226
+ });
227
+ }
228
+
229
+ // Check allowlist (if strict mode)
230
+ if (policy.licensePolicy.strictMode && !policy.licensePolicy.allowlist.includes(license)) {
231
+ violations.push({
232
+ type: 'UNALLOWED_LICENSE',
233
+ package: packageName,
234
+ license: license,
235
+ severity: 'BLOCKING',
236
+ message: `Package ${packageName} uses unallowed license: ${license}`
237
+ });
238
+ }
239
+ } else {
240
+ warnings.push({
241
+ type: 'MISSING_LICENSE',
242
+ package: packageName,
243
+ message: `Package ${packageName} has no license information`
244
+ });
245
+ }
246
+ }
247
+
248
+ // Check integrity hashes
249
+ if (policy.integrityPolicy.requireIntegrityHash) {
250
+ const integrityCheck = checkIntegrityHashes(projectDir);
251
+ for (const missing of integrityCheck.missing) {
252
+ if (!policy.integrityPolicy.allowedMissingIntegrity.includes(missing.package)) {
253
+ violations.push({
254
+ type: 'MISSING_INTEGRITY',
255
+ package: missing.package,
256
+ version: missing.version,
257
+ severity: 'BLOCKING',
258
+ message: `Package ${missing.package}@${missing.version} missing integrity hash`
259
+ });
260
+ }
261
+ }
262
+ }
263
+
264
+ // Check postinstall scripts
265
+ if (policy.scriptPolicy.forbidPostinstall ||
266
+ policy.scriptPolicy.forbidPreinstall ||
267
+ policy.scriptPolicy.forbidInstall) {
268
+ const scripts = checkPostinstallScripts(projectDir);
269
+ for (const pkg of scripts) {
270
+ const forbiddenScripts = pkg.scripts.filter(s => {
271
+ if (s === 'postinstall' && policy.scriptPolicy.forbidPostinstall) return true;
272
+ if (s === 'preinstall' && policy.scriptPolicy.forbidPreinstall) return true;
273
+ if (s === 'install' && policy.scriptPolicy.forbidInstall) return true;
274
+ return false;
275
+ });
276
+
277
+ if (forbiddenScripts.length > 0 && !policy.scriptPolicy.allowlist.includes(pkg.name)) {
278
+ violations.push({
279
+ type: 'FORBIDDEN_SCRIPT',
280
+ package: pkg.name,
281
+ version: pkg.version,
282
+ scripts: forbiddenScripts,
283
+ severity: 'BLOCKING',
284
+ message: `Package ${pkg.name} has forbidden scripts: ${forbiddenScripts.join(', ')}`
285
+ });
286
+ }
287
+ }
288
+ }
289
+
290
+ const ok = violations.length === 0;
291
+
292
+ return {
293
+ ok,
294
+ violations,
295
+ warnings,
296
+ summary: {
297
+ totalViolations: violations.length,
298
+ totalWarnings: warnings.length,
299
+ byType: violations.reduce((acc, v) => {
300
+ acc[v.type] = (acc[v.type] || 0) + 1;
301
+ return acc;
302
+ }, {}),
303
+ evaluatedAt: new Date().toISOString()
304
+ },
305
+ policy: {
306
+ version: policy.version,
307
+ licensePolicy: {
308
+ allowlist: policy.licensePolicy.allowlist,
309
+ denylist: policy.licensePolicy.denylist,
310
+ strictMode: policy.licensePolicy.strictMode
311
+ }
312
+ }
313
+ };
314
+ }
315
+
316
+ /**
317
+ * Write supply-chain report
318
+ *
319
+ * @param {string} projectDir - Project directory
320
+ * @param {Object} report - Evaluation results
321
+ * @returns {string} Path to written file
322
+ */
323
+ export function writeSupplyChainReport(projectDir, report) {
324
+ const outputDir = resolve(projectDir, 'release');
325
+ if (!existsSync(outputDir)) {
326
+ mkdirSync(outputDir, { recursive: true });
327
+ }
328
+
329
+ const outputPath = resolve(outputDir, 'security.supplychain.report.json');
330
+ writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
331
+
332
+ return outputPath;
333
+ }
334
+
@@ -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 as _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 = { blockMedium: false, requireOSV: false }) {
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
+