@xagents_in/unmask 0.1.3 → 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 +86 -4
- package/dist/cli/annotations.mjs +145 -0
- package/dist/cli/audit.mjs +223 -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 +97 -14
- package/dist/cli/preflight.mjs +388 -0
- package/dist/cli/sarif.mjs +100 -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/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 +60 -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/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,556 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { LocalInputError } from "./pipeline.mjs";
|
|
3
|
+
import { validateReport } from "./report.mjs";
|
|
4
|
+
|
|
5
|
+
export const SARIF_VERSION = "2.1.0";
|
|
6
|
+
export const SARIF_SCHEMA =
|
|
7
|
+
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json";
|
|
8
|
+
const MAX_RESULTS = 1000;
|
|
9
|
+
const MAX_LOCATIONS = 10;
|
|
10
|
+
const MAX_TEXT = 1024;
|
|
11
|
+
const advisoryStatuses = [
|
|
12
|
+
"found",
|
|
13
|
+
"none-reported",
|
|
14
|
+
"incomplete",
|
|
15
|
+
"unavailable",
|
|
16
|
+
"not-assessed",
|
|
17
|
+
];
|
|
18
|
+
const RULES = {
|
|
19
|
+
"policy/integrity-mismatch": [
|
|
20
|
+
"error",
|
|
21
|
+
"Artifact digest differs from the expected registry integrity.",
|
|
22
|
+
],
|
|
23
|
+
"policy/provenance-verification-failed": [
|
|
24
|
+
"error",
|
|
25
|
+
"Artifact provenance verification failed.",
|
|
26
|
+
],
|
|
27
|
+
"policy/identity-claim-conflict": [
|
|
28
|
+
"error",
|
|
29
|
+
"Artifact manifest identity differs from the claimed package identity.",
|
|
30
|
+
],
|
|
31
|
+
"policy/lifecycle-source-absent": [
|
|
32
|
+
"error",
|
|
33
|
+
"Artifact install lifecycle script is absent from or differs in the complete selected source.",
|
|
34
|
+
],
|
|
35
|
+
"policy/lifecycle-implicit-gyp": [
|
|
36
|
+
"error",
|
|
37
|
+
"Artifact-only binding.gyp enables an implicit npm install build step.",
|
|
38
|
+
],
|
|
39
|
+
"advisory/known-malware": [
|
|
40
|
+
"error",
|
|
41
|
+
"OSV reports a malicious-package advisory for this exact package version.",
|
|
42
|
+
],
|
|
43
|
+
"advisory/known-vulnerability": [
|
|
44
|
+
"warning",
|
|
45
|
+
"OSV reports a vulnerability advisory for this exact package version.",
|
|
46
|
+
],
|
|
47
|
+
"advisory/withdrawn": [
|
|
48
|
+
"note",
|
|
49
|
+
"OSV lists a withdrawn advisory for this exact package version.",
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const failure = (code, message) => new LocalInputError(code, message);
|
|
53
|
+
const text = (value, limit = MAX_TEXT) =>
|
|
54
|
+
typeof value === "string"
|
|
55
|
+
? value.length > limit
|
|
56
|
+
? `${value.slice(0, limit)}…`
|
|
57
|
+
: value
|
|
58
|
+
: "";
|
|
59
|
+
const token = (value) =>
|
|
60
|
+
typeof value === "string" && /^[a-z][a-z0-9-]{0,63}$/.test(value)
|
|
61
|
+
? value
|
|
62
|
+
: "unrecognized";
|
|
63
|
+
const sha256 = (value) =>
|
|
64
|
+
createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
65
|
+
const usableManifest = (manifest) =>
|
|
66
|
+
manifest?.status === "usable" &&
|
|
67
|
+
typeof manifest.value?.name === "string" &&
|
|
68
|
+
typeof manifest.value?.version === "string";
|
|
69
|
+
|
|
70
|
+
export function reportIdentity(report) {
|
|
71
|
+
const claim = report.inputs.claim;
|
|
72
|
+
const requested = claim ? { name: claim.name, version: claim.version } : null;
|
|
73
|
+
const declared = usableManifest(report.manifests.artifact)
|
|
74
|
+
? {
|
|
75
|
+
name: report.manifests.artifact.value.name,
|
|
76
|
+
version: report.manifests.artifact.value.version,
|
|
77
|
+
}
|
|
78
|
+
: null;
|
|
79
|
+
const status =
|
|
80
|
+
requested && declared
|
|
81
|
+
? requested.name === declared.name &&
|
|
82
|
+
requested.version === declared.version
|
|
83
|
+
? "claim-matches-manifest"
|
|
84
|
+
: "claim-conflicts-with-manifest"
|
|
85
|
+
: requested
|
|
86
|
+
? "claimed-manifest-unavailable"
|
|
87
|
+
: declared
|
|
88
|
+
? "manifest-declared-unclaimed"
|
|
89
|
+
: "unknown";
|
|
90
|
+
const bound = requested ?? declared;
|
|
91
|
+
return bound
|
|
92
|
+
? {
|
|
93
|
+
...bound,
|
|
94
|
+
source: requested ? "claim" : "artifact-manifest",
|
|
95
|
+
identityStatus: status,
|
|
96
|
+
requested,
|
|
97
|
+
declared,
|
|
98
|
+
}
|
|
99
|
+
: null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function advisoryId(value) {
|
|
103
|
+
return typeof value === "string" && /^[A-Za-z0-9:_.-]{1,256}$/.test(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function advisorySnapshot(details, identity) {
|
|
107
|
+
if (details === undefined) return undefined;
|
|
108
|
+
const snapshot =
|
|
109
|
+
details && typeof details === "object" && !Array.isArray(details)
|
|
110
|
+
? details.source === "OSV"
|
|
111
|
+
? details
|
|
112
|
+
: details.vulnerabilityCheck
|
|
113
|
+
: undefined;
|
|
114
|
+
const invalid = () =>
|
|
115
|
+
failure(
|
|
116
|
+
"details-invalid",
|
|
117
|
+
"Details must be an UNMASK package details document or OSV vulnerability check",
|
|
118
|
+
);
|
|
119
|
+
if (
|
|
120
|
+
!snapshot ||
|
|
121
|
+
typeof snapshot !== "object" ||
|
|
122
|
+
Array.isArray(snapshot) ||
|
|
123
|
+
snapshot.source !== "OSV" ||
|
|
124
|
+
!advisoryStatuses.includes(snapshot.status) ||
|
|
125
|
+
!Array.isArray(snapshot.advisories) ||
|
|
126
|
+
!Array.isArray(snapshot.reasons ?? []) ||
|
|
127
|
+
snapshot.advisories.length > 1000 ||
|
|
128
|
+
snapshot.advisories.some(
|
|
129
|
+
(item) =>
|
|
130
|
+
!item ||
|
|
131
|
+
typeof item !== "object" ||
|
|
132
|
+
Array.isArray(item) ||
|
|
133
|
+
!advisoryId(item.id),
|
|
134
|
+
) ||
|
|
135
|
+
(snapshot.status === "found" && snapshot.advisories.length === 0) ||
|
|
136
|
+
(snapshot.status === "none-reported" && snapshot.advisories.length > 0) ||
|
|
137
|
+
snapshot.complete !== ["found", "none-reported"].includes(snapshot.status)
|
|
138
|
+
)
|
|
139
|
+
throw invalid();
|
|
140
|
+
const pkg = snapshot.package;
|
|
141
|
+
if (pkg !== undefined && pkg !== null) {
|
|
142
|
+
if (
|
|
143
|
+
!identity ||
|
|
144
|
+
pkg.ecosystem !== "npm" ||
|
|
145
|
+
pkg.name !== identity.name ||
|
|
146
|
+
pkg.version !== identity.version
|
|
147
|
+
)
|
|
148
|
+
throw failure(
|
|
149
|
+
"details-identity-mismatch",
|
|
150
|
+
"Advisory package identity does not match the report identity",
|
|
151
|
+
);
|
|
152
|
+
} else if (snapshot.advisories.length > 0)
|
|
153
|
+
throw failure(
|
|
154
|
+
"details-identity-mismatch",
|
|
155
|
+
"Advisory findings without a package identity cannot be bound to the report",
|
|
156
|
+
);
|
|
157
|
+
return {
|
|
158
|
+
status: snapshot.status,
|
|
159
|
+
complete: snapshot.complete,
|
|
160
|
+
queriedAt:
|
|
161
|
+
typeof snapshot.queriedAt === "string" ? snapshot.queriedAt : null,
|
|
162
|
+
reasons: (snapshot.reasons ?? []).map(token).slice(0, 32),
|
|
163
|
+
boundTo: identity?.source ?? null,
|
|
164
|
+
advisories: snapshot.advisories.map((item) => ({
|
|
165
|
+
id: item.id,
|
|
166
|
+
aliases: Array.isArray(item.aliases)
|
|
167
|
+
? item.aliases.filter(advisoryId).slice(0, 16)
|
|
168
|
+
: [],
|
|
169
|
+
summary: text(item.summary),
|
|
170
|
+
withdrawn: typeof item.withdrawn === "string",
|
|
171
|
+
severity: severityOf(item),
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function severityOf(item) {
|
|
177
|
+
const affected = Array.isArray(item.affected) ? item.affected : [];
|
|
178
|
+
const scores = [
|
|
179
|
+
...(Array.isArray(item.severity) ? item.severity : []),
|
|
180
|
+
...affected.flatMap((entry) =>
|
|
181
|
+
Array.isArray(entry?.severity) ? entry.severity : [],
|
|
182
|
+
),
|
|
183
|
+
]
|
|
184
|
+
.filter(
|
|
185
|
+
(entry) =>
|
|
186
|
+
typeof entry?.type === "string" && typeof entry?.score === "string",
|
|
187
|
+
)
|
|
188
|
+
.slice(0, 8)
|
|
189
|
+
.map((entry) => ({
|
|
190
|
+
type: text(entry.type, 32),
|
|
191
|
+
score: text(entry.score, 512),
|
|
192
|
+
}));
|
|
193
|
+
const labels = [
|
|
194
|
+
...new Set(
|
|
195
|
+
[
|
|
196
|
+
item.databaseSeverity,
|
|
197
|
+
...affected.map((entry) => entry?.ecosystemSeverity),
|
|
198
|
+
]
|
|
199
|
+
.filter((value) => typeof value === "string" && value)
|
|
200
|
+
.map((value) => text(value, 64)),
|
|
201
|
+
),
|
|
202
|
+
].slice(0, 8);
|
|
203
|
+
return scores.length || labels.length
|
|
204
|
+
? { status: "recorded", scores, labels, levelBasis: "fixed-rule-mapping" }
|
|
205
|
+
: { status: "not-recorded", levelBasis: "fixed-rule-mapping" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function malwareIdentifier(advisory) {
|
|
209
|
+
return (
|
|
210
|
+
[advisory.id, ...advisory.aliases].find((id) => /^MAL-/.test(id)) ?? null
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function policyMessage(finding) {
|
|
215
|
+
switch (finding.ruleId) {
|
|
216
|
+
case "integrity-mismatch":
|
|
217
|
+
return `Expected ${finding.algorithm} ${finding.expected} but the artifact digest is ${finding.actual}.`;
|
|
218
|
+
case "identity-claim-conflict":
|
|
219
|
+
return `Artifact manifest differs from the claimed identity in: ${finding.fields.join(", ")}.`;
|
|
220
|
+
case "lifecycle-source-absent":
|
|
221
|
+
return `Install hook "${finding.script}" in the artifact is absent from or differs in the complete selected source; it was not executed.`;
|
|
222
|
+
default:
|
|
223
|
+
return RULES[`policy/${finding.ruleId}`]?.[1] ?? "Policy finding.";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function locate(report, evidenceIds) {
|
|
228
|
+
const byId = new Map(report.evidence.map((item) => [item.id, item]));
|
|
229
|
+
const physical = [];
|
|
230
|
+
const unmapped = [];
|
|
231
|
+
for (const id of evidenceIds) {
|
|
232
|
+
const item = byId.get(id);
|
|
233
|
+
if (!item) continue;
|
|
234
|
+
if (item.path === null) {
|
|
235
|
+
unmapped.push({ side: item.side, originalPath: text(item.originalPath) });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (physical.length < MAX_LOCATIONS)
|
|
239
|
+
physical.push({
|
|
240
|
+
side: item.side,
|
|
241
|
+
path: item.path,
|
|
242
|
+
byteOffset: item.range[0],
|
|
243
|
+
byteLength: item.range[1] - item.range[0],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return { physical, unmapped };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function collectFindings(input, details) {
|
|
250
|
+
let report;
|
|
251
|
+
try {
|
|
252
|
+
report = validateReport(input);
|
|
253
|
+
} catch {
|
|
254
|
+
throw failure(
|
|
255
|
+
"report-invalid",
|
|
256
|
+
"Report failed schema, evidence or deterministic policy validation",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const identity = reportIdentity(report);
|
|
260
|
+
const advisories = advisorySnapshot(details, identity);
|
|
261
|
+
const results = [];
|
|
262
|
+
const limitations = [];
|
|
263
|
+
function add(ruleId, level, message, evidenceIds, discriminator, extra = {}) {
|
|
264
|
+
const { physical, unmapped } = locate(report, evidenceIds);
|
|
265
|
+
results.push({
|
|
266
|
+
ruleId,
|
|
267
|
+
level,
|
|
268
|
+
message: text(message, 2048),
|
|
269
|
+
locations: physical,
|
|
270
|
+
evidenceIds,
|
|
271
|
+
fingerprint: sha256([
|
|
272
|
+
ruleId,
|
|
273
|
+
report.inputs.artifact.sha256,
|
|
274
|
+
evidenceIds,
|
|
275
|
+
discriminator,
|
|
276
|
+
]),
|
|
277
|
+
...(unmapped.length ? { unmappedEvidence: unmapped } : {}),
|
|
278
|
+
...extra,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
for (const finding of report.policy.findings)
|
|
282
|
+
add(
|
|
283
|
+
`policy/${finding.ruleId}`,
|
|
284
|
+
"error",
|
|
285
|
+
policyMessage(finding),
|
|
286
|
+
finding.evidenceIds,
|
|
287
|
+
finding.script ?? finding.fields ?? finding.algorithm ?? null,
|
|
288
|
+
);
|
|
289
|
+
for (const advisory of advisories?.advisories ?? []) {
|
|
290
|
+
const malware = malwareIdentifier(advisory);
|
|
291
|
+
const ruleId = advisory.withdrawn
|
|
292
|
+
? "advisory/withdrawn"
|
|
293
|
+
: malware
|
|
294
|
+
? "advisory/known-malware"
|
|
295
|
+
: "advisory/known-vulnerability";
|
|
296
|
+
add(
|
|
297
|
+
ruleId,
|
|
298
|
+
RULES[ruleId][0],
|
|
299
|
+
`${advisory.id}${malware && malware !== advisory.id ? ` (alias ${malware})` : ""}: ${advisory.summary || RULES[ruleId][1]} (OSV exact-version query for ${identity.name}@${identity.version}).`,
|
|
300
|
+
[],
|
|
301
|
+
advisory.id,
|
|
302
|
+
{
|
|
303
|
+
advisoryId: advisory.id,
|
|
304
|
+
aliases: advisory.aliases,
|
|
305
|
+
malwareIdentifier: malware,
|
|
306
|
+
severity: advisory.severity,
|
|
307
|
+
package: identity,
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
for (const cue of report.cues) {
|
|
312
|
+
const evidenceIds = Array.isArray(cue.evidenceIds) ? cue.evidenceIds : [];
|
|
313
|
+
const parts = [
|
|
314
|
+
typeof cue.field === "string" ? `field ${text(cue.field, 256)}` : "",
|
|
315
|
+
typeof cue.literalStatus === "string"
|
|
316
|
+
? `literal ${token(cue.literalStatus)}`
|
|
317
|
+
: "",
|
|
318
|
+
].filter(Boolean);
|
|
319
|
+
add(
|
|
320
|
+
`cue/${token(cue.ruleId)}`,
|
|
321
|
+
"note",
|
|
322
|
+
`${text(cue.summary) || "Review cue recorded by the analyzer; behavior is not proven."}${parts.length ? ` (${parts.join("; ")})` : ""}`,
|
|
323
|
+
evidenceIds,
|
|
324
|
+
cue.field ?? null,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const coverage = report.coverage;
|
|
328
|
+
const note = (id, level, message) => limitations.push({ id, level, message });
|
|
329
|
+
if (!coverage.inventory.complete)
|
|
330
|
+
note(
|
|
331
|
+
"coverage/inventory-lower-bound",
|
|
332
|
+
"warning",
|
|
333
|
+
"Artifact inventory reached a limit; file and byte counts are lower bounds.",
|
|
334
|
+
);
|
|
335
|
+
if (!coverage.sourceAvailable)
|
|
336
|
+
note(
|
|
337
|
+
"coverage/source-unavailable",
|
|
338
|
+
"warning",
|
|
339
|
+
"No source archive was compared; artifact files have no source correspondence.",
|
|
340
|
+
);
|
|
341
|
+
else if (!coverage.sourceComplete)
|
|
342
|
+
note(
|
|
343
|
+
"coverage/source-incomplete",
|
|
344
|
+
"warning",
|
|
345
|
+
"Source archive coverage is incomplete; source absence is not established.",
|
|
346
|
+
);
|
|
347
|
+
if (coverage.unresolved.files > 0)
|
|
348
|
+
note(
|
|
349
|
+
"coverage/unresolved-files",
|
|
350
|
+
"note",
|
|
351
|
+
`${coverage.unresolved.files} of ${coverage.inventory.files} artifact files are not byte-identical to selected source (${coverage.states["not-compared"].files} not compared).`,
|
|
352
|
+
);
|
|
353
|
+
const skipped = report.annotations.filter(
|
|
354
|
+
(item) =>
|
|
355
|
+
item?.type === "syntax-not-inspected" &&
|
|
356
|
+
item.side === "artifact" &&
|
|
357
|
+
item.reason !== "unsupported-syntax",
|
|
358
|
+
).length;
|
|
359
|
+
if (skipped > 0)
|
|
360
|
+
note(
|
|
361
|
+
"coverage/syntax-not-inspected",
|
|
362
|
+
"note",
|
|
363
|
+
`${skipped} artifact JavaScript files were not syntax-inspected.`,
|
|
364
|
+
);
|
|
365
|
+
for (const item of report.failures.slice(0, 20))
|
|
366
|
+
note(
|
|
367
|
+
"analysis/failure",
|
|
368
|
+
"error",
|
|
369
|
+
`${text(item.code, 128)}: ${text(item.message)}`,
|
|
370
|
+
);
|
|
371
|
+
if (!advisories)
|
|
372
|
+
note(
|
|
373
|
+
"advisory/not-supplied",
|
|
374
|
+
"note",
|
|
375
|
+
"No advisory snapshot was supplied; known malware and vulnerability advisories are not assessed in this export.",
|
|
376
|
+
);
|
|
377
|
+
else if (!advisories.complete)
|
|
378
|
+
note(
|
|
379
|
+
"advisory/incomplete",
|
|
380
|
+
"warning",
|
|
381
|
+
`OSV advisory check is ${advisories.status}${advisories.reasons.length ? ` (${advisories.reasons.join(", ")})` : ""}; missing advisories do not mean none exist.`,
|
|
382
|
+
);
|
|
383
|
+
if (advisories && identity.identityStatus === "claim-conflicts-with-manifest")
|
|
384
|
+
note(
|
|
385
|
+
"advisory/identity-conflict",
|
|
386
|
+
"warning",
|
|
387
|
+
`Advisories were queried for the requested identity ${identity.requested.name}@${identity.requested.version}, which conflicts with the artifact manifest ${text(identity.declared.name, 214)}@${text(identity.declared.version, 256)}; the declared identity was not screened.`,
|
|
388
|
+
);
|
|
389
|
+
else if (
|
|
390
|
+
advisories &&
|
|
391
|
+
identity.identityStatus === "claimed-manifest-unavailable"
|
|
392
|
+
)
|
|
393
|
+
note(
|
|
394
|
+
"advisory/identity-unverified",
|
|
395
|
+
"note",
|
|
396
|
+
`Advisories were queried for the requested identity ${identity.requested.name}@${identity.requested.version}; the artifact manifest is unavailable, so the binding to the analyzed bytes is unverified.`,
|
|
397
|
+
);
|
|
398
|
+
if (results.length > MAX_RESULTS)
|
|
399
|
+
note(
|
|
400
|
+
"export/results-truncated",
|
|
401
|
+
"warning",
|
|
402
|
+
`Only the first ${MAX_RESULTS} of ${results.length} results are exported.`,
|
|
403
|
+
);
|
|
404
|
+
note(
|
|
405
|
+
"limitation/absence-not-safety",
|
|
406
|
+
"note",
|
|
407
|
+
"Absence of results does not establish that the package is safe; only recorded evidence, deterministic policy rules and the supplied advisory snapshot are reported.",
|
|
408
|
+
);
|
|
409
|
+
return {
|
|
410
|
+
report,
|
|
411
|
+
identity,
|
|
412
|
+
advisories,
|
|
413
|
+
results: results.slice(0, MAX_RESULTS),
|
|
414
|
+
limitations,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function ruleDescriptor(ruleId, level) {
|
|
419
|
+
const known = RULES[ruleId];
|
|
420
|
+
return {
|
|
421
|
+
id: ruleId,
|
|
422
|
+
name: ruleId.replace(/[^A-Za-z0-9]+(.)?/g, (_, next = "") =>
|
|
423
|
+
next.toUpperCase(),
|
|
424
|
+
),
|
|
425
|
+
shortDescription: {
|
|
426
|
+
text: known?.[1] ?? "Analyzer review cue; behavior is not proven.",
|
|
427
|
+
},
|
|
428
|
+
defaultConfiguration: { level: known?.[0] ?? level },
|
|
429
|
+
properties: { tags: ["security", "supply-chain", ruleId.split("/")[0]] },
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function toSarif(input, details) {
|
|
434
|
+
const collected = collectFindings(input, details);
|
|
435
|
+
const { report, identity, advisories, results, limitations } = collected;
|
|
436
|
+
const ruleIds = [...new Set(results.map((item) => item.ruleId))].sort();
|
|
437
|
+
const rules = ruleIds.map((id) =>
|
|
438
|
+
ruleDescriptor(id, results.find((item) => item.ruleId === id).level),
|
|
439
|
+
);
|
|
440
|
+
const index = new Map(ruleIds.map((id, position) => [id, position]));
|
|
441
|
+
return {
|
|
442
|
+
$schema: SARIF_SCHEMA,
|
|
443
|
+
version: SARIF_VERSION,
|
|
444
|
+
runs: [
|
|
445
|
+
{
|
|
446
|
+
tool: {
|
|
447
|
+
driver: {
|
|
448
|
+
name: "unmask",
|
|
449
|
+
version: text(report.analyzerVersion, 64),
|
|
450
|
+
informationUri: "https://github.com/Vaiditya2207/unmask",
|
|
451
|
+
rules,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
originalUriBaseIds: {
|
|
455
|
+
ARTIFACT_ROOT: {
|
|
456
|
+
description: {
|
|
457
|
+
text: "Root of the analyzed npm artifact after removing its archive prefix.",
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
SOURCE_ROOT: {
|
|
461
|
+
description: {
|
|
462
|
+
text: "Root of the selected source archive after removing its archive prefix.",
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
invocations: [
|
|
467
|
+
{
|
|
468
|
+
executionSuccessful: report.failures.length === 0,
|
|
469
|
+
toolExecutionNotifications: limitations.map((item) => ({
|
|
470
|
+
descriptor: { id: item.id },
|
|
471
|
+
level: item.level,
|
|
472
|
+
message: { text: item.message },
|
|
473
|
+
})),
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
results: results.map((item) => ({
|
|
477
|
+
ruleId: item.ruleId,
|
|
478
|
+
ruleIndex: index.get(item.ruleId),
|
|
479
|
+
level: item.level,
|
|
480
|
+
message: { text: item.message },
|
|
481
|
+
...(item.locations.length
|
|
482
|
+
? {
|
|
483
|
+
locations: item.locations.map((location) => ({
|
|
484
|
+
physicalLocation: {
|
|
485
|
+
artifactLocation: {
|
|
486
|
+
uri: location.path
|
|
487
|
+
.split("/")
|
|
488
|
+
.map(encodeURIComponent)
|
|
489
|
+
.join("/"),
|
|
490
|
+
uriBaseId:
|
|
491
|
+
location.side === "artifact"
|
|
492
|
+
? "ARTIFACT_ROOT"
|
|
493
|
+
: "SOURCE_ROOT",
|
|
494
|
+
},
|
|
495
|
+
region: {
|
|
496
|
+
byteOffset: location.byteOffset,
|
|
497
|
+
byteLength: location.byteLength,
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
})),
|
|
501
|
+
}
|
|
502
|
+
: item.package
|
|
503
|
+
? {
|
|
504
|
+
locations: [
|
|
505
|
+
{
|
|
506
|
+
logicalLocations: [
|
|
507
|
+
{
|
|
508
|
+
fullyQualifiedName: `npm:${item.package.name}@${item.package.version}`,
|
|
509
|
+
kind: "module",
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
}
|
|
515
|
+
: {}),
|
|
516
|
+
partialFingerprints: { "unmaskFinding/v1": item.fingerprint },
|
|
517
|
+
properties: {
|
|
518
|
+
evidenceIds: item.evidenceIds,
|
|
519
|
+
...(item.advisoryId
|
|
520
|
+
? {
|
|
521
|
+
advisoryId: item.advisoryId,
|
|
522
|
+
aliases: item.aliases,
|
|
523
|
+
malwareIdentifier: item.malwareIdentifier,
|
|
524
|
+
severity: item.severity,
|
|
525
|
+
}
|
|
526
|
+
: {}),
|
|
527
|
+
...(item.unmappedEvidence
|
|
528
|
+
? { unmappedEvidence: item.unmappedEvidence }
|
|
529
|
+
: {}),
|
|
530
|
+
},
|
|
531
|
+
})),
|
|
532
|
+
properties: {
|
|
533
|
+
unmask: {
|
|
534
|
+
reportSchemaVersion: report.schemaVersion,
|
|
535
|
+
policyVersion: report.policyVersion,
|
|
536
|
+
policyExitCode: report.policy.exitCode,
|
|
537
|
+
package: identity,
|
|
538
|
+
artifactSha256: report.inputs.artifact.sha256,
|
|
539
|
+
coverageComplete: report.coverage.complete,
|
|
540
|
+
sourceStatus: report.source.status,
|
|
541
|
+
modelStatus: text(report.model.status, 64),
|
|
542
|
+
advisories: advisories
|
|
543
|
+
? {
|
|
544
|
+
status: advisories.status,
|
|
545
|
+
complete: advisories.complete,
|
|
546
|
+
queriedAt: advisories.queriedAt,
|
|
547
|
+
count: advisories.advisories.length,
|
|
548
|
+
boundTo: advisories.boundTo,
|
|
549
|
+
}
|
|
550
|
+
: { status: "not-supplied", complete: false },
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
}
|