@xagents_in/unmask 0.1.2 → 0.1.4
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 +301 -85
- package/dist/cli/annotations.mjs +145 -0
- package/dist/cli/audit.mjs +223 -0
- package/dist/cli/batch.mjs +218 -0
- package/dist/cli/cache.mjs +488 -0
- package/dist/cli/compare.mjs +261 -0
- package/dist/cli/config.mjs +141 -0
- package/dist/cli/discovery.mjs +372 -0
- package/dist/cli/doctor.mjs +354 -0
- package/dist/cli/exceptions.mjs +170 -0
- package/dist/cli/file.mjs +271 -0
- package/dist/cli/files.mjs +141 -0
- package/dist/cli/history.mjs +232 -0
- package/dist/cli/hosted.mjs +251 -43
- package/dist/cli/info.mjs +125 -0
- package/dist/cli/init.mjs +57 -0
- package/dist/cli/install-guard.mjs +896 -0
- package/dist/cli/lockfile.mjs +330 -0
- package/dist/cli/main.mjs +101 -14
- package/dist/cli/preflight.mjs +388 -0
- package/dist/cli/sarif.mjs +100 -0
- package/dist/cli/scan.mjs +128 -0
- package/dist/cli/shell-integration.mjs +122 -0
- package/dist/cli/terminal.mjs +396 -0
- package/dist/cli/watch.mjs +233 -0
- package/dist/cli/why.mjs +256 -0
- package/dist/core/advisories.mjs +101 -24
- package/dist/core/advisory-cache.mjs +295 -0
- package/dist/core/behavior-signals.mjs +1088 -0
- package/dist/core/dependency-graph.mjs +332 -0
- package/dist/core/evidence-exports.mjs +469 -0
- package/dist/core/guard-config.mjs +463 -0
- package/dist/core/guard-policy.mjs +308 -0
- package/dist/core/package-details.mjs +42 -6
- package/dist/core/registry-facts.mjs +342 -0
- package/dist/core/report-exploration.mjs +328 -0
- package/dist/core/report.mjs +7 -0
- package/dist/core/sarif.mjs +556 -0
- package/dist/core/syntax.mjs +296 -15
- package/dist/model/client.mjs +204 -56
- package/dist/model/diff-evidence.mjs +177 -0
- package/dist/model/summary.mjs +183 -0
- package/dist/server/advisory-result.mjs +137 -0
- package/dist/server/app.mjs +66 -2
- package/dist/server/dependency-screening.mjs +386 -0
- package/dist/server/exports.mjs +63 -0
- package/dist/server/guard-decision.mjs +37 -0
- package/dist/server/main.mjs +25 -7
- package/dist/server/queue.mjs +95 -53
- package/dist/server/registry-facts.mjs +118 -0
- package/dist/server/report-exploration.mjs +87 -0
- package/dist/server/store.mjs +21 -1
- package/dist/version.json +1 -1
- package/dist/viewer/render.mjs +10 -2
- package/dist/viewer/shell.mjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalReport, validateReport } from "./report.mjs";
|
|
3
|
+
|
|
4
|
+
export const SBOM_LIMITS = Object.freeze({
|
|
5
|
+
maxComponents: 2000,
|
|
6
|
+
maxNameLength: 214,
|
|
7
|
+
maxRangeLength: 1024,
|
|
8
|
+
});
|
|
9
|
+
export const CYCLONEDX_MEDIA_TYPE = "application/vnd.cyclonedx+json";
|
|
10
|
+
|
|
11
|
+
const ROOT_REF = "unmask:artifact";
|
|
12
|
+
const DECLARED_FIELDS = [
|
|
13
|
+
["dependencies", "required"],
|
|
14
|
+
["optionalDependencies", "optional"],
|
|
15
|
+
["peerDependencies", "optional"],
|
|
16
|
+
["devDependencies", "excluded"],
|
|
17
|
+
];
|
|
18
|
+
const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
19
|
+
const object = (value) =>
|
|
20
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
const npmName = (value) =>
|
|
22
|
+
typeof value === "string" &&
|
|
23
|
+
value.length <= SBOM_LIMITS.maxNameLength &&
|
|
24
|
+
/^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/i.test(value);
|
|
25
|
+
const property = (name, value) => ({
|
|
26
|
+
name: `unmask:${name}`,
|
|
27
|
+
value: String(value),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function splitName(name) {
|
|
31
|
+
const slash = name.startsWith("@") ? name.indexOf("/") : -1;
|
|
32
|
+
return slash > 0
|
|
33
|
+
? { group: name.slice(0, slash), name: name.slice(slash + 1) }
|
|
34
|
+
: { name };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function npmPurl(name, version) {
|
|
38
|
+
if (!npmName(name) || typeof version !== "string" || !version) return null;
|
|
39
|
+
const parts = splitName(name);
|
|
40
|
+
const namespace = parts.group ? `${encodeURIComponent(parts.group)}/` : "";
|
|
41
|
+
return `pkg:npm/${namespace}${encodeURIComponent(parts.name)}@${encodeURIComponent(version)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function reportIdentity(report) {
|
|
45
|
+
const manifest = report.manifests.artifact;
|
|
46
|
+
const declared =
|
|
47
|
+
manifest.status === "usable" && object(manifest.value)
|
|
48
|
+
? manifest.value
|
|
49
|
+
: null;
|
|
50
|
+
const claim = report.inputs.claim ?? null;
|
|
51
|
+
const declaredName =
|
|
52
|
+
typeof declared?.name === "string" ? declared.name : null;
|
|
53
|
+
const declaredVersion =
|
|
54
|
+
typeof declared?.version === "string" ? declared.version : null;
|
|
55
|
+
const status =
|
|
56
|
+
claim && declared
|
|
57
|
+
? claim.name === declaredName && claim.version === declaredVersion
|
|
58
|
+
? "claim-matches-manifest"
|
|
59
|
+
: "claim-conflicts-with-manifest"
|
|
60
|
+
: claim
|
|
61
|
+
? "claimed-manifest-unavailable"
|
|
62
|
+
: declared
|
|
63
|
+
? "manifest-declared-unclaimed"
|
|
64
|
+
: "unknown";
|
|
65
|
+
// Only bind a purl when the artifact's own manifest agrees with (or stands in for) the claim.
|
|
66
|
+
const bound = [
|
|
67
|
+
"claim-matches-manifest",
|
|
68
|
+
"manifest-declared-unclaimed",
|
|
69
|
+
].includes(status);
|
|
70
|
+
const name =
|
|
71
|
+
bound || status === "claim-conflicts-with-manifest" ? declaredName : null;
|
|
72
|
+
const version =
|
|
73
|
+
bound || status === "claim-conflicts-with-manifest"
|
|
74
|
+
? declaredVersion
|
|
75
|
+
: null;
|
|
76
|
+
return {
|
|
77
|
+
status,
|
|
78
|
+
name,
|
|
79
|
+
version,
|
|
80
|
+
purl: bound ? npmPurl(name, version) : null,
|
|
81
|
+
requested: claim ? { name: claim.name, version: claim.version } : null,
|
|
82
|
+
declared: declared
|
|
83
|
+
? { name: declaredName, version: declaredVersion }
|
|
84
|
+
: null,
|
|
85
|
+
manifestStatus: manifest.status,
|
|
86
|
+
declaredManifest: declared,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function integrityStatus(artifact) {
|
|
91
|
+
const expected = artifact.expectedIntegrity;
|
|
92
|
+
if (!expected) return "not-recorded";
|
|
93
|
+
if (expected.status === "unavailable") return "unavailable";
|
|
94
|
+
return expected.digest === artifact[expected.algorithm]
|
|
95
|
+
? "matches"
|
|
96
|
+
: "mismatch";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function bundledObservations(report) {
|
|
100
|
+
const found = new Map();
|
|
101
|
+
for (const file of report.files) {
|
|
102
|
+
if (typeof file.path !== "string") continue;
|
|
103
|
+
const segments = file.path.split("/");
|
|
104
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
105
|
+
if (segments[i] !== "node_modules") continue;
|
|
106
|
+
const scoped = segments[i + 1].startsWith("@");
|
|
107
|
+
const end = i + (scoped ? 3 : 2);
|
|
108
|
+
if (end >= segments.length) continue;
|
|
109
|
+
const name = segments.slice(i + 1, end).join("/");
|
|
110
|
+
const prefix = segments.slice(0, end).join("/");
|
|
111
|
+
const item = found.get(prefix) ?? {
|
|
112
|
+
prefix,
|
|
113
|
+
name,
|
|
114
|
+
within: i === 0 ? null : segments.slice(0, i).join("/"),
|
|
115
|
+
files: 0,
|
|
116
|
+
bytes: 0,
|
|
117
|
+
};
|
|
118
|
+
item.files++;
|
|
119
|
+
item.bytes += file.size;
|
|
120
|
+
found.set(prefix, item);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...found.values()].sort((a, b) => order(a.prefix, b.prefix));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildCycloneDxSbom(report, { toolVersion = null } = {}) {
|
|
127
|
+
validateReport(report);
|
|
128
|
+
const identity = reportIdentity(report);
|
|
129
|
+
const declared = identity.declaredManifest;
|
|
130
|
+
const artifact = report.inputs.artifact;
|
|
131
|
+
const components = [];
|
|
132
|
+
const dependsOn = [];
|
|
133
|
+
const fieldStatus = [];
|
|
134
|
+
let invalidEntries = 0;
|
|
135
|
+
let omitted = 0;
|
|
136
|
+
function add(component, runtime) {
|
|
137
|
+
if (components.length >= SBOM_LIMITS.maxComponents) {
|
|
138
|
+
omitted++;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
components.push(component);
|
|
142
|
+
if (runtime) dependsOn.push(component["bom-ref"]);
|
|
143
|
+
}
|
|
144
|
+
for (const [field, scope] of DECLARED_FIELDS) {
|
|
145
|
+
const value = declared?.[field];
|
|
146
|
+
if (declared === null) {
|
|
147
|
+
fieldStatus.push(property(`declared:${field}`, "manifest-unavailable"));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (value === undefined) {
|
|
151
|
+
fieldStatus.push(property(`declared:${field}`, "not-declared"));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!object(value)) {
|
|
155
|
+
fieldStatus.push(property(`declared:${field}`, "invalid-type"));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
fieldStatus.push(property(`declared:${field}`, "declared"));
|
|
159
|
+
for (const name of Object.keys(value).sort(order)) {
|
|
160
|
+
const range = value[name];
|
|
161
|
+
if (
|
|
162
|
+
!npmName(name) ||
|
|
163
|
+
typeof range !== "string" ||
|
|
164
|
+
range.length > SBOM_LIMITS.maxRangeLength
|
|
165
|
+
) {
|
|
166
|
+
invalidEntries++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
add(
|
|
170
|
+
{
|
|
171
|
+
type: "library",
|
|
172
|
+
"bom-ref": `unmask:declared:${field}:${name}`,
|
|
173
|
+
...splitName(name),
|
|
174
|
+
scope,
|
|
175
|
+
properties: [
|
|
176
|
+
property("resolution", "unresolved"),
|
|
177
|
+
property("evidence", "publisher-declared-manifest-entry"),
|
|
178
|
+
property("declared-field", field),
|
|
179
|
+
property("declared-range", range),
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
scope !== "excluded",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const bundledDeclared = new Set(
|
|
187
|
+
[declared?.bundledDependencies, declared?.bundleDependencies]
|
|
188
|
+
.filter(Array.isArray)
|
|
189
|
+
.flat()
|
|
190
|
+
.filter((item) => typeof item === "string"),
|
|
191
|
+
);
|
|
192
|
+
for (const item of bundledObservations(report)) {
|
|
193
|
+
if (!npmName(item.name)) {
|
|
194
|
+
invalidEntries++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
add(
|
|
198
|
+
{
|
|
199
|
+
type: "library",
|
|
200
|
+
"bom-ref": `unmask:bundled:${item.prefix}`,
|
|
201
|
+
...splitName(item.name),
|
|
202
|
+
scope: "required",
|
|
203
|
+
properties: [
|
|
204
|
+
property("resolution", "unresolved"),
|
|
205
|
+
property("evidence", "observed-bundled-path-in-artifact"),
|
|
206
|
+
property("bundled-path", item.prefix),
|
|
207
|
+
property(
|
|
208
|
+
"bundled-location",
|
|
209
|
+
item.within === null ? "artifact-root-node-modules" : "nested",
|
|
210
|
+
),
|
|
211
|
+
...(item.within === null
|
|
212
|
+
? []
|
|
213
|
+
: [property("bundled-within", item.within)]),
|
|
214
|
+
property("bundled-files", item.files),
|
|
215
|
+
property("bundled-bytes", item.bytes),
|
|
216
|
+
property("bundled-declared", bundledDeclared.has(item.name)),
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
item.within === null,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const root = {
|
|
223
|
+
type: "library",
|
|
224
|
+
"bom-ref": ROOT_REF,
|
|
225
|
+
...(identity.name && npmName(identity.name)
|
|
226
|
+
? splitName(identity.name)
|
|
227
|
+
: { name: "unidentified-npm-artifact" }),
|
|
228
|
+
...(identity.version ? { version: identity.version } : {}),
|
|
229
|
+
...(identity.purl ? { purl: identity.purl } : {}),
|
|
230
|
+
hashes: [
|
|
231
|
+
{ alg: "SHA-256", content: artifact.sha256 },
|
|
232
|
+
{ alg: "SHA-512", content: artifact.sha512 },
|
|
233
|
+
],
|
|
234
|
+
properties: [
|
|
235
|
+
property("identity-status", identity.status),
|
|
236
|
+
property("manifest-status", identity.manifestStatus),
|
|
237
|
+
property("expected-integrity", integrityStatus(artifact)),
|
|
238
|
+
...(identity.requested
|
|
239
|
+
? [
|
|
240
|
+
property(
|
|
241
|
+
"requested-identity",
|
|
242
|
+
`${identity.requested.name}@${identity.requested.version}`,
|
|
243
|
+
),
|
|
244
|
+
]
|
|
245
|
+
: []),
|
|
246
|
+
...(typeof identity.declared?.name === "string"
|
|
247
|
+
? [
|
|
248
|
+
property(
|
|
249
|
+
"declared-identity",
|
|
250
|
+
`${identity.declared.name}@${identity.declared.version}`,
|
|
251
|
+
),
|
|
252
|
+
]
|
|
253
|
+
: []),
|
|
254
|
+
property("source-status", report.source.status),
|
|
255
|
+
property("source-method", report.source.method),
|
|
256
|
+
...(report.source.commit
|
|
257
|
+
? [property("source-commit-recorded", report.source.commit)]
|
|
258
|
+
: []),
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
const unresolved = components.length;
|
|
262
|
+
return {
|
|
263
|
+
$schema: "http://cyclonedx.org/schema/bom-1.6.schema.json",
|
|
264
|
+
bomFormat: "CycloneDX",
|
|
265
|
+
specVersion: "1.6",
|
|
266
|
+
version: 1,
|
|
267
|
+
metadata: {
|
|
268
|
+
tools: {
|
|
269
|
+
components: [
|
|
270
|
+
{
|
|
271
|
+
type: "application",
|
|
272
|
+
name: "unmask",
|
|
273
|
+
...(toolVersion ? { version: toolVersion } : {}),
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
component: root,
|
|
278
|
+
properties: [
|
|
279
|
+
property("sbom-scope", "observed-artifact-and-declared-direct-entries"),
|
|
280
|
+
property("dependency-resolution", "not-performed"),
|
|
281
|
+
property("transitive-dependencies", "not-assessed"),
|
|
282
|
+
property("unresolved-dependencies", unresolved),
|
|
283
|
+
property("invalid-entries-omitted", invalidEntries),
|
|
284
|
+
property("components-omitted", omitted),
|
|
285
|
+
property("vulnerabilities", "not-included"),
|
|
286
|
+
property("signature", "unsigned"),
|
|
287
|
+
property("inventory-complete", report.coverage.inventory.complete),
|
|
288
|
+
property("report-schema", report.schemaVersion),
|
|
289
|
+
property("analyzer-version", report.analyzerVersion),
|
|
290
|
+
...fieldStatus,
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
components,
|
|
294
|
+
dependencies: [{ ref: ROOT_REF, dependsOn }],
|
|
295
|
+
compositions: [
|
|
296
|
+
{
|
|
297
|
+
aggregate: declared === null ? "unknown" : "incomplete",
|
|
298
|
+
dependencies: [ROOT_REF],
|
|
299
|
+
},
|
|
300
|
+
...(components.length
|
|
301
|
+
? [
|
|
302
|
+
{
|
|
303
|
+
aggregate: "unknown",
|
|
304
|
+
dependencies: components.map((item) => item["bom-ref"]),
|
|
305
|
+
},
|
|
306
|
+
]
|
|
307
|
+
: []),
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
313
|
+
|
|
314
|
+
function limitations(report, identity) {
|
|
315
|
+
const coverage = report.coverage;
|
|
316
|
+
return [
|
|
317
|
+
"Unsigned manifest: digests support integrity comparison only and do not prove authorship or origin.",
|
|
318
|
+
"Static bounded analysis; package code, install scripts and builds were not executed.",
|
|
319
|
+
"Dependency versions and transitive dependencies were not resolved.",
|
|
320
|
+
"Vulnerability advisories are not included; consult the report and details exports.",
|
|
321
|
+
"Absence of a current finding does not establish that the package is safe.",
|
|
322
|
+
...(coverage.inventory.complete
|
|
323
|
+
? []
|
|
324
|
+
: ["Artifact inventory is incomplete; counts are lower bounds."]),
|
|
325
|
+
...(coverage.sourceAvailable
|
|
326
|
+
? coverage.sourceComplete
|
|
327
|
+
? []
|
|
328
|
+
: ["Source archive was incomplete; comparison is partial."]
|
|
329
|
+
: ["No source archive was compared with the published artifact."]),
|
|
330
|
+
...(["provenance-verified"].includes(report.source.method)
|
|
331
|
+
? []
|
|
332
|
+
: [
|
|
333
|
+
`Source selection method is ${report.source.method}; the recorded commit is not cryptographically verified by this manifest.`,
|
|
334
|
+
]),
|
|
335
|
+
...(report.failures.length
|
|
336
|
+
? [
|
|
337
|
+
`Analysis recorded ${report.failures.length} deterministic failure(s).`,
|
|
338
|
+
]
|
|
339
|
+
: []),
|
|
340
|
+
...(report.model.status === "complete"
|
|
341
|
+
? []
|
|
342
|
+
: [
|
|
343
|
+
`AI review status is ${report.model.status}; no AI conclusion is included.`,
|
|
344
|
+
]),
|
|
345
|
+
...(identity.status === "claim-conflicts-with-manifest"
|
|
346
|
+
? ["Requested identity conflicts with the artifact manifest."]
|
|
347
|
+
: []),
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function buildEvidenceManifest(
|
|
352
|
+
report,
|
|
353
|
+
{ reportId = null, completedAt = null, versionInfo = null } = {},
|
|
354
|
+
) {
|
|
355
|
+
validateReport(report);
|
|
356
|
+
const identity = reportIdentity(report);
|
|
357
|
+
const { declaredManifest, ...packageIdentity } = identity;
|
|
358
|
+
const artifact = report.inputs.artifact;
|
|
359
|
+
const expected = artifact.expectedIntegrity;
|
|
360
|
+
const inventoryLines = report.files
|
|
361
|
+
.map((file) => [file.originalPath, file.artifactSha256, file.size])
|
|
362
|
+
.sort((a, b) => order(a[0], b[0]));
|
|
363
|
+
const ruleCounts = new Map();
|
|
364
|
+
for (const finding of report.policy.findings)
|
|
365
|
+
ruleCounts.set(finding.ruleId, (ruleCounts.get(finding.ruleId) ?? 0) + 1);
|
|
366
|
+
const sbom = buildCycloneDxSbom(report, {
|
|
367
|
+
toolVersion: versionInfo?.version ?? null,
|
|
368
|
+
});
|
|
369
|
+
const path = reportId ? `/r/${reportId}` : null;
|
|
370
|
+
return {
|
|
371
|
+
manifestVersion: "1",
|
|
372
|
+
kind: "unmask-evidence-manifest",
|
|
373
|
+
signature: {
|
|
374
|
+
status: "unsigned",
|
|
375
|
+
note: "No cryptographic signature or attestation is attached to this manifest.",
|
|
376
|
+
},
|
|
377
|
+
report: {
|
|
378
|
+
id: reportId,
|
|
379
|
+
completedAt:
|
|
380
|
+
typeof completedAt === "number"
|
|
381
|
+
? new Date(completedAt).toISOString()
|
|
382
|
+
: null,
|
|
383
|
+
schemaVersion: report.schemaVersion,
|
|
384
|
+
servedJsonSha256: sha256(JSON.stringify(report)),
|
|
385
|
+
canonicalSha256: sha256(canonicalReport(report)),
|
|
386
|
+
canonicalization:
|
|
387
|
+
"Sorted-key JSON of the report excluding run and model, newline terminated.",
|
|
388
|
+
},
|
|
389
|
+
analyzer: {
|
|
390
|
+
version: report.analyzerVersion,
|
|
391
|
+
policyVersion: report.policyVersion,
|
|
392
|
+
limitsVersion: report.limitsVersion,
|
|
393
|
+
limitsDigest: report.inputs.limitsDigest ?? null,
|
|
394
|
+
service: versionInfo
|
|
395
|
+
? {
|
|
396
|
+
version: versionInfo.version,
|
|
397
|
+
commit: versionInfo.commit,
|
|
398
|
+
tarballSha256: versionInfo.tarballSha256,
|
|
399
|
+
}
|
|
400
|
+
: null,
|
|
401
|
+
},
|
|
402
|
+
package: packageIdentity,
|
|
403
|
+
artifacts: [
|
|
404
|
+
{
|
|
405
|
+
role: "npm-tarball",
|
|
406
|
+
sha256: artifact.sha256,
|
|
407
|
+
sha512: artifact.sha512,
|
|
408
|
+
expectedIntegrity: {
|
|
409
|
+
status: integrityStatus(artifact),
|
|
410
|
+
...(expected?.algorithm
|
|
411
|
+
? { algorithm: expected.algorithm, digest: expected.digest }
|
|
412
|
+
: {}),
|
|
413
|
+
},
|
|
414
|
+
provenance: artifact.provenance ?? { status: "not-recorded" },
|
|
415
|
+
},
|
|
416
|
+
...["sourceArchive", "registryDocument"]
|
|
417
|
+
.filter((name) => report.inputs[name] !== undefined)
|
|
418
|
+
.map((name) => ({
|
|
419
|
+
role:
|
|
420
|
+
name === "sourceArchive" ? "source-archive" : "registry-document",
|
|
421
|
+
sha256: report.inputs[name].sha256,
|
|
422
|
+
kind: report.inputs[name].kind ?? null,
|
|
423
|
+
})),
|
|
424
|
+
],
|
|
425
|
+
inventory: {
|
|
426
|
+
files: report.coverage.inventory.files,
|
|
427
|
+
bytes: report.coverage.inventory.bytes,
|
|
428
|
+
complete: report.coverage.inventory.complete,
|
|
429
|
+
digestSha256: sha256(JSON.stringify(inventoryLines)),
|
|
430
|
+
digestInput:
|
|
431
|
+
"JSON array of [originalPath, artifactSha256, size] sorted by originalPath.",
|
|
432
|
+
},
|
|
433
|
+
source: {
|
|
434
|
+
status: report.source.status,
|
|
435
|
+
method: report.source.method,
|
|
436
|
+
commit: report.source.commit ?? null,
|
|
437
|
+
complete: report.source.complete,
|
|
438
|
+
alternatives: report.source.alternatives.length,
|
|
439
|
+
label: "Recorded source selection; method states how it was chosen.",
|
|
440
|
+
},
|
|
441
|
+
assessment: {
|
|
442
|
+
policyExitCode: report.policy.exitCode,
|
|
443
|
+
findings: [...ruleCounts]
|
|
444
|
+
.sort((a, b) => order(a[0], b[0]))
|
|
445
|
+
.map(([ruleId, count]) => ({ ruleId, count })),
|
|
446
|
+
failures: report.failures.map((failure) => failure.code),
|
|
447
|
+
coverage: {
|
|
448
|
+
complete: report.coverage.complete,
|
|
449
|
+
sourceAvailable: report.coverage.sourceAvailable,
|
|
450
|
+
sourceComplete: report.coverage.sourceComplete,
|
|
451
|
+
states: Object.fromEntries(
|
|
452
|
+
Object.entries(report.coverage.states).map(([key, value]) => [
|
|
453
|
+
key,
|
|
454
|
+
{ files: value.files, bytes: value.bytes },
|
|
455
|
+
]),
|
|
456
|
+
),
|
|
457
|
+
},
|
|
458
|
+
aiStatus: report.model.status,
|
|
459
|
+
safetyVerdict: "not-provided",
|
|
460
|
+
limitations: limitations(report, identity),
|
|
461
|
+
},
|
|
462
|
+
exports: {
|
|
463
|
+
report: path && `${path}.json`,
|
|
464
|
+
details: path && `${path}.details.json`,
|
|
465
|
+
sbom: path && `${path}.cdx.json`,
|
|
466
|
+
sbomServedSha256: sha256(JSON.stringify(sbom)),
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|