@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.
- package/README.md +10 -6
- package/bin/verax.js +11 -11
- package/package.json +29 -8
- package/src/cli/commands/baseline.js +103 -0
- package/src/cli/commands/default.js +51 -6
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +246 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +4 -2
- package/src/cli/commands/release-check.js +215 -0
- package/src/cli/commands/run.js +45 -6
- package/src/cli/commands/security-check.js +212 -0
- package/src/cli/commands/truth.js +113 -0
- package/src/cli/entry.js +30 -20
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +544 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +124 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +151 -5
- package/src/cli/util/findings-writer.js +3 -0
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -0
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -0
- package/src/cli/util/project-discovery.js +284 -0
- package/src/cli/util/project-writer.js +2 -0
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +2 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +146 -0
- package/src/cli/util/svelte-state-detector.js +242 -0
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +178 -0
- package/src/cli/util/vue-sfc-extractor.js +161 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +139 -0
- package/src/verax/core/artifacts/verifier.js +990 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +233 -0
- package/src/verax/core/capabilities/gates.js +505 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +144 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +80 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +489 -0
- package/src/verax/core/confidence-engine.js +625 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +186 -0
- package/src/verax/core/contracts/validators.js +456 -0
- package/src/verax/core/decisions/decision.trace.js +278 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +405 -0
- package/src/verax/core/determinism/engine.js +222 -0
- package/src/verax/core/determinism/finding-identity.js +149 -0
- package/src/verax/core/determinism/normalize.js +466 -0
- package/src/verax/core/determinism/report-writer.js +93 -0
- package/src/verax/core/determinism/run-fingerprint.js +123 -0
- package/src/verax/core/dynamic-route-intelligence.js +529 -0
- package/src/verax/core/evidence/evidence-capture-service.js +308 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +166 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +192 -0
- package/src/verax/core/failures/exit-codes.js +88 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +133 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +435 -0
- package/src/verax/core/ga/ga.enforcer.js +87 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +84 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +318 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +200 -0
- package/src/verax/core/pipeline-tracker.js +243 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +130 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +164 -0
- package/src/verax/core/release/reproducibility.check.js +222 -0
- package/src/verax/core/release/sbom.builder.js +292 -0
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +195 -0
- package/src/verax/core/report/human-summary.js +362 -0
- package/src/verax/core/route-intelligence.js +420 -0
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +329 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +128 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +334 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +252 -0
- package/src/verax/core/ui-feedback-intelligence.js +481 -0
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +62 -34
- package/src/verax/detect/confidence-helper.js +34 -0
- package/src/verax/detect/dynamic-route-findings.js +338 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +2 -2
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +131 -35
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +46 -5
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +219 -0
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +4 -0
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +3 -0
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +51 -155
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -513
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +192 -0
- package/src/verax/observe/observe-runner.js +230 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/scan-summary-writer.js +2 -0
- package/src/verax/shared/artifact-manager.js +25 -5
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.7 — Release Enforcer
|
|
3
|
+
*
|
|
4
|
+
* Hard lock: blocks publish/release without GA-READY + Provenance + SBOM + Reproducible.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
import { checkGAStatus } from '../ga/ga.enforcer.js';
|
|
10
|
+
import { findLatestRunId } from '../../../cli/util/run-resolver.js';
|
|
11
|
+
import { FAILURE_CODE } from '../failures/failure.types.js';
|
|
12
|
+
import { isBaselineFrozen, enforceBaseline } from '../baseline/baseline.enforcer.js';
|
|
13
|
+
import { FailureLedger } from '../failures/failure.ledger.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if release is allowed
|
|
17
|
+
*
|
|
18
|
+
* @param {string} projectDir - Project directory
|
|
19
|
+
* @param {string} operation - Operation name (publish, release, tag)
|
|
20
|
+
* @throws {Error} If release is blocked
|
|
21
|
+
*/
|
|
22
|
+
export async function enforceReleaseReadiness(projectDir, operation = 'release') {
|
|
23
|
+
const blockers = [];
|
|
24
|
+
|
|
25
|
+
// 1. Check GA status
|
|
26
|
+
try {
|
|
27
|
+
const runId = findLatestRunId(projectDir);
|
|
28
|
+
if (!runId) {
|
|
29
|
+
blockers.push('No runs found. Run a scan and verify GA readiness first.');
|
|
30
|
+
} else {
|
|
31
|
+
const gaCheck = checkGAStatus(projectDir, runId);
|
|
32
|
+
if (!gaCheck.ready) {
|
|
33
|
+
const blockerMessages = gaCheck.status?.blockers?.map(b => b.message).join('; ') || 'GA not ready';
|
|
34
|
+
blockers.push(`GA-BLOCKED: ${blockerMessages}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
blockers.push(`GA check failed: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 2. Check Provenance
|
|
42
|
+
const provenancePath = resolve(projectDir, 'release', 'release.provenance.json');
|
|
43
|
+
if (!existsSync(provenancePath)) {
|
|
44
|
+
blockers.push('Provenance not found. Run "verax release:check" first.');
|
|
45
|
+
} else {
|
|
46
|
+
try {
|
|
47
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
48
|
+
const provenance = JSON.parse(readFileSync(provenancePath, 'utf-8'));
|
|
49
|
+
if (provenance.git?.dirty) {
|
|
50
|
+
blockers.push('Provenance indicates dirty git repository');
|
|
51
|
+
}
|
|
52
|
+
if (provenance.gaStatus !== 'GA-READY') {
|
|
53
|
+
blockers.push(`Provenance GA status is ${provenance.gaStatus}, not GA-READY`);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
blockers.push(`Invalid provenance: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Check SBOM
|
|
61
|
+
const sbomPath = resolve(projectDir, 'release', 'sbom.json');
|
|
62
|
+
if (!existsSync(sbomPath)) {
|
|
63
|
+
blockers.push('SBOM not found. Run "verax release:check" first.');
|
|
64
|
+
} else {
|
|
65
|
+
try {
|
|
66
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
67
|
+
const sbom = JSON.parse(readFileSync(sbomPath, 'utf-8'));
|
|
68
|
+
if (!sbom.bomFormat || !sbom.components || !Array.isArray(sbom.components) || sbom.components.length === 0) {
|
|
69
|
+
blockers.push('Invalid or empty SBOM');
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
blockers.push(`Invalid SBOM: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Check Reproducibility
|
|
77
|
+
const reproducibilityPath = resolve(projectDir, 'release', 'reproducibility.report.json');
|
|
78
|
+
if (!existsSync(reproducibilityPath)) {
|
|
79
|
+
blockers.push('Reproducibility report not found. Run "verax release:check" first.');
|
|
80
|
+
} else {
|
|
81
|
+
try {
|
|
82
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
83
|
+
const report = JSON.parse(readFileSync(reproducibilityPath, 'utf-8'));
|
|
84
|
+
if (report.verdict !== 'REPRODUCIBLE') {
|
|
85
|
+
const differences = report.differences?.map(d => d.message).join('; ') || 'Build is not reproducible';
|
|
86
|
+
blockers.push(`NON_REPRODUCIBLE: ${differences}`);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
blockers.push(`Invalid reproducibility report: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Check Security (PHASE 21.8)
|
|
94
|
+
const secretsPath = resolve(projectDir, 'release', 'security.secrets.report.json');
|
|
95
|
+
const vulnPath = resolve(projectDir, 'release', 'security.vuln.report.json');
|
|
96
|
+
const supplyChainPath = resolve(projectDir, 'release', 'security.supplychain.report.json');
|
|
97
|
+
|
|
98
|
+
if (!existsSync(secretsPath) || !existsSync(vulnPath) || !existsSync(supplyChainPath)) {
|
|
99
|
+
blockers.push('Security reports not found. Run "verax security:check" first.');
|
|
100
|
+
} else {
|
|
101
|
+
try {
|
|
102
|
+
// Check secrets
|
|
103
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
104
|
+
const secretsReport = JSON.parse(readFileSync(secretsPath, 'utf-8'));
|
|
105
|
+
if (secretsReport.hasSecrets) {
|
|
106
|
+
blockers.push(`SECURITY-BLOCKED: Secrets detected (${secretsReport.summary.total} finding(s))`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check vulnerabilities
|
|
110
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
111
|
+
const vulnReport = JSON.parse(readFileSync(vulnPath, 'utf-8'));
|
|
112
|
+
if (vulnReport.blocking) {
|
|
113
|
+
blockers.push(`SECURITY-BLOCKED: Critical/High vulnerabilities detected (${vulnReport.summary.critical + vulnReport.summary.high} total)`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check supply-chain
|
|
117
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
118
|
+
const supplyChainReport = JSON.parse(readFileSync(supplyChainPath, 'utf-8'));
|
|
119
|
+
if (!supplyChainReport.ok) {
|
|
120
|
+
blockers.push(`SECURITY-BLOCKED: Supply-chain violations (${supplyChainReport.summary.totalViolations} violation(s))`);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
blockers.push(`Security check failed: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 6. Check Performance (PHASE 21.9) - BLOCKING perf violations block release
|
|
128
|
+
const runId = findLatestRunId(projectDir);
|
|
129
|
+
if (runId) {
|
|
130
|
+
try {
|
|
131
|
+
const { checkPerformanceStatus } = await import('../perf/perf.enforcer.js');
|
|
132
|
+
const perfCheck = checkPerformanceStatus(projectDir, runId);
|
|
133
|
+
|
|
134
|
+
if (perfCheck.exists && !perfCheck.ok) {
|
|
135
|
+
blockers.push(`PERFORMANCE-BLOCKED: ${perfCheck.blockers.join('; ')}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// Performance check failure is not a blocker (may be from old runs)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 7. Baseline Freeze Enforcement (PHASE 21.11)
|
|
143
|
+
// After GA, baseline must be frozen and unchanged
|
|
144
|
+
if (isBaselineFrozen(projectDir)) {
|
|
145
|
+
const failureLedger = new FailureLedger(projectDir, runId || 'unknown');
|
|
146
|
+
const baselineCheck = enforceBaseline(projectDir, failureLedger);
|
|
147
|
+
if (baselineCheck.blocked) {
|
|
148
|
+
blockers.push(`BASELINE-DRIFT: ${baselineCheck.message}. Changes to core contracts/policies after GA require MAJOR version bump and baseline regeneration.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If any blockers, throw error
|
|
153
|
+
if (blockers.length > 0) {
|
|
154
|
+
const message = `Cannot ${operation}: RELEASE-BLOCKED. ${blockers.join('; ')}`;
|
|
155
|
+
const error = new Error(message);
|
|
156
|
+
/** @type {any} */
|
|
157
|
+
const e = error;
|
|
158
|
+
e.code = FAILURE_CODE.INTERNAL_UNEXPECTED_ERROR;
|
|
159
|
+
e.component = 'release.enforcer';
|
|
160
|
+
e.context = { operation, blockers };
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.7 — Reproducibility Check
|
|
3
|
+
*
|
|
4
|
+
* Verifies that same commit + same policies = same hashes.
|
|
5
|
+
* Difference = NON_REPRODUCIBLE (BLOCKING for GA).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get current git commit
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectDir - Project directory
|
|
17
|
+
* @returns {string|null} Commit hash or null
|
|
18
|
+
*/
|
|
19
|
+
function getGitCommit(projectDir) {
|
|
20
|
+
try {
|
|
21
|
+
const result = execSync('git rev-parse HEAD', {
|
|
22
|
+
cwd: projectDir,
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
25
|
+
});
|
|
26
|
+
return result.trim();
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get policy hashes
|
|
34
|
+
*
|
|
35
|
+
* @param {string} projectDir - Project directory
|
|
36
|
+
* @returns {Promise<Object>} Policy hashes
|
|
37
|
+
*/
|
|
38
|
+
async function getPolicyHashes(projectDir) {
|
|
39
|
+
try {
|
|
40
|
+
const { loadGuardrailsPolicy } = await import('../guardrails/policy.loader.js');
|
|
41
|
+
const { loadConfidencePolicy } = await import('../confidence/confidence.loader.js');
|
|
42
|
+
|
|
43
|
+
const guardrails = loadGuardrailsPolicy(null, projectDir);
|
|
44
|
+
const confidence = loadConfidencePolicy(null, projectDir);
|
|
45
|
+
|
|
46
|
+
const guardrailsHash = createHash('sha256')
|
|
47
|
+
.update(JSON.stringify(guardrails, null, 0))
|
|
48
|
+
.digest('hex');
|
|
49
|
+
|
|
50
|
+
const confidenceHash = createHash('sha256')
|
|
51
|
+
.update(JSON.stringify(confidence, null, 0))
|
|
52
|
+
.digest('hex');
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
guardrails: guardrailsHash,
|
|
56
|
+
confidence: confidenceHash
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return {
|
|
60
|
+
guardrails: null,
|
|
61
|
+
confidence: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get artifact hashes
|
|
68
|
+
*
|
|
69
|
+
* @param {string} projectDir - Project directory
|
|
70
|
+
* @returns {Object} Artifact hashes
|
|
71
|
+
*/
|
|
72
|
+
function getArtifactHashes(projectDir) {
|
|
73
|
+
const hashes = {};
|
|
74
|
+
|
|
75
|
+
// Hash key files
|
|
76
|
+
const keyFiles = [
|
|
77
|
+
'package.json',
|
|
78
|
+
'bin/verax.js',
|
|
79
|
+
'src/cli/entry.js'
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const file of keyFiles) {
|
|
83
|
+
const filePath = resolve(projectDir, file);
|
|
84
|
+
if (existsSync(filePath)) {
|
|
85
|
+
try {
|
|
86
|
+
const content = readFileSync(filePath);
|
|
87
|
+
hashes[file] = createHash('sha256').update(content).digest('hex');
|
|
88
|
+
} catch {
|
|
89
|
+
hashes[file] = null;
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
hashes[file] = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return hashes;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Load previous reproducibility report
|
|
101
|
+
*
|
|
102
|
+
* @param {string} projectDir - Project directory
|
|
103
|
+
* @returns {Object|null} Previous report or null
|
|
104
|
+
*/
|
|
105
|
+
function loadPreviousReport(projectDir) {
|
|
106
|
+
const reportPath = resolve(projectDir, 'release', 'reproducibility.report.json');
|
|
107
|
+
if (!existsSync(reportPath)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const content = readFileSync(reportPath, 'utf-8');
|
|
113
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
114
|
+
return JSON.parse(content);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check reproducibility
|
|
122
|
+
*
|
|
123
|
+
* @param {string} projectDir - Project directory
|
|
124
|
+
* @returns {Promise<Object>} Reproducibility check result
|
|
125
|
+
*/
|
|
126
|
+
export async function checkReproducibility(projectDir) {
|
|
127
|
+
const gitCommit = getGitCommit(projectDir);
|
|
128
|
+
const policyHashes = await getPolicyHashes(projectDir);
|
|
129
|
+
const artifactHashes = getArtifactHashes(projectDir);
|
|
130
|
+
|
|
131
|
+
const current = {
|
|
132
|
+
gitCommit,
|
|
133
|
+
policies: policyHashes,
|
|
134
|
+
artifacts: artifactHashes,
|
|
135
|
+
checkedAt: new Date().toISOString()
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const previous = loadPreviousReport(projectDir);
|
|
139
|
+
|
|
140
|
+
let reproducible = true;
|
|
141
|
+
const differences = [];
|
|
142
|
+
|
|
143
|
+
if (previous) {
|
|
144
|
+
// Compare with previous build
|
|
145
|
+
if (previous.gitCommit !== gitCommit) {
|
|
146
|
+
reproducible = false;
|
|
147
|
+
differences.push({
|
|
148
|
+
type: 'git_commit',
|
|
149
|
+
previous: previous.gitCommit,
|
|
150
|
+
current: gitCommit,
|
|
151
|
+
message: 'Git commit changed'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (previous.policies?.guardrails !== policyHashes.guardrails) {
|
|
156
|
+
reproducible = false;
|
|
157
|
+
differences.push({
|
|
158
|
+
type: 'guardrails_policy',
|
|
159
|
+
previous: previous.policies?.guardrails,
|
|
160
|
+
current: policyHashes.guardrails,
|
|
161
|
+
message: 'Guardrails policy changed'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (previous.policies?.confidence !== policyHashes.confidence) {
|
|
166
|
+
reproducible = false;
|
|
167
|
+
differences.push({
|
|
168
|
+
type: 'confidence_policy',
|
|
169
|
+
previous: previous.policies?.confidence,
|
|
170
|
+
current: policyHashes.confidence,
|
|
171
|
+
message: 'Confidence policy changed'
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Compare artifact hashes
|
|
176
|
+
for (const [file, hash] of Object.entries(artifactHashes)) {
|
|
177
|
+
if (previous.artifacts?.[file] !== hash) {
|
|
178
|
+
reproducible = false;
|
|
179
|
+
differences.push({
|
|
180
|
+
type: 'artifact',
|
|
181
|
+
file,
|
|
182
|
+
previous: previous.artifacts?.[file],
|
|
183
|
+
current: hash,
|
|
184
|
+
message: `Artifact ${file} changed`
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const verdict = reproducible ? 'REPRODUCIBLE' : 'NON_REPRODUCIBLE';
|
|
191
|
+
|
|
192
|
+
const report = {
|
|
193
|
+
verdict,
|
|
194
|
+
reproducible,
|
|
195
|
+
differences,
|
|
196
|
+
current,
|
|
197
|
+
previous: previous || null,
|
|
198
|
+
checkedAt: new Date().toISOString()
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return report;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Write reproducibility report
|
|
206
|
+
*
|
|
207
|
+
* @param {string} projectDir - Project directory
|
|
208
|
+
* @param {Object} report - Reproducibility report
|
|
209
|
+
* @returns {string} Path to written file
|
|
210
|
+
*/
|
|
211
|
+
export function writeReproducibilityReport(projectDir, report) {
|
|
212
|
+
const outputDir = resolve(projectDir, 'release');
|
|
213
|
+
if (!existsSync(outputDir)) {
|
|
214
|
+
mkdirSync(outputDir, { recursive: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const outputPath = resolve(outputDir, 'reproducibility.report.json');
|
|
218
|
+
writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
219
|
+
|
|
220
|
+
return outputPath;
|
|
221
|
+
}
|
|
222
|
+
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHASE 21.7 — SBOM Builder
|
|
3
|
+
*
|
|
4
|
+
* Generates Software Bill of Materials (SBOM) in CycloneDX format.
|
|
5
|
+
* Missing SBOM = BLOCKING.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get package.json dependencies
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectDir - Project directory
|
|
17
|
+
* @returns {Object} Dependencies object
|
|
18
|
+
*/
|
|
19
|
+
function getDependencies(projectDir) {
|
|
20
|
+
try {
|
|
21
|
+
const pkgPath = resolve(projectDir, 'package.json');
|
|
22
|
+
if (!existsSync(pkgPath)) {
|
|
23
|
+
return { dependencies: {}, devDependencies: {} };
|
|
24
|
+
}
|
|
25
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
27
|
+
return {
|
|
28
|
+
dependencies: pkg.dependencies || {},
|
|
29
|
+
devDependencies: pkg.devDependencies || {}
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return { dependencies: {}, devDependencies: {} };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Helper to traverse dependency tree recursively
|
|
38
|
+
*/
|
|
39
|
+
function traverseDepsHelper(deps, packages, parent = null) {
|
|
40
|
+
if (!deps || typeof deps !== 'object') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
45
|
+
if (info && typeof info === 'object' && info.version) {
|
|
46
|
+
packages.push({
|
|
47
|
+
name,
|
|
48
|
+
version: info.version,
|
|
49
|
+
parent: parent || null
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (info.dependencies) {
|
|
53
|
+
traverseDepsHelper(info.dependencies, packages, name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get transitive dependencies from node_modules
|
|
61
|
+
*
|
|
62
|
+
* @param {string} projectDir - Project directory
|
|
63
|
+
* @returns {Array} Array of package info
|
|
64
|
+
*/
|
|
65
|
+
function getTransitiveDependencies(projectDir) {
|
|
66
|
+
const packages = [];
|
|
67
|
+
const nodeModulesPath = resolve(projectDir, 'node_modules');
|
|
68
|
+
|
|
69
|
+
if (!existsSync(nodeModulesPath)) {
|
|
70
|
+
return packages;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Use npm ls to get dependency tree (with timeout to prevent hanging)
|
|
75
|
+
const result = execSync('npm ls --all --json', {
|
|
76
|
+
cwd: projectDir,
|
|
77
|
+
encoding: 'utf-8',
|
|
78
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
79
|
+
timeout: 5000 // 5 second timeout
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const tree = JSON.parse(result);
|
|
83
|
+
|
|
84
|
+
if (tree.dependencies) {
|
|
85
|
+
traverseDepsHelper(tree.dependencies, packages);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Fallback: scan node_modules directory
|
|
89
|
+
try {
|
|
90
|
+
const entries = readdirSync(nodeModulesPath, { withFileTypes: true });
|
|
91
|
+
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
94
|
+
const pkgPath = resolve(nodeModulesPath, entry.name, 'package.json');
|
|
95
|
+
if (existsSync(pkgPath)) {
|
|
96
|
+
try {
|
|
97
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
98
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
99
|
+
packages.push({
|
|
100
|
+
name: pkg.name || entry.name,
|
|
101
|
+
version: pkg.version || 'unknown',
|
|
102
|
+
parent: null
|
|
103
|
+
});
|
|
104
|
+
} catch {
|
|
105
|
+
// Skip invalid packages
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// If scanning fails, return empty
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return packages;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get license from package.json
|
|
120
|
+
*
|
|
121
|
+
* @param {string} packagePath - Path to package.json
|
|
122
|
+
* @returns {string|null} License or null
|
|
123
|
+
*/
|
|
124
|
+
function getPackageLicense(packagePath) {
|
|
125
|
+
try {
|
|
126
|
+
if (!existsSync(packagePath)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
130
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
131
|
+
if (typeof pkg.license === 'string') {
|
|
132
|
+
return pkg.license;
|
|
133
|
+
} else if (pkg.license && pkg.license.type) {
|
|
134
|
+
return pkg.license.type;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compute integrity hash for a package
|
|
144
|
+
*
|
|
145
|
+
* @param {string} projectDir - Project directory
|
|
146
|
+
* @param {string} packageName - Package name
|
|
147
|
+
* @returns {string|null} SHA256 hash or null
|
|
148
|
+
*/
|
|
149
|
+
function getPackageIntegrity(projectDir, packageName) {
|
|
150
|
+
try {
|
|
151
|
+
const packagePath = resolve(projectDir, 'node_modules', packageName);
|
|
152
|
+
if (!existsSync(packagePath)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Hash the package.json as a proxy for package integrity
|
|
157
|
+
const pkgPath = resolve(packagePath, 'package.json');
|
|
158
|
+
if (existsSync(pkgPath)) {
|
|
159
|
+
const content = readFileSync(pkgPath);
|
|
160
|
+
// @ts-expect-error - digest returns string
|
|
161
|
+
return createHash('sha256').update(content).digest('hex');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build SBOM in CycloneDX format
|
|
172
|
+
*
|
|
173
|
+
* @param {string} projectDir - Project directory
|
|
174
|
+
* @returns {Promise<Object>} SBOM object
|
|
175
|
+
*/
|
|
176
|
+
export async function buildSBOM(projectDir) {
|
|
177
|
+
const pkgPath = resolve(projectDir, 'package.json');
|
|
178
|
+
if (!existsSync(pkgPath)) {
|
|
179
|
+
throw new Error('Cannot build SBOM: package.json not found');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
183
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
184
|
+
const deps = getDependencies(projectDir);
|
|
185
|
+
const transitive = getTransitiveDependencies(projectDir);
|
|
186
|
+
|
|
187
|
+
// Build components list
|
|
188
|
+
const components = [];
|
|
189
|
+
|
|
190
|
+
// Add main package
|
|
191
|
+
components.push({
|
|
192
|
+
type: 'application',
|
|
193
|
+
name: pkg.name || 'unknown',
|
|
194
|
+
version: pkg.version || 'unknown',
|
|
195
|
+
purl: `pkg:npm/${pkg.name}@${pkg.version}`,
|
|
196
|
+
licenses: pkg.license ? [{ license: { id: pkg.license } }] : []
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Add direct dependencies
|
|
200
|
+
for (const [name, version] of Object.entries(deps.dependencies)) {
|
|
201
|
+
const integrity = getPackageIntegrity(projectDir, name);
|
|
202
|
+
const license = getPackageLicense(resolve(projectDir, 'node_modules', name, 'package.json'));
|
|
203
|
+
|
|
204
|
+
components.push({
|
|
205
|
+
type: 'library',
|
|
206
|
+
name,
|
|
207
|
+
version: version.replace(/^[\^~]/, ''),
|
|
208
|
+
purl: `pkg:npm/${name}@${version.replace(/^[\^~]/, '')}`,
|
|
209
|
+
hashes: integrity ? [{ alg: 'SHA-256', content: integrity }] : [],
|
|
210
|
+
licenses: license ? [{ license: { id: license } }] : []
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Add dev dependencies (marked as development)
|
|
215
|
+
for (const [name, version] of Object.entries(deps.devDependencies)) {
|
|
216
|
+
const integrity = getPackageIntegrity(projectDir, name);
|
|
217
|
+
const license = getPackageLicense(resolve(projectDir, 'node_modules', name, 'package.json'));
|
|
218
|
+
|
|
219
|
+
components.push({
|
|
220
|
+
type: 'library',
|
|
221
|
+
name,
|
|
222
|
+
version: version.replace(/^[\^~]/, ''),
|
|
223
|
+
purl: `pkg:npm/${name}@${version.replace(/^[\^~]/, '')}`,
|
|
224
|
+
hashes: integrity ? [{ alg: 'SHA-256', content: integrity }] : [],
|
|
225
|
+
licenses: license ? [{ license: { id: license } }] : [],
|
|
226
|
+
scope: 'development'
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Add transitive dependencies (simplified - in production, use proper dependency resolution)
|
|
231
|
+
const transitiveMap = new Map();
|
|
232
|
+
for (const trans of transitive) {
|
|
233
|
+
const key = `${trans.name}@${trans.version}`;
|
|
234
|
+
if (!transitiveMap.has(key)) {
|
|
235
|
+
transitiveMap.set(key, trans);
|
|
236
|
+
|
|
237
|
+
const integrity = getPackageIntegrity(projectDir, trans.name);
|
|
238
|
+
const license = getPackageLicense(resolve(projectDir, 'node_modules', trans.name, 'package.json'));
|
|
239
|
+
|
|
240
|
+
components.push({
|
|
241
|
+
type: 'library',
|
|
242
|
+
name: trans.name,
|
|
243
|
+
version: trans.version,
|
|
244
|
+
purl: `pkg:npm/${trans.name}@${trans.version}`,
|
|
245
|
+
hashes: integrity ? [{ alg: 'SHA-256', content: integrity }] : [],
|
|
246
|
+
licenses: license ? [{ license: { id: license } }] : []
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const sbom = {
|
|
252
|
+
bomFormat: 'CycloneDX',
|
|
253
|
+
specVersion: '1.4',
|
|
254
|
+
version: 1,
|
|
255
|
+
metadata: {
|
|
256
|
+
timestamp: new Date().toISOString(),
|
|
257
|
+
tools: [{
|
|
258
|
+
vendor: 'VERAX',
|
|
259
|
+
name: 'SBOM Builder',
|
|
260
|
+
version: '1.0.0'
|
|
261
|
+
}],
|
|
262
|
+
component: {
|
|
263
|
+
type: 'application',
|
|
264
|
+
name: pkg.name || 'unknown',
|
|
265
|
+
version: pkg.version || 'unknown'
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
components: components
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return sbom;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Write SBOM to file
|
|
276
|
+
*
|
|
277
|
+
* @param {string} projectDir - Project directory
|
|
278
|
+
* @param {Object} sbom - SBOM object
|
|
279
|
+
* @returns {string} Path to written file
|
|
280
|
+
*/
|
|
281
|
+
export function writeSBOM(projectDir, sbom) {
|
|
282
|
+
const outputDir = resolve(projectDir, 'release');
|
|
283
|
+
if (!existsSync(outputDir)) {
|
|
284
|
+
mkdirSync(outputDir, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const outputPath = resolve(outputDir, 'sbom.json');
|
|
288
|
+
writeFileSync(outputPath, JSON.stringify(sbom, null, 2), 'utf-8');
|
|
289
|
+
|
|
290
|
+
return outputPath;
|
|
291
|
+
}
|
|
292
|
+
|
|
@@ -292,7 +292,9 @@ export function validateReplay(baselinePath, currentPath) {
|
|
|
292
292
|
throw new Error(`Current decisions not found: ${currentPath}`);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
295
296
|
const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
297
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
296
298
|
const current = JSON.parse(fs.readFileSync(currentPath, 'utf-8'));
|
|
297
299
|
|
|
298
300
|
return compareReplayDecisions(baseline, current);
|