@nebulacomponents/citable 1.19.0 → 1.20.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/CHANGELOG.md +18 -0
- package/README.md +12 -1
- package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +15 -12
- package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +15 -12
- package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +15 -12
- package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +15 -12
- package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.github/skills/citable/SKILL.md +11 -2
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +15 -12
- package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +15 -12
- package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +15 -12
- package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +15 -12
- package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +15 -12
- package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +15 -12
- package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/manifest.json +184 -148
- package/package.json +1 -1
- package/schemas/action-plan.schema.json +38 -1
- package/schemas/audit-coverage.schema.json +118 -0
- package/schemas/collection-result.schema.json +35 -0
- package/schemas/config.schema.json +2 -0
- package/schemas/cro-report.schema.json +8 -1
- package/schemas/finding.schema.json +105 -2
- package/schemas/remediation-verification.schema.json +12 -1
- package/schemas/run.schema.json +15 -2
- package/schemas/search-report.schema.json +8 -1
- package/schemas/summary.schema.json +41 -0
- package/skill/SKILL.md +11 -2
- package/src/acceptance/reproducibility.js +5 -25
- package/src/analysis/croRoadmap.js +3 -3
- package/src/analysis/strategicRoadmap.js +3 -3
- package/src/artifacts/interchange.js +2 -6
- package/src/cli/index.js +15 -3
- package/src/commands/actionPlan.js +46 -5
- package/src/commands/applyRemediation.js +5 -5
- package/src/commands/audit.js +75 -8
- package/src/commands/ciWorkflow.js +7 -2
- package/src/commands/compareSnapshots.js +155 -47
- package/src/commands/connect.js +24 -4
- package/src/commands/context.js +41 -2
- package/src/commands/croSuite.js +51 -13
- package/src/commands/delivery.js +7 -7
- package/src/commands/governance.js +9 -10
- package/src/commands/implementationKit.js +2 -1
- package/src/commands/init.js +8 -1
- package/src/commands/previewCro.js +1 -1
- package/src/commands/prioritize.js +3 -2
- package/src/commands/reviews.js +31 -5
- package/src/commands/roadmapCmd.js +7 -13
- package/src/commands/sweep.js +15 -2
- package/src/commands/verifyRemediation.js +94 -19
- package/src/connectors/collectionResult.js +80 -0
- package/src/connectors/ga4.js +46 -11
- package/src/connectors/gsc.js +41 -6
- package/src/connectors/index.js +1 -1
- package/src/connectors/webflow.js +140 -10
- package/src/connectors/wordpress.js +92 -8
- package/src/crawler/fetch.js +220 -24
- package/src/crawler/resourceValidity.js +119 -0
- package/src/crawler/sitemap.js +435 -16
- package/src/crawler/sitemapCollector.js +364 -0
- package/src/crawler/urlIdentity.js +97 -0
- package/src/detectors/agent.js +11 -0
- package/src/detectors/ans.js +16 -1
- package/src/detectors/arch.js +6 -0
- package/src/detectors/claim.js +9 -0
- package/src/detectors/crawl.js +7 -0
- package/src/detectors/cro.js +21 -0
- package/src/detectors/cwv.js +4 -0
- package/src/detectors/entity.js +7 -0
- package/src/detectors/evd.js +9 -0
- package/src/detectors/framework.js +28 -2
- package/src/detectors/geoReco.js +16 -0
- package/src/detectors/hreflang.js +3 -0
- package/src/detectors/lifeMeas.js +9 -0
- package/src/detectors/link.js +6 -1
- package/src/detectors/page.js +11 -0
- package/src/detectors/schemaData.js +16 -1
- package/src/detectors/tech.js +34 -3
- package/src/evidence/coverage.js +448 -0
- package/src/evidence/determination.js +157 -0
- package/src/evidence/downstream.js +110 -0
- package/src/evidence/hashes.js +109 -0
- package/src/evidence/run.js +68 -8
- package/src/extractor/page.js +29 -2
- package/src/extractor/site.js +298 -50
- package/src/reporting/executiveCroReport.js +36 -13
- package/src/reporting/executiveExport.js +45 -16
- package/src/reporting/executiveSearchReport.js +56 -22
- package/src/reporting/report.js +56 -9
- package/src/shared/epistemicLanguage.js +86 -0
- package/src/shared/evidenceSourceResolver.js +48 -17
- package/src/shared/htmlEscape.js +8 -4
- package/src/shared/runPackageVerifier.js +99 -8
- package/src/shared/verifiedRunLoader.js +129 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { sha256 } from './io.js';
|
|
4
4
|
import { validateAgainst } from './schemaValidator.js';
|
|
5
5
|
|
|
6
6
|
export class RunVerificationError extends Error {
|
|
@@ -12,6 +12,17 @@ export class RunVerificationError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function readStableBytes(file, runDir, relative) {
|
|
16
|
+
const first = fs.readFileSync(file);
|
|
17
|
+
const second = fs.readFileSync(file);
|
|
18
|
+
if (!first.equals(second)) {
|
|
19
|
+
throw new RunVerificationError(`Artifact changed while being verified: ${relative}`, {
|
|
20
|
+
code: 'PACKAGE_MUTATION', runDir, file: relative,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return first;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
/**
|
|
16
27
|
* Verifies the cryptographic and structural integrity of an audit run evidence package.
|
|
17
28
|
*
|
|
@@ -28,13 +39,24 @@ export function verifyRunPackage(runDir, options = {}) {
|
|
|
28
39
|
requireFindings = true,
|
|
29
40
|
requireCompleted = false,
|
|
30
41
|
requireChecksums = true,
|
|
42
|
+
ignoredFiles = [],
|
|
31
43
|
} = options;
|
|
32
44
|
|
|
33
45
|
if (!runDir || !fs.existsSync(runDir)) {
|
|
34
46
|
throw new RunVerificationError(`Run directory not found: ${runDir}`, { code: 'RUN_NOT_FOUND', runDir });
|
|
35
47
|
}
|
|
48
|
+
let rootStat;
|
|
49
|
+
try { rootStat = fs.lstatSync(runDir); } catch (error) {
|
|
50
|
+
throw new RunVerificationError(`Run directory cannot be inspected: ${error.message}`, { code: 'RUN_NOT_FOUND', runDir });
|
|
51
|
+
}
|
|
52
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
53
|
+
throw new RunVerificationError(`Run path is not a real directory: ${runDir}`, { code: 'RUN_PATH_INVALID', runDir });
|
|
54
|
+
}
|
|
36
55
|
|
|
37
56
|
const manifestPath = path.join(runDir, 'manifest.json');
|
|
57
|
+
if (fs.existsSync(manifestPath) && fs.lstatSync(manifestPath).isSymbolicLink()) {
|
|
58
|
+
throw new RunVerificationError('Run manifest.json is a symbolic link', { code: 'PACKAGE_SYMLINK', runDir, file: 'manifest.json' });
|
|
59
|
+
}
|
|
38
60
|
if (!fs.existsSync(manifestPath)) {
|
|
39
61
|
throw new RunVerificationError(`Run package is missing manifest.json at ${manifestPath}`, {
|
|
40
62
|
code: 'MANIFEST_MISSING',
|
|
@@ -44,7 +66,7 @@ export function verifyRunPackage(runDir, options = {}) {
|
|
|
44
66
|
|
|
45
67
|
let manifest;
|
|
46
68
|
try {
|
|
47
|
-
manifest =
|
|
69
|
+
manifest = JSON.parse(readStableBytes(manifestPath, runDir, 'manifest.json').toString('utf8'));
|
|
48
70
|
} catch (err) {
|
|
49
71
|
throw new RunVerificationError(`Run manifest.json is invalid JSON: ${err.message}`, {
|
|
50
72
|
code: 'MANIFEST_INVALID',
|
|
@@ -53,6 +75,12 @@ export function verifyRunPackage(runDir, options = {}) {
|
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
const schemaValidation = validateAgainst('run.schema.json', manifest);
|
|
78
|
+
if (Object.prototype.hasOwnProperty.call(manifest, 'schema_version')
|
|
79
|
+
&& ![1, 2].includes(manifest.schema_version)) {
|
|
80
|
+
throw new RunVerificationError(`Unsupported run manifest schema_version: ${String(manifest.schema_version)}`, {
|
|
81
|
+
code: 'MANIFEST_SCHEMA_UNSUPPORTED', runDir, schema_version: manifest.schema_version,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
56
84
|
if (!schemaValidation.valid) {
|
|
57
85
|
throw new RunVerificationError(`Run manifest violates run.schema.json: ${schemaValidation.errors.join('; ')}`, {
|
|
58
86
|
code: 'MANIFEST_SCHEMA_INVALID',
|
|
@@ -69,42 +97,94 @@ export function verifyRunPackage(runDir, options = {}) {
|
|
|
69
97
|
});
|
|
70
98
|
}
|
|
71
99
|
|
|
100
|
+
// A sealed run is closed-world: every regular file except the checksum
|
|
101
|
+
// index itself must be listed, and every listed path must be safe. Do not
|
|
102
|
+
// use stat() here because it follows symlinks and could verify bytes outside
|
|
103
|
+
// the package directory.
|
|
104
|
+
const packageFiles = [];
|
|
105
|
+
const walk = (directory) => {
|
|
106
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
107
|
+
const absolute = path.join(directory, entry.name);
|
|
108
|
+
const relative = path.relative(runDir, absolute).split(path.sep).join('/');
|
|
109
|
+
if (entry.isSymbolicLink()) {
|
|
110
|
+
throw new RunVerificationError(`Run package contains symbolic link: ${relative}`, {
|
|
111
|
+
code: 'PACKAGE_SYMLINK', runDir, file: relative,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (entry.isDirectory()) walk(absolute);
|
|
115
|
+
else if (entry.isFile()) packageFiles.push(relative);
|
|
116
|
+
else throw new RunVerificationError(`Run package contains unsupported filesystem entry: ${relative}`, {
|
|
117
|
+
code: 'PACKAGE_ENTRY_INVALID', runDir, file: relative,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
walk(runDir);
|
|
122
|
+
const ignored = new Set(ignoredFiles);
|
|
123
|
+
|
|
72
124
|
// Verify checksums.json
|
|
73
125
|
const checksumsPath = path.join(runDir, 'checksums.json');
|
|
74
126
|
let checksumsVerified = false;
|
|
127
|
+
let artifactHashes = {};
|
|
128
|
+
const artifactBytes = new Map();
|
|
129
|
+
const retainedArtifactNames = new Set(['findings.json', 'coverage.json', 'summary.json']);
|
|
75
130
|
if (fs.existsSync(checksumsPath)) {
|
|
131
|
+
if (fs.lstatSync(checksumsPath).isSymbolicLink()) throw new RunVerificationError('checksums.json is a symbolic link', { code: 'PACKAGE_SYMLINK', runDir, file: 'checksums.json' });
|
|
76
132
|
let checksums;
|
|
77
133
|
try {
|
|
78
|
-
checksums =
|
|
134
|
+
checksums = JSON.parse(readStableBytes(checksumsPath, runDir, 'checksums.json').toString('utf8'));
|
|
79
135
|
} catch (err) {
|
|
80
136
|
throw new RunVerificationError(`checksums.json is invalid JSON: ${err.message}`, {
|
|
81
137
|
code: 'CHECKSUMS_INVALID',
|
|
82
138
|
runDir,
|
|
83
139
|
});
|
|
84
140
|
}
|
|
141
|
+
artifactHashes = checksums;
|
|
85
142
|
|
|
86
143
|
const tamperedFiles = [];
|
|
87
144
|
const missingFiles = [];
|
|
88
145
|
|
|
146
|
+
const safePath = (value) => typeof value === 'string'
|
|
147
|
+
&& value.length > 0
|
|
148
|
+
&& !path.isAbsolute(value)
|
|
149
|
+
&& !value.includes('\\')
|
|
150
|
+
&& !value.split('/').includes('..')
|
|
151
|
+
&& !value.split('/').includes('')
|
|
152
|
+
&& !/^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
89
153
|
for (const [relPath, expectedHash] of Object.entries(checksums)) {
|
|
154
|
+
if (!safePath(relPath) || relPath === 'checksums.json') {
|
|
155
|
+
throw new RunVerificationError(`unsafe checksum path (escapes the run package): ${relPath}`, {
|
|
156
|
+
code: 'CHECKSUM_PATH_INVALID', runDir, file: relPath,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
90
159
|
const artifactPath = path.join(runDir, relPath);
|
|
91
160
|
if (!fs.existsSync(artifactPath)) {
|
|
92
161
|
missingFiles.push(relPath);
|
|
93
162
|
continue;
|
|
94
163
|
}
|
|
95
|
-
const
|
|
164
|
+
const artifactStat = fs.lstatSync(artifactPath);
|
|
165
|
+
if (artifactStat.isSymbolicLink() || !artifactStat.isFile()) {
|
|
166
|
+
throw new RunVerificationError(`checksum artifact is not a regular file: ${relPath}`, { code: 'PACKAGE_SYMLINK', runDir, file: relPath });
|
|
167
|
+
}
|
|
168
|
+
const bytes = readStableBytes(artifactPath, runDir, relPath);
|
|
169
|
+
if (retainedArtifactNames.has(relPath)) artifactBytes.set(relPath, bytes);
|
|
170
|
+
const actualHash = sha256(bytes);
|
|
96
171
|
if (actualHash !== expectedHash) {
|
|
97
172
|
tamperedFiles.push({ file: relPath, expected: expectedHash, actual: actualHash });
|
|
98
173
|
}
|
|
99
174
|
}
|
|
100
175
|
|
|
101
|
-
|
|
176
|
+
const expectedFiles = Object.keys(checksums).sort();
|
|
177
|
+
const actualFiles = packageFiles.filter((file) => file !== 'checksums.json' && !ignored.has(file)).sort();
|
|
178
|
+
const unexpectedFiles = actualFiles.filter((file) => !expectedFiles.includes(file));
|
|
179
|
+
const unlistedFiles = expectedFiles.filter((file) => !actualFiles.includes(file));
|
|
180
|
+
if (tamperedFiles.length > 0 || missingFiles.length > 0 || unexpectedFiles.length > 0 || unlistedFiles.length > 0) {
|
|
102
181
|
throw new RunVerificationError(
|
|
103
|
-
`Cryptographic verification failed for run package: ${tamperedFiles.length} tampered files, ${missingFiles.length} missing files`,
|
|
182
|
+
`Cryptographic verification failed for run package (checksum mismatch; integrity failed; completeness failure; unsealed): ${tamperedFiles.length} tampered files, ${missingFiles.length + unlistedFiles.length} missing files, ${unexpectedFiles.length} unexpected files`,
|
|
104
183
|
{
|
|
105
184
|
code: 'CHECKSUM_MISMATCH',
|
|
106
185
|
tamperedFiles,
|
|
107
|
-
missingFiles,
|
|
186
|
+
missingFiles: [...missingFiles, ...unlistedFiles],
|
|
187
|
+
unexpectedFiles,
|
|
108
188
|
runDir,
|
|
109
189
|
}
|
|
110
190
|
);
|
|
@@ -122,10 +202,19 @@ export function verifyRunPackage(runDir, options = {}) {
|
|
|
122
202
|
let findings = null;
|
|
123
203
|
if (fs.existsSync(findingsPath)) {
|
|
124
204
|
try {
|
|
125
|
-
|
|
205
|
+
if (fs.lstatSync(findingsPath).isSymbolicLink()) throw new Error('findings.json is a symbolic link');
|
|
206
|
+
const findingsBytes = artifactBytes.get('findings.json') || readStableBytes(findingsPath, runDir, 'findings.json');
|
|
207
|
+
artifactBytes.set('findings.json', findingsBytes);
|
|
208
|
+
findings = JSON.parse(findingsBytes.toString('utf8'));
|
|
126
209
|
if (!Array.isArray(findings)) {
|
|
127
210
|
throw new Error('findings.json must contain an array of findings');
|
|
128
211
|
}
|
|
212
|
+
for (const [index, finding] of findings.entries()) {
|
|
213
|
+
const findingValidation = validateAgainst('finding.schema.json', finding);
|
|
214
|
+
if (!findingValidation.valid) {
|
|
215
|
+
throw new Error(`finding[${index}] violates finding.schema.json: ${findingValidation.errors.join('; ')}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
129
218
|
} catch (err) {
|
|
130
219
|
throw new RunVerificationError(`findings.json is malformed: ${err.message}`, {
|
|
131
220
|
code: 'FINDINGS_MALFORMED',
|
|
@@ -149,5 +238,7 @@ export function verifyRunPackage(runDir, options = {}) {
|
|
|
149
238
|
findingsPath: fs.existsSync(findingsPath) ? findingsPath : null,
|
|
150
239
|
findingsCount: Array.isArray(findings) ? findings.length : 0,
|
|
151
240
|
findings,
|
|
241
|
+
artifactHashes,
|
|
242
|
+
artifactBytes,
|
|
152
243
|
};
|
|
153
244
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJson, sha256File } from './io.js';
|
|
4
|
+
import { verifyRunPackage, RunVerificationError } from './runPackageVerifier.js';
|
|
5
|
+
import { validateAgainst } from './schemaValidator.js';
|
|
6
|
+
|
|
7
|
+
export const VERIFIED_RUN_LOADER_VERSION = 'verified-run-loader-v1';
|
|
8
|
+
|
|
9
|
+
export class VerifiedRunLoadError extends Error {
|
|
10
|
+
constructor(message, details = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'VerifiedRunLoadError';
|
|
13
|
+
this.code = details.code || 'VERIFIED_RUN_LOAD_FAILED';
|
|
14
|
+
this.details = details;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readContractArtifact(runDir, file, schema, { required = true, bytes = null } = {}) {
|
|
19
|
+
const filePath = path.join(runDir, file);
|
|
20
|
+
if (!fs.existsSync(filePath)) {
|
|
21
|
+
if (required) throw new VerifiedRunLoadError(`Verified run is missing ${file}`, { code: 'ARTIFACT_MISSING', file });
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (fs.lstatSync(filePath).isSymbolicLink()) {
|
|
25
|
+
throw new VerifiedRunLoadError(`${file} is a symbolic link`, { code: 'PACKAGE_SYMLINK', file });
|
|
26
|
+
}
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = bytes ? JSON.parse(bytes.toString('utf8')) : readJson(filePath);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new VerifiedRunLoadError(`${file} is invalid JSON: ${error.message}`, { code: 'ARTIFACT_INVALID', file });
|
|
32
|
+
}
|
|
33
|
+
const validation = schema ? validateAgainst(schema, value) : { valid: true, errors: [] };
|
|
34
|
+
if (!validation.valid) {
|
|
35
|
+
throw new VerifiedRunLoadError(`${file} violates ${schema}: ${validation.errors.join('; ')}`, {
|
|
36
|
+
code: 'ARTIFACT_SCHEMA_INVALID', file, errors: validation.errors,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readFindingsArtifact(runDir, bytes = null) {
|
|
43
|
+
const findings = readContractArtifact(runDir, 'findings.json', null, { bytes });
|
|
44
|
+
if (!Array.isArray(findings)) throw new VerifiedRunLoadError('findings.json must contain an array of findings', { code: 'ARTIFACT_SCHEMA_INVALID', file: 'findings.json' });
|
|
45
|
+
for (const [index, finding] of findings.entries()) {
|
|
46
|
+
const validation = validateAgainst('finding.schema.json', finding);
|
|
47
|
+
if (!validation.valid) throw new VerifiedRunLoadError(`findings.json finding[${index}] violates finding.schema.json: ${validation.errors.join('; ')}`, { code: 'ARTIFACT_SCHEMA_INVALID', file: 'findings.json' });
|
|
48
|
+
}
|
|
49
|
+
return findings;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load a run only after its closed-world checksum seal and execution contracts
|
|
54
|
+
* have been verified. Consumers must use this API rather than reading
|
|
55
|
+
* findings.json directly: the returned findings are bound to the verified
|
|
56
|
+
* package, manifest, and (when present) coverage artifact.
|
|
57
|
+
*
|
|
58
|
+
* Legacy packages can be opened explicitly for migration. Their coverage is
|
|
59
|
+
* indeterminate and never receives a synthesized complete default.
|
|
60
|
+
*/
|
|
61
|
+
export function loadVerifiedRun(runDir, {
|
|
62
|
+
requireCompletedExecution = true,
|
|
63
|
+
allowLegacy = false,
|
|
64
|
+
requireCoverage = !allowLegacy,
|
|
65
|
+
} = {}) {
|
|
66
|
+
try {
|
|
67
|
+
let verification;
|
|
68
|
+
try {
|
|
69
|
+
verification = verifyRunPackage(runDir, {
|
|
70
|
+
requireFindings: true,
|
|
71
|
+
requireCompleted: requireCompletedExecution,
|
|
72
|
+
requireChecksums: !allowLegacy,
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!allowLegacy || !['CHECKSUMS_MISSING', 'MANIFEST_MISSING'].includes(error.code)) throw error;
|
|
76
|
+
const manifestPath = path.join(runDir, 'manifest.json');
|
|
77
|
+
const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : { run_id: path.basename(runDir), status: 'incomplete' };
|
|
78
|
+
const findings = readFindingsArtifact(runDir);
|
|
79
|
+
const coverageFile = path.join(runDir, 'coverage.json');
|
|
80
|
+
const coverage = fs.existsSync(coverageFile) ? readContractArtifact(runDir, 'coverage.json', 'audit-coverage.schema.json') : null;
|
|
81
|
+
const summary = readContractArtifact(runDir, 'summary.json', 'summary.schema.json', { required: false });
|
|
82
|
+
return {
|
|
83
|
+
verified: false, verification_version: VERIFIED_RUN_LOADER_VERSION,
|
|
84
|
+
integrity_mode: 'legacy_unverified', package_dir: path.resolve(runDir),
|
|
85
|
+
package_hash: null, manifest, findings, findingsCount: findings.length,
|
|
86
|
+
coverage, summary, legacy: true,
|
|
87
|
+
coverage_status: coverage?.coverage_status || 'indeterminate', determination_status: 'indeterminate',
|
|
88
|
+
checksumsVerified: false,
|
|
89
|
+
artifactHashes: {
|
|
90
|
+
...(fs.existsSync(path.join(runDir, 'manifest.json')) ? { 'manifest.json': sha256File(path.join(runDir, 'manifest.json')) } : {}),
|
|
91
|
+
'findings.json': sha256File(path.join(runDir, 'findings.json')),
|
|
92
|
+
...(fs.existsSync(coverageFile) ? { 'coverage.json': sha256File(coverageFile) } : {}),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const coveragePath = path.join(runDir, 'coverage.json');
|
|
98
|
+
const hasCoverage = fs.existsSync(coveragePath);
|
|
99
|
+
if (!hasCoverage && !allowLegacy && requireCoverage) {
|
|
100
|
+
throw new VerifiedRunLoadError('Verified run is missing coverage.json; use explicit legacy mode to open historical packages', {
|
|
101
|
+
code: 'COVERAGE_MISSING',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const coverage = hasCoverage ? readContractArtifact(runDir, 'coverage.json', 'audit-coverage.schema.json', { bytes: verification.artifactBytes.get('coverage.json') }) : null;
|
|
105
|
+
const summary = readContractArtifact(runDir, 'summary.json', 'summary.schema.json', { required: false, bytes: verification.artifactBytes.get('summary.json') });
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
...verification,
|
|
109
|
+
verified: hasCoverage,
|
|
110
|
+
verification_version: VERIFIED_RUN_LOADER_VERSION,
|
|
111
|
+
integrity_mode: hasCoverage ? 'sealed' : 'legacy_unverified',
|
|
112
|
+
package_dir: path.resolve(runDir),
|
|
113
|
+
package_hash: verification.checksumsVerified ? sha256File(path.join(runDir, 'checksums.json')) : null,
|
|
114
|
+
findings: verification.findings,
|
|
115
|
+
coverage,
|
|
116
|
+
summary,
|
|
117
|
+
legacy: !hasCoverage,
|
|
118
|
+
coverage_status: coverage?.coverage_status || 'indeterminate',
|
|
119
|
+
determination_status: coverage ? verification.manifest.determination_status : 'indeterminate',
|
|
120
|
+
artifactHashes: verification.artifactHashes,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof VerifiedRunLoadError) throw error;
|
|
124
|
+
if (error instanceof RunVerificationError) {
|
|
125
|
+
throw new VerifiedRunLoadError(error.message, { code: error.code, cause: error });
|
|
126
|
+
}
|
|
127
|
+
throw new VerifiedRunLoadError(error.message, { cause: error });
|
|
128
|
+
}
|
|
129
|
+
}
|