@xagents_in/unmask 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +123 -2
- package/dist/cli/hosted.mjs +251 -0
- package/dist/cli/main.mjs +210 -0
- package/dist/core/acquire.mjs +283 -0
- package/dist/core/archive.mjs +485 -0
- package/dist/core/compare.mjs +230 -0
- package/dist/core/manifests.mjs +548 -0
- package/dist/core/online.mjs +310 -0
- package/dist/core/pipeline.mjs +345 -0
- package/dist/core/policy.mjs +92 -0
- package/dist/core/report.mjs +506 -0
- package/dist/core/resolve.mjs +186 -0
- package/dist/core/source.mjs +106 -0
- package/dist/core/syntax.mjs +340 -0
- package/dist/model/client.mjs +1257 -0
- package/dist/model/config.mjs +90 -0
- package/dist/server/app.mjs +299 -0
- package/dist/server/main.mjs +146 -0
- package/dist/server/queue.mjs +177 -0
- package/dist/server/runner.mjs +280 -0
- package/dist/server/store.mjs +105 -0
- package/dist/server/worker.mjs +57 -0
- package/dist/version.json +1 -0
- package/dist/viewer/render.mjs +255 -0
- package/dist/viewer/shell.mjs +73 -0
- package/package.json +39 -4
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const POLICY_VERSION = "default-1";
|
|
2
|
+
|
|
3
|
+
const nonempty = (value) => typeof value === "string" && value.length > 0;
|
|
4
|
+
const usable = (manifest) =>
|
|
5
|
+
manifest?.status === "usable" &&
|
|
6
|
+
manifest.value &&
|
|
7
|
+
typeof manifest.value === "object" &&
|
|
8
|
+
!Array.isArray(manifest.value);
|
|
9
|
+
|
|
10
|
+
export function policyResult(report) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
const artifact = report.inputs.artifact;
|
|
13
|
+
const manifests = report.manifests;
|
|
14
|
+
function finding(ruleId, evidenceIds = [], details = {}) {
|
|
15
|
+
findings.push({
|
|
16
|
+
ruleId,
|
|
17
|
+
evidenceIds: [...new Set(evidenceIds)].sort(),
|
|
18
|
+
...details,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const expected = artifact.expectedIntegrity;
|
|
22
|
+
if (
|
|
23
|
+
expected &&
|
|
24
|
+
["sha256", "sha512"].includes(expected.algorithm) &&
|
|
25
|
+
typeof expected.digest === "string" &&
|
|
26
|
+
typeof artifact[expected.algorithm] === "string" &&
|
|
27
|
+
expected.digest !== artifact[expected.algorithm]
|
|
28
|
+
) {
|
|
29
|
+
finding("integrity-mismatch", [], {
|
|
30
|
+
algorithm: expected.algorithm,
|
|
31
|
+
expected: expected.digest,
|
|
32
|
+
actual: artifact[expected.algorithm],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (
|
|
36
|
+
artifact.provenance?.verificationImplemented === true &&
|
|
37
|
+
artifact.provenance.status === "failed"
|
|
38
|
+
)
|
|
39
|
+
finding("provenance-verification-failed");
|
|
40
|
+
if (report.inputs.claim && usable(manifests.artifact)) {
|
|
41
|
+
const conflicts = ["name", "version"].filter(
|
|
42
|
+
(field) =>
|
|
43
|
+
typeof report.inputs.claim[field] === "string" &&
|
|
44
|
+
report.inputs.claim[field] !== manifests.artifact.value[field],
|
|
45
|
+
);
|
|
46
|
+
if (conflicts.length)
|
|
47
|
+
finding("identity-claim-conflict", manifests.artifact.evidenceIds, {
|
|
48
|
+
fields: conflicts,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const sourceUsable =
|
|
52
|
+
report.source.complete === true &&
|
|
53
|
+
["selected", "resolved"].includes(report.source.status) &&
|
|
54
|
+
!["unresolved", "conflicting"].includes(report.source.method) &&
|
|
55
|
+
report.coverage.sourceComplete === true &&
|
|
56
|
+
usable(manifests.artifact) &&
|
|
57
|
+
usable(manifests.source);
|
|
58
|
+
if (sourceUsable) {
|
|
59
|
+
const a = manifests.artifact.value;
|
|
60
|
+
const s = manifests.source.value;
|
|
61
|
+
const manifestEvidence = [
|
|
62
|
+
...(manifests.artifact.evidenceIds || []),
|
|
63
|
+
...(manifests.source.evidenceIds || []),
|
|
64
|
+
];
|
|
65
|
+
for (const name of ["preinstall", "install", "postinstall"]) {
|
|
66
|
+
if (nonempty(a.scripts?.[name]) && a.scripts[name] !== s.scripts?.[name])
|
|
67
|
+
finding("lifecycle-source-absent", manifestEvidence, { script: name });
|
|
68
|
+
}
|
|
69
|
+
const gyp = report.files.find(
|
|
70
|
+
(file) => file.path === "binding.gyp" && file.state === "artifact-only",
|
|
71
|
+
);
|
|
72
|
+
if (
|
|
73
|
+
gyp &&
|
|
74
|
+
!nonempty(a.scripts?.install) &&
|
|
75
|
+
!nonempty(a.scripts?.preinstall) &&
|
|
76
|
+
a.gypfile !== false
|
|
77
|
+
)
|
|
78
|
+
finding("lifecycle-implicit-gyp", [
|
|
79
|
+
...manifestEvidence,
|
|
80
|
+
...(gyp.evidenceIds || []),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
exitCode:
|
|
85
|
+
report.coverage.complete !== true || report.failures.length > 0
|
|
86
|
+
? 2
|
|
87
|
+
: findings.length > 0
|
|
88
|
+
? 3
|
|
89
|
+
: 0,
|
|
90
|
+
findings,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { POLICY_VERSION, policyResult } from "./policy.mjs";
|
|
3
|
+
|
|
4
|
+
const fields = [
|
|
5
|
+
"schemaVersion",
|
|
6
|
+
"analyzerVersion",
|
|
7
|
+
"policyVersion",
|
|
8
|
+
"limitsVersion",
|
|
9
|
+
"inputs",
|
|
10
|
+
"source",
|
|
11
|
+
"files",
|
|
12
|
+
"coverage",
|
|
13
|
+
"evidence",
|
|
14
|
+
"manifests",
|
|
15
|
+
"cues",
|
|
16
|
+
"annotations",
|
|
17
|
+
"failures",
|
|
18
|
+
"model",
|
|
19
|
+
"run",
|
|
20
|
+
"policy",
|
|
21
|
+
];
|
|
22
|
+
const states = ["byte-identical", "changed", "artifact-only", "not-compared"];
|
|
23
|
+
const methods = [
|
|
24
|
+
"provenance-verified",
|
|
25
|
+
"provenance-claimed",
|
|
26
|
+
"publisher-gitHead",
|
|
27
|
+
"user-full-SHA",
|
|
28
|
+
"unresolved",
|
|
29
|
+
"conflicting",
|
|
30
|
+
];
|
|
31
|
+
const digest = (value, size = 64) =>
|
|
32
|
+
typeof value === "string" && new RegExp(`^[a-f0-9]{${size}}$`).test(value);
|
|
33
|
+
const integer = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
34
|
+
function requireValue(condition, message) {
|
|
35
|
+
if (!condition) throw new TypeError(message);
|
|
36
|
+
}
|
|
37
|
+
function jsonData(value, ancestors = new Set(), depth = 0) {
|
|
38
|
+
requireValue(depth <= 64, "Report nesting limit exceeded");
|
|
39
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
40
|
+
return;
|
|
41
|
+
if (typeof value === "number") {
|
|
42
|
+
requireValue(Number.isFinite(value), "Nonfinite JSON number");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
requireValue(
|
|
46
|
+
typeof value === "object" &&
|
|
47
|
+
(Array.isArray(value) ||
|
|
48
|
+
Object.getPrototypeOf(value) === Object.prototype ||
|
|
49
|
+
Object.getPrototypeOf(value) === null),
|
|
50
|
+
"Report requires plain JSON data",
|
|
51
|
+
);
|
|
52
|
+
requireValue(!ancestors.has(value), "Cyclic report");
|
|
53
|
+
ancestors.add(value);
|
|
54
|
+
const keys = Reflect.ownKeys(value).filter(
|
|
55
|
+
(key) => !(Array.isArray(value) && key === "length"),
|
|
56
|
+
);
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
requireValue(
|
|
59
|
+
keys.length === value.length &&
|
|
60
|
+
keys.every((key, index) => key === String(index)),
|
|
61
|
+
"Sparse or extended JSON array",
|
|
62
|
+
);
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
requireValue(typeof key === "string", "Symbol report key");
|
|
65
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
66
|
+
requireValue(
|
|
67
|
+
descriptor.enumerable && Object.hasOwn(descriptor, "value"),
|
|
68
|
+
"Report accessor or hidden field",
|
|
69
|
+
);
|
|
70
|
+
jsonData(descriptor.value, ancestors, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
ancestors.delete(value);
|
|
73
|
+
}
|
|
74
|
+
function canonical(value) {
|
|
75
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
76
|
+
if (value !== null && typeof value === "object")
|
|
77
|
+
return `{${Object.keys(value)
|
|
78
|
+
.sort()
|
|
79
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
|
|
80
|
+
.join(",")}}`;
|
|
81
|
+
return JSON.stringify(value);
|
|
82
|
+
}
|
|
83
|
+
function safePath(value) {
|
|
84
|
+
return (
|
|
85
|
+
typeof value === "string" &&
|
|
86
|
+
value.length > 0 &&
|
|
87
|
+
!value.startsWith("/") &&
|
|
88
|
+
!value.includes("\\") &&
|
|
89
|
+
!value.includes("\0") &&
|
|
90
|
+
value.split("/").every((part) => part && part !== "." && part !== "..")
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
function counter(value) {
|
|
94
|
+
return value && integer(value.files) && integer(value.bytes);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createReport({
|
|
98
|
+
inputs,
|
|
99
|
+
source,
|
|
100
|
+
comparison,
|
|
101
|
+
manifests,
|
|
102
|
+
failures = [],
|
|
103
|
+
run = {},
|
|
104
|
+
analyzerVersion = "0.0.0",
|
|
105
|
+
limitsVersion = "1",
|
|
106
|
+
}) {
|
|
107
|
+
const report = {
|
|
108
|
+
schemaVersion: "1",
|
|
109
|
+
analyzerVersion,
|
|
110
|
+
policyVersion: POLICY_VERSION,
|
|
111
|
+
limitsVersion,
|
|
112
|
+
inputs,
|
|
113
|
+
source: {
|
|
114
|
+
...source,
|
|
115
|
+
complete: source.complete ?? comparison.coverage.sourceComplete,
|
|
116
|
+
},
|
|
117
|
+
files: comparison.files,
|
|
118
|
+
coverage: comparison.coverage,
|
|
119
|
+
evidence: comparison.evidence,
|
|
120
|
+
manifests,
|
|
121
|
+
cues: [],
|
|
122
|
+
annotations: [],
|
|
123
|
+
failures,
|
|
124
|
+
model: { status: "disabled" },
|
|
125
|
+
run: {
|
|
126
|
+
timestamps: {},
|
|
127
|
+
timings: {},
|
|
128
|
+
surface: "unknown",
|
|
129
|
+
observations: [],
|
|
130
|
+
...run,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
jsonData(report);
|
|
134
|
+
const snapshot = structuredClone(report);
|
|
135
|
+
snapshot.policy = policyResult(snapshot);
|
|
136
|
+
return validateReport(snapshot);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function validateReport(report) {
|
|
140
|
+
jsonData(report);
|
|
141
|
+
requireValue(
|
|
142
|
+
report &&
|
|
143
|
+
!Array.isArray(report) &&
|
|
144
|
+
fields.every((key) => Object.hasOwn(report, key)) &&
|
|
145
|
+
Object.keys(report).every((key) => fields.includes(key)),
|
|
146
|
+
"Invalid report fields",
|
|
147
|
+
);
|
|
148
|
+
requireValue(
|
|
149
|
+
report.schemaVersion === "1" &&
|
|
150
|
+
typeof report.analyzerVersion === "string" &&
|
|
151
|
+
report.analyzerVersion.length > 0 &&
|
|
152
|
+
report.policyVersion === POLICY_VERSION &&
|
|
153
|
+
typeof report.limitsVersion === "string" &&
|
|
154
|
+
report.limitsVersion.length > 0,
|
|
155
|
+
"Unsupported report version",
|
|
156
|
+
);
|
|
157
|
+
requireValue(
|
|
158
|
+
report.inputs &&
|
|
159
|
+
digest(report.inputs.artifact?.sha256) &&
|
|
160
|
+
digest(report.inputs.artifact?.sha512, 128),
|
|
161
|
+
"Original artifact digests are required",
|
|
162
|
+
);
|
|
163
|
+
requireValue(
|
|
164
|
+
(report.inputs.limits &&
|
|
165
|
+
typeof report.inputs.limits === "object" &&
|
|
166
|
+
!Array.isArray(report.inputs.limits) &&
|
|
167
|
+
Object.keys(report.inputs.limits).length > 0) ||
|
|
168
|
+
digest(report.inputs.limitsDigest),
|
|
169
|
+
"Effective limits object or digest is required",
|
|
170
|
+
);
|
|
171
|
+
for (const key of ["sourceArchive", "registryDocument"]) {
|
|
172
|
+
if (report.inputs[key] !== undefined)
|
|
173
|
+
requireValue(digest(report.inputs[key]?.sha256), `Invalid ${key} digest`);
|
|
174
|
+
}
|
|
175
|
+
const expected = report.inputs.artifact.expectedIntegrity;
|
|
176
|
+
if (expected !== undefined)
|
|
177
|
+
requireValue(
|
|
178
|
+
expected &&
|
|
179
|
+
((expected.status === "unavailable" && expected.digest === undefined) ||
|
|
180
|
+
(["sha256", "sha512"].includes(expected.algorithm) &&
|
|
181
|
+
digest(
|
|
182
|
+
expected.digest,
|
|
183
|
+
expected.algorithm === "sha512" ? 128 : 64,
|
|
184
|
+
))),
|
|
185
|
+
"Invalid expected integrity",
|
|
186
|
+
);
|
|
187
|
+
if (report.inputs.claim !== undefined)
|
|
188
|
+
requireValue(
|
|
189
|
+
report.inputs.claim &&
|
|
190
|
+
typeof report.inputs.claim.name === "string" &&
|
|
191
|
+
typeof report.inputs.claim.version === "string",
|
|
192
|
+
"Invalid identity claim",
|
|
193
|
+
);
|
|
194
|
+
requireValue(
|
|
195
|
+
report.source &&
|
|
196
|
+
typeof report.source.complete === "boolean" &&
|
|
197
|
+
[
|
|
198
|
+
"selected",
|
|
199
|
+
"resolved",
|
|
200
|
+
"unresolved",
|
|
201
|
+
"conflicting",
|
|
202
|
+
"unavailable",
|
|
203
|
+
].includes(report.source.status) &&
|
|
204
|
+
methods.includes(report.source.method) &&
|
|
205
|
+
Array.isArray(report.source.alternatives),
|
|
206
|
+
"Invalid source selection",
|
|
207
|
+
);
|
|
208
|
+
if (report.source.commit !== undefined)
|
|
209
|
+
requireValue(
|
|
210
|
+
/^[a-f0-9]{40}$/.test(report.source.commit),
|
|
211
|
+
"Source commit must be a full SHA",
|
|
212
|
+
);
|
|
213
|
+
requireValue(
|
|
214
|
+
Array.isArray(report.files) &&
|
|
215
|
+
Array.isArray(report.evidence) &&
|
|
216
|
+
Array.isArray(report.failures) &&
|
|
217
|
+
Array.isArray(report.cues) &&
|
|
218
|
+
Array.isArray(report.annotations),
|
|
219
|
+
"Invalid report collections",
|
|
220
|
+
);
|
|
221
|
+
requireValue(
|
|
222
|
+
report.model &&
|
|
223
|
+
typeof report.model.status === "string" &&
|
|
224
|
+
report.run &&
|
|
225
|
+
typeof report.run === "object",
|
|
226
|
+
"Invalid model or run section",
|
|
227
|
+
);
|
|
228
|
+
const coverage = report.coverage;
|
|
229
|
+
requireValue(
|
|
230
|
+
coverage &&
|
|
231
|
+
counter(coverage.inventory) &&
|
|
232
|
+
typeof coverage.inventory.complete === "boolean" &&
|
|
233
|
+
coverage.inventory.lowerBound === !coverage.inventory.complete &&
|
|
234
|
+
typeof coverage.complete === "boolean" &&
|
|
235
|
+
typeof coverage.sourceAvailable === "boolean" &&
|
|
236
|
+
typeof coverage.sourceComplete === "boolean",
|
|
237
|
+
"Invalid coverage inventory",
|
|
238
|
+
);
|
|
239
|
+
requireValue(
|
|
240
|
+
coverage.complete ===
|
|
241
|
+
(coverage.inventory.complete &&
|
|
242
|
+
(!coverage.sourceAvailable || coverage.sourceComplete)),
|
|
243
|
+
"Invalid completeness claim",
|
|
244
|
+
);
|
|
245
|
+
requireValue(
|
|
246
|
+
report.source.complete === coverage.sourceComplete,
|
|
247
|
+
"Source completeness differs from comparison",
|
|
248
|
+
);
|
|
249
|
+
for (const name of [
|
|
250
|
+
"compared",
|
|
251
|
+
"unresolved",
|
|
252
|
+
"syntaxInspected",
|
|
253
|
+
"modelReviewed",
|
|
254
|
+
])
|
|
255
|
+
requireValue(
|
|
256
|
+
counter(coverage[name]) &&
|
|
257
|
+
coverage[name].files <= coverage.inventory.files &&
|
|
258
|
+
coverage[name].bytes <= coverage.inventory.bytes,
|
|
259
|
+
"Invalid coverage counter",
|
|
260
|
+
);
|
|
261
|
+
requireValue(
|
|
262
|
+
coverage.states && states.every((state) => counter(coverage.states[state])),
|
|
263
|
+
"Invalid correspondence coverage",
|
|
264
|
+
);
|
|
265
|
+
for (const name of ["links", "nonregular"])
|
|
266
|
+
requireValue(
|
|
267
|
+
coverage[name] &&
|
|
268
|
+
integer(coverage[name].artifact) &&
|
|
269
|
+
integer(coverage[name].source),
|
|
270
|
+
"Invalid nonregular coverage",
|
|
271
|
+
);
|
|
272
|
+
requireValue(
|
|
273
|
+
Array.isArray(coverage.linkEntries) &&
|
|
274
|
+
coverage.linkEntries.every(
|
|
275
|
+
(link) =>
|
|
276
|
+
link &&
|
|
277
|
+
["artifact", "source"].includes(link.side) &&
|
|
278
|
+
["symlink", "hardlink"].includes(link.type) &&
|
|
279
|
+
(link.path === null || safePath(link.path)) &&
|
|
280
|
+
typeof link.originalPath === "string" &&
|
|
281
|
+
typeof link.linkTarget === "string",
|
|
282
|
+
) &&
|
|
283
|
+
["artifact", "source"].every(
|
|
284
|
+
(side) =>
|
|
285
|
+
coverage.linkEntries.filter((link) => link.side === side).length ===
|
|
286
|
+
coverage.links[side],
|
|
287
|
+
),
|
|
288
|
+
"Invalid inert link inventory",
|
|
289
|
+
);
|
|
290
|
+
requireValue(
|
|
291
|
+
Array.isArray(coverage.sourceOnly) && coverage.sourceOnly.every(safePath),
|
|
292
|
+
"Invalid source-only paths",
|
|
293
|
+
);
|
|
294
|
+
const evidence = new Map();
|
|
295
|
+
for (const item of report.evidence) {
|
|
296
|
+
requireValue(
|
|
297
|
+
item &&
|
|
298
|
+
["artifact", "source"].includes(item.side) &&
|
|
299
|
+
(item.path === null || safePath(item.path)) &&
|
|
300
|
+
typeof item.originalPath === "string" &&
|
|
301
|
+
digest(item.blobDigest) &&
|
|
302
|
+
digest(item.fileSha256) &&
|
|
303
|
+
integer(item.size) &&
|
|
304
|
+
Array.isArray(item.range) &&
|
|
305
|
+
item.range.length === 2 &&
|
|
306
|
+
item.range.every(integer) &&
|
|
307
|
+
item.range[0] <= item.range[1] &&
|
|
308
|
+
item.range[1] <= item.size,
|
|
309
|
+
"Invalid evidence provenance/range",
|
|
310
|
+
);
|
|
311
|
+
const blob =
|
|
312
|
+
item.side === "artifact"
|
|
313
|
+
? report.inputs.artifact
|
|
314
|
+
: report.inputs.sourceArchive;
|
|
315
|
+
requireValue(
|
|
316
|
+
item.blobDigest === blob?.sha256,
|
|
317
|
+
"Evidence is not bound to an input",
|
|
318
|
+
);
|
|
319
|
+
const identity = [
|
|
320
|
+
item.side,
|
|
321
|
+
item.blobDigest,
|
|
322
|
+
item.path ?? item.originalPath,
|
|
323
|
+
item.range,
|
|
324
|
+
];
|
|
325
|
+
const expectedId = `ev-${createHash("sha256").update(JSON.stringify(identity)).digest("hex")}`;
|
|
326
|
+
requireValue(
|
|
327
|
+
item.id === expectedId && !evidence.has(item.id),
|
|
328
|
+
"Invalid or duplicate evidence ID",
|
|
329
|
+
);
|
|
330
|
+
if (item.excerpt !== undefined)
|
|
331
|
+
requireValue(
|
|
332
|
+
typeof item.excerpt === "string" &&
|
|
333
|
+
Array.isArray(item.excerptRange) &&
|
|
334
|
+
item.excerptRange.length === 2 &&
|
|
335
|
+
item.excerptRange.every(integer) &&
|
|
336
|
+
item.excerptRange[0] === item.range[0] &&
|
|
337
|
+
item.excerptRange[1] <= item.range[1] &&
|
|
338
|
+
item.excerptRange[1] - item.excerptRange[0] ===
|
|
339
|
+
Buffer.byteLength(item.excerpt),
|
|
340
|
+
"Invalid evidence excerpt range",
|
|
341
|
+
);
|
|
342
|
+
evidence.set(item.id, item);
|
|
343
|
+
}
|
|
344
|
+
function references(ids) {
|
|
345
|
+
requireValue(
|
|
346
|
+
Array.isArray(ids) && ids.every((id) => evidence.has(id)),
|
|
347
|
+
"Unknown evidence reference",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const totals = Object.fromEntries(
|
|
351
|
+
states.map((state) => [state, { files: 0, bytes: 0 }]),
|
|
352
|
+
);
|
|
353
|
+
const paths = new Set();
|
|
354
|
+
for (const file of report.files) {
|
|
355
|
+
requireValue(
|
|
356
|
+
file &&
|
|
357
|
+
(file.path === null || safePath(file.path)) &&
|
|
358
|
+
typeof file.originalPath === "string" &&
|
|
359
|
+
integer(file.size) &&
|
|
360
|
+
digest(file.artifactSha256) &&
|
|
361
|
+
states.includes(file.state) &&
|
|
362
|
+
["whole-file", "byte-spans"].includes(file.localization),
|
|
363
|
+
"Invalid file correspondence",
|
|
364
|
+
);
|
|
365
|
+
const key = file.path ?? `\0${file.originalPath}`;
|
|
366
|
+
requireValue(!paths.has(key), "Duplicate artifact file");
|
|
367
|
+
paths.add(key);
|
|
368
|
+
references(file.evidenceIds);
|
|
369
|
+
requireValue(
|
|
370
|
+
file.evidenceIds.some((id) => {
|
|
371
|
+
const e = evidence.get(id);
|
|
372
|
+
return (
|
|
373
|
+
e.side === "artifact" &&
|
|
374
|
+
e.path === file.path &&
|
|
375
|
+
e.originalPath === file.originalPath &&
|
|
376
|
+
e.size === file.size &&
|
|
377
|
+
e.fileSha256 === file.artifactSha256
|
|
378
|
+
);
|
|
379
|
+
}),
|
|
380
|
+
"Missing artifact file evidence",
|
|
381
|
+
);
|
|
382
|
+
if (["byte-identical", "changed"].includes(file.state))
|
|
383
|
+
requireValue(
|
|
384
|
+
digest(file.sourceSha256) &&
|
|
385
|
+
file.evidenceIds.some((id) => {
|
|
386
|
+
const e = evidence.get(id);
|
|
387
|
+
return (
|
|
388
|
+
e.side === "source" &&
|
|
389
|
+
e.path === file.path &&
|
|
390
|
+
e.fileSha256 === file.sourceSha256 &&
|
|
391
|
+
(file.state !== "byte-identical" ||
|
|
392
|
+
(e.size === file.size &&
|
|
393
|
+
file.sourceSha256 === file.artifactSha256))
|
|
394
|
+
);
|
|
395
|
+
}),
|
|
396
|
+
"Invalid source correspondence evidence",
|
|
397
|
+
);
|
|
398
|
+
if (file.state === "artifact-only")
|
|
399
|
+
requireValue(
|
|
400
|
+
coverage.sourceAvailable &&
|
|
401
|
+
coverage.sourceComplete &&
|
|
402
|
+
["selected", "resolved"].includes(report.source.status),
|
|
403
|
+
"Source absence is not established",
|
|
404
|
+
);
|
|
405
|
+
if (file.localization === "byte-spans") {
|
|
406
|
+
requireValue(
|
|
407
|
+
file.state === "changed" &&
|
|
408
|
+
file.spans &&
|
|
409
|
+
["artifact", "source"].every((side) => {
|
|
410
|
+
const range = file.spans[side];
|
|
411
|
+
return (
|
|
412
|
+
Array.isArray(range) &&
|
|
413
|
+
range.length === 2 &&
|
|
414
|
+
range.every(integer) &&
|
|
415
|
+
range[0] <= range[1] &&
|
|
416
|
+
file.evidenceIds.some((id) => {
|
|
417
|
+
const e = evidence.get(id);
|
|
418
|
+
return (
|
|
419
|
+
e.side === side && canonical(e.range) === canonical(range)
|
|
420
|
+
);
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
}),
|
|
424
|
+
"Invalid byte-span localization",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (file.state === "not-compared")
|
|
428
|
+
requireValue(
|
|
429
|
+
typeof file.reason === "string" && file.reason.length > 0,
|
|
430
|
+
"Unknown comparison reason",
|
|
431
|
+
);
|
|
432
|
+
totals[file.state].files++;
|
|
433
|
+
totals[file.state].bytes += file.size;
|
|
434
|
+
}
|
|
435
|
+
requireValue(
|
|
436
|
+
states.every(
|
|
437
|
+
(state) => canonical(totals[state]) === canonical(coverage.states[state]),
|
|
438
|
+
),
|
|
439
|
+
"Coverage state totals differ",
|
|
440
|
+
);
|
|
441
|
+
const totalBytes = Object.values(totals).reduce(
|
|
442
|
+
(sum, value) => sum + value.bytes,
|
|
443
|
+
0,
|
|
444
|
+
);
|
|
445
|
+
requireValue(
|
|
446
|
+
Number.isSafeInteger(totalBytes) &&
|
|
447
|
+
report.files.length === coverage.inventory.files &&
|
|
448
|
+
totalBytes === coverage.inventory.bytes,
|
|
449
|
+
"Coverage denominator differs",
|
|
450
|
+
);
|
|
451
|
+
requireValue(
|
|
452
|
+
coverage.compared.files ===
|
|
453
|
+
totals["byte-identical"].files + totals.changed.files &&
|
|
454
|
+
coverage.compared.bytes ===
|
|
455
|
+
totals["byte-identical"].bytes + totals.changed.bytes &&
|
|
456
|
+
coverage.unresolved.files ===
|
|
457
|
+
report.files.length - totals["byte-identical"].files &&
|
|
458
|
+
coverage.unresolved.bytes === totalBytes - totals["byte-identical"].bytes,
|
|
459
|
+
"Coverage correspondence totals differ",
|
|
460
|
+
);
|
|
461
|
+
for (const side of ["artifact", "source", "registry"]) {
|
|
462
|
+
const manifest = report.manifests?.[side];
|
|
463
|
+
requireValue(
|
|
464
|
+
manifest &&
|
|
465
|
+
["usable", "unavailable", "invalid"].includes(manifest.status),
|
|
466
|
+
"Invalid manifest status",
|
|
467
|
+
);
|
|
468
|
+
if (manifest.status === "usable") {
|
|
469
|
+
requireValue(
|
|
470
|
+
manifest.value &&
|
|
471
|
+
typeof manifest.value === "object" &&
|
|
472
|
+
!Array.isArray(manifest.value),
|
|
473
|
+
"Invalid manifest value",
|
|
474
|
+
);
|
|
475
|
+
references(manifest.evidenceIds);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const failure of report.failures)
|
|
479
|
+
requireValue(
|
|
480
|
+
failure &&
|
|
481
|
+
typeof failure.code === "string" &&
|
|
482
|
+
typeof failure.message === "string",
|
|
483
|
+
"Invalid deterministic failure",
|
|
484
|
+
);
|
|
485
|
+
for (const item of [...report.cues, ...report.annotations])
|
|
486
|
+
if (item.evidenceIds !== undefined) references(item.evidenceIds);
|
|
487
|
+
requireValue(
|
|
488
|
+
canonical(policyResult(report)) === canonical(report.policy),
|
|
489
|
+
"Policy result differs from deterministic evidence",
|
|
490
|
+
);
|
|
491
|
+
for (const finding of report.policy.findings) references(finding.evidenceIds);
|
|
492
|
+
return report;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function canonicalReport(report) {
|
|
496
|
+
validateReport(report);
|
|
497
|
+
return (
|
|
498
|
+
canonical(
|
|
499
|
+
Object.fromEntries(
|
|
500
|
+
Object.entries(report).filter(
|
|
501
|
+
([key]) => key !== "run" && key !== "model",
|
|
502
|
+
),
|
|
503
|
+
),
|
|
504
|
+
) + "\n"
|
|
505
|
+
);
|
|
506
|
+
}
|