@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,183 @@
|
|
|
1
|
+
export const SUMMARY_POLICY = "every-check-2";
|
|
2
|
+
const SUMMARY_TEMPLATE = "≤600 chars";
|
|
3
|
+
const SUMMARY_TASK =
|
|
4
|
+
" Ground summary and assessment in scope and evidence; no safety verdicts.";
|
|
5
|
+
const ASSESSMENT_TEMPLATE = {
|
|
6
|
+
uncertainty: "unchecked ≤300",
|
|
7
|
+
nextAction: "review step ≤200",
|
|
8
|
+
};
|
|
9
|
+
const VERDICT =
|
|
10
|
+
/\b(?:(?:un)?safe(?:ly|ty)?|benign|harmless|malware-free|risk-free|trustworthy|no risks?)\b/iu;
|
|
11
|
+
// Only exact caveat templates may mention safety; any other negation still counts as a verdict.
|
|
12
|
+
const CAVEAT =
|
|
13
|
+
/\bnot (?:a |an )?(?:proof|evidence|guarantee|indication)(?: that)?(?: (?:the|this) package| it)?(?: is)? safe\b|\b(?:not a|no) safety (?:verdict|signal|claim|guarantee)\b|\bdoes not (?:mean|imply|prove|establish|show) (?:that )?(?:it|the package|this package) is safe\b/giu;
|
|
14
|
+
const ABSENCE_CLAIM =
|
|
15
|
+
/\b(?:absent|missing|lacks?|omits?)\b|does not (?:exist|contain|include)|not (?:present|in)\b|not found(?! within supplied evidence)/iu;
|
|
16
|
+
const SCOPED_ABSENCE =
|
|
17
|
+
/\b(?:absent|missing|not (?:present|found|in))\s+(?:(?:from|in|within)\s+)?(?:the\s+)?(?:selected source|supplied (?:evidence|scope))\b|\bsource(?: archive)? (?:is|was) (?:missing|absent)\b|\bomits?\s+\d+\s+(?:evidence\s+)?(?:items?|groups?)\b/giu;
|
|
18
|
+
const REVIEW_STEP =
|
|
19
|
+
/^(?:review|inspect|compare|confirm|verify|check|locate|examine|trace|investigate|read|diff|audit)\b/iu;
|
|
20
|
+
const GO_AHEAD =
|
|
21
|
+
/\b(?:proceed|go ahead|approve|ok(?:ay)? to|fine to|safe to|allow(?:ed)? to install)\b/iu;
|
|
22
|
+
// Controls, invisible format characters and line separators are rejected; newline and tab stay allowed.
|
|
23
|
+
const CONTROL = /(?![\n\t])[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
24
|
+
const FENCE = /^\s*```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```\s*$/u;
|
|
25
|
+
export const hasControl = (value) => CONTROL.test(value);
|
|
26
|
+
// One whole-body code fence is unwrapped; JSON embedded in other prose is still rejected.
|
|
27
|
+
export const unfence = (raw) => FENCE.exec(raw)?.[1] ?? raw;
|
|
28
|
+
const counter = (value) =>
|
|
29
|
+
Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
30
|
+
const ruleIds = (items) =>
|
|
31
|
+
[
|
|
32
|
+
...new Set(
|
|
33
|
+
items
|
|
34
|
+
.map((item) => item?.ruleId)
|
|
35
|
+
.filter(
|
|
36
|
+
(id) => typeof id === "string" && /^[a-z0-9-]{1,64}$/u.test(id),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
].sort();
|
|
40
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
41
|
+
|
|
42
|
+
export function scopeFor(report, bundle) {
|
|
43
|
+
const files = list(report.files);
|
|
44
|
+
const states = {};
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const state = /^[a-z-]{1,32}$/u.test(file?.state ?? "")
|
|
47
|
+
? file.state
|
|
48
|
+
: "other";
|
|
49
|
+
states[state] = (states[state] ?? 0) + 1;
|
|
50
|
+
}
|
|
51
|
+
const coverage = report.coverage ?? {};
|
|
52
|
+
const findings = list(report.policy?.findings);
|
|
53
|
+
const cues = list(report.cues);
|
|
54
|
+
const supplied = new Set(list(bundle.evidenceGroups).map((g) => g.ruleId));
|
|
55
|
+
const scope = {
|
|
56
|
+
files: files.length,
|
|
57
|
+
states,
|
|
58
|
+
inventory: counter(coverage.inventory?.files),
|
|
59
|
+
complete: coverage.inventory?.complete === true,
|
|
60
|
+
compared: counter(coverage.compared?.files),
|
|
61
|
+
syntax: counter(coverage.syntaxInspected?.files),
|
|
62
|
+
source: /^[a-z-]{1,32}$/u.test(report.source?.status ?? "")
|
|
63
|
+
? report.source.status
|
|
64
|
+
: "unknown",
|
|
65
|
+
exit: counter(report.policy?.exitCode),
|
|
66
|
+
findings: findings.length,
|
|
67
|
+
cues: cues.length,
|
|
68
|
+
// Rule IDs already named by a supplied group are not repeated.
|
|
69
|
+
unseenRules: ruleIds([...findings, ...cues])
|
|
70
|
+
.filter((id) => !supplied.has(id))
|
|
71
|
+
.slice(0, 6),
|
|
72
|
+
supplied: bundle.selected.size,
|
|
73
|
+
omitted: bundle.omittedEvidenceIds.length,
|
|
74
|
+
};
|
|
75
|
+
for (const [key, value] of Object.entries(scope))
|
|
76
|
+
if (value === null || (Array.isArray(value) && !value.length))
|
|
77
|
+
delete scope[key];
|
|
78
|
+
return scope;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function summaryBundle(promptFor, report, inputLimit) {
|
|
82
|
+
let limit = inputLimit;
|
|
83
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
84
|
+
const bundle = promptFor(report, limit);
|
|
85
|
+
const scope = scopeFor(report, bundle);
|
|
86
|
+
const payload = JSON.parse(bundle.prompt);
|
|
87
|
+
const prompt = JSON.stringify({
|
|
88
|
+
...payload,
|
|
89
|
+
task: payload.task + SUMMARY_TASK,
|
|
90
|
+
output: {
|
|
91
|
+
summary: SUMMARY_TEMPLATE,
|
|
92
|
+
assessment: ASSESSMENT_TEMPLATE,
|
|
93
|
+
...payload.output,
|
|
94
|
+
},
|
|
95
|
+
scope,
|
|
96
|
+
});
|
|
97
|
+
const over = Buffer.byteLength(prompt) + 256 - inputLimit;
|
|
98
|
+
if (over <= 0 || pass === 5) return { ...bundle, prompt, scope };
|
|
99
|
+
// Repack below the bytes actually used so each pass strictly shrinks evidence.
|
|
100
|
+
limit = Math.min(limit, Buffer.byteLength(bundle.prompt) + 256) - over;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function prose(value, max, secret) {
|
|
105
|
+
return (
|
|
106
|
+
typeof value === "string" &&
|
|
107
|
+
value.trim().length > 0 &&
|
|
108
|
+
value.length <= max &&
|
|
109
|
+
!value.includes(secret) &&
|
|
110
|
+
!hasControl(value) &&
|
|
111
|
+
!VERDICT.test(value.replace(CAVEAT, " ")) &&
|
|
112
|
+
!ABSENCE_CLAIM.test(value.replace(SCOPED_ABSENCE, " "))
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const NONE = "(?:no|zero|0|without(?: any)?)\\s+";
|
|
117
|
+
const UNLOCALIZED = "(?!\\s+(?:in|for|on|within)\\b)";
|
|
118
|
+
const NO_FINDINGS = new RegExp(
|
|
119
|
+
`\\b${NONE}(?:default\\s+)?(?:policy\\s+)?findings?\\b${UNLOCALIZED}`,
|
|
120
|
+
"iu",
|
|
121
|
+
);
|
|
122
|
+
const NO_CUES = new RegExp(
|
|
123
|
+
`\\b${NONE}(?:review\\s+)?(?:cues?|suggestions?|observations?)\\b${UNLOCALIZED}`,
|
|
124
|
+
"iu",
|
|
125
|
+
);
|
|
126
|
+
const ALL_SAME = new RegExp(
|
|
127
|
+
`\\b(?:all|every|each)\\b[^.;]{0,40}?\\b(?:byte-identical|identical|unchanged|match(?:es|ed)?)\\b|\\b(?:no|zero)\\s+(?:file\\s+)?(?:changes?|differences?)\\b${UNLOCALIZED}|\\bno\\s+files?\\s+(?:changed|differ(?:s|ed)?)\\b|\\bnothing\\s+(?:changed|differs)\\b`,
|
|
128
|
+
"iu",
|
|
129
|
+
);
|
|
130
|
+
// Whole-report claims must not contradict the deterministic scope the model was given.
|
|
131
|
+
export function consistentWithScope(value, scope = {}) {
|
|
132
|
+
const states = scope.states ?? {};
|
|
133
|
+
const differs = Object.keys(states).some(
|
|
134
|
+
(state) => state !== "byte-identical" && states[state] > 0,
|
|
135
|
+
);
|
|
136
|
+
return !(
|
|
137
|
+
(scope.findings > 0 && NO_FINDINGS.test(value)) ||
|
|
138
|
+
(scope.cues > 0 && NO_CUES.test(value)) ||
|
|
139
|
+
(differs && ALL_SAME.test(value))
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function validSummary(value, secret, scope) {
|
|
144
|
+
return prose(value, 600, secret) && consistentWithScope(value, scope);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function validAssessment(value, secret) {
|
|
148
|
+
return (
|
|
149
|
+
value !== null &&
|
|
150
|
+
typeof value === "object" &&
|
|
151
|
+
!Array.isArray(value) &&
|
|
152
|
+
Object.keys(value).length === 2 &&
|
|
153
|
+
prose(value.uncertainty, 300, secret) &&
|
|
154
|
+
prose(value.nextAction, 200, secret) &&
|
|
155
|
+
REVIEW_STEP.test(value.nextAction.trim()) &&
|
|
156
|
+
!GO_AHEAD.test(value.nextAction)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function checkedScopeFor(bundle) {
|
|
161
|
+
const { scope } = bundle;
|
|
162
|
+
const notChecked = ["runtime-behavior"];
|
|
163
|
+
if (scope.omitted) notChecked.push("omitted-evidence");
|
|
164
|
+
if (!scope.complete) notChecked.push("incomplete-inventory");
|
|
165
|
+
if (!["selected", "resolved"].includes(scope.source))
|
|
166
|
+
notChecked.push("unselected-source");
|
|
167
|
+
for (const [key, code] of [
|
|
168
|
+
["compared", "uncompared-files"],
|
|
169
|
+
["syntax", "uninspected-syntax"],
|
|
170
|
+
])
|
|
171
|
+
if (!(scope[key] >= (scope.inventory ?? 0))) notChecked.push(code);
|
|
172
|
+
return {
|
|
173
|
+
basis: "deterministic-prompt-selection",
|
|
174
|
+
evidenceSupplied: scope.supplied,
|
|
175
|
+
evidenceOmitted: scope.omitted,
|
|
176
|
+
groupsSupplied: bundle.evidenceGroups.length,
|
|
177
|
+
groupsOmitted: bundle.omittedEvidenceGroups.length,
|
|
178
|
+
paths: [...new Set([...bundle.selected.values()].map((e) => e.path))]
|
|
179
|
+
.sort()
|
|
180
|
+
.slice(0, 16),
|
|
181
|
+
notChecked,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boundedVulnerabilityCheck,
|
|
3
|
+
unavailableVulnerabilityCheck,
|
|
4
|
+
} from "./enrichment.mjs";
|
|
5
|
+
|
|
6
|
+
const escape = (value) =>
|
|
7
|
+
String(value ?? "").replace(
|
|
8
|
+
/[&<>"']/g,
|
|
9
|
+
(ch) =>
|
|
10
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
11
|
+
ch
|
|
12
|
+
],
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const ASSESSMENTS = {
|
|
16
|
+
found: "advisories-reported",
|
|
17
|
+
incomplete: "advisory-lookup-incomplete",
|
|
18
|
+
"none-reported": "no-advisory-reported",
|
|
19
|
+
unavailable: "advisory-lookup-unavailable",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function advisoryOnlyResult(vulnerabilityCheck, { artifactReason }) {
|
|
23
|
+
const status = vulnerabilityCheck?.status;
|
|
24
|
+
const assessment = ASSESSMENTS[status] ?? "advisory-lookup-unavailable";
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: "1",
|
|
27
|
+
kind: "advisory-only",
|
|
28
|
+
package: vulnerabilityCheck?.package ?? null,
|
|
29
|
+
assessment,
|
|
30
|
+
advisoryCount: vulnerabilityCheck?.advisories?.length ?? 0,
|
|
31
|
+
artifactVerification: { status: "not-performed", reason: artifactReason },
|
|
32
|
+
sourceComparison: { status: "not-performed", reason: artifactReason },
|
|
33
|
+
safetyVerdict: "not-provided",
|
|
34
|
+
notice:
|
|
35
|
+
assessment === "advisories-reported" ||
|
|
36
|
+
(assessment === "advisory-lookup-incomplete" &&
|
|
37
|
+
vulnerabilityCheck.advisories.length > 0)
|
|
38
|
+
? "Published advisories match this exact version. The registry artifact was not inspected, so no tarball or source evidence is included."
|
|
39
|
+
: "No artifact was inspected. Absence of a current advisory, or an unavailable lookup, is not evidence of safety, and an unacquirable artifact is not by itself evidence of malware.",
|
|
40
|
+
vulnerabilityCheck,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderAdvisoryResult(result) {
|
|
45
|
+
const check = result.vulnerabilityCheck;
|
|
46
|
+
const items = (check?.advisories ?? [])
|
|
47
|
+
.slice(0, 50)
|
|
48
|
+
.map(
|
|
49
|
+
(item) =>
|
|
50
|
+
`<li><code>${escape(item.id)}</code>${item.aliases?.length ? ` (${item.aliases.map(escape).join(", ")})` : ""}${item.summary ? `: ${escape(item.summary)}` : ""}</li>`,
|
|
51
|
+
)
|
|
52
|
+
.join("");
|
|
53
|
+
return `<section aria-label="Advisory-only result"><h2>Advisory-only result</h2><p class="status failed">${escape(result.assessment)}</p><p>${escape(result.notice)}</p>${items ? `<ul>${items}</ul>` : ""}<p class="meta">OSV lookup status: <code>${escape(check?.status)}</code>${check?.reasons?.length ? `; reasons: <code>${check.reasons.map(escape).join(", ")}</code>` : ""}. Artifact verification: <code>${escape(result.artifactVerification.status)}</code>.</p></section>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function withAdvisoryResult(statusHtml, job) {
|
|
57
|
+
if (!job.advisoryResult) return statusHtml;
|
|
58
|
+
return statusHtml.replace(
|
|
59
|
+
"</div></main>",
|
|
60
|
+
() => renderAdvisoryResult(job.advisoryResult) + "</div></main>",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const ADVISORY_ENDPOINT_LIMITS = Object.freeze({
|
|
65
|
+
maxLookups: 30,
|
|
66
|
+
windowMs: 600000,
|
|
67
|
+
maxActive: 2,
|
|
68
|
+
timeoutMs: 15000,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export function createAdvisoryService({
|
|
72
|
+
lookup,
|
|
73
|
+
clock,
|
|
74
|
+
limits = ADVISORY_ENDPOINT_LIMITS,
|
|
75
|
+
}) {
|
|
76
|
+
const inflight = new Map();
|
|
77
|
+
const shutdown = new AbortController();
|
|
78
|
+
let admissions = [];
|
|
79
|
+
function run(spec) {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const signal = AbortSignal.any([shutdown.signal, controller.signal]);
|
|
82
|
+
let timer;
|
|
83
|
+
const timeout = new Promise((resolve) => {
|
|
84
|
+
timer = clock.setTimeout(() => {
|
|
85
|
+
controller.abort();
|
|
86
|
+
resolve(unavailableVulnerabilityCheck(spec, clock, "timeout"));
|
|
87
|
+
}, limits.timeoutMs);
|
|
88
|
+
timer?.unref?.();
|
|
89
|
+
});
|
|
90
|
+
return Promise.race([
|
|
91
|
+
Promise.resolve()
|
|
92
|
+
.then(() => lookup(spec, { signal }))
|
|
93
|
+
.then((value) => boundedVulnerabilityCheck(value, spec, clock))
|
|
94
|
+
.catch(() =>
|
|
95
|
+
unavailableVulnerabilityCheck(spec, clock, "lookup-failed"),
|
|
96
|
+
),
|
|
97
|
+
timeout,
|
|
98
|
+
])
|
|
99
|
+
.then((check) =>
|
|
100
|
+
advisoryOnlyResult(check, { artifactReason: "not-requested" }),
|
|
101
|
+
)
|
|
102
|
+
.finally(() => {
|
|
103
|
+
clock.clearTimeout(timer);
|
|
104
|
+
inflight.delete(spec);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
request(spec) {
|
|
109
|
+
const existing = inflight.get(spec);
|
|
110
|
+
if (existing) return { promise: existing, reused: true };
|
|
111
|
+
const now = clock.now();
|
|
112
|
+
admissions = admissions.filter((time) => now - time < limits.windowMs);
|
|
113
|
+
if (
|
|
114
|
+
shutdown.signal.aborted ||
|
|
115
|
+
inflight.size >= limits.maxActive ||
|
|
116
|
+
admissions.length >= limits.maxLookups
|
|
117
|
+
)
|
|
118
|
+
return {
|
|
119
|
+
retryAfter: Math.max(
|
|
120
|
+
1,
|
|
121
|
+
Math.ceil(
|
|
122
|
+
(admissions.length >= limits.maxLookups
|
|
123
|
+
? admissions[0] + limits.windowMs - now
|
|
124
|
+
: limits.timeoutMs) / 1000,
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
admissions.push(now);
|
|
129
|
+
const promise = run(spec);
|
|
130
|
+
inflight.set(spec, promise);
|
|
131
|
+
return { promise, reused: false };
|
|
132
|
+
},
|
|
133
|
+
close() {
|
|
134
|
+
shutdown.abort();
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
package/dist/server/app.mjs
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { createRegistryFactsHandler } from "./registry-facts.mjs";
|
|
3
|
+
import { guardDecisionResponse } from "./guard-decision.mjs";
|
|
2
4
|
import { buildPackageDetails } from "../core/package-details.mjs";
|
|
3
5
|
import { attachVulnerabilityCheck } from "./enrichment.mjs";
|
|
6
|
+
import {
|
|
7
|
+
createAdvisoryService,
|
|
8
|
+
withAdvisoryResult,
|
|
9
|
+
} from "./advisory-result.mjs";
|
|
10
|
+
import { createDependencyScreening } from "./dependency-screening.mjs";
|
|
11
|
+
import { serveExport } from "./exports.mjs";
|
|
4
12
|
import { createQueue, resolveLimits, systemClock } from "./queue.mjs";
|
|
5
13
|
import { createReportStore } from "./store.mjs";
|
|
14
|
+
import { handleReportExploration } from "./report-exploration.mjs";
|
|
6
15
|
import { renderReport } from "../viewer/render.mjs";
|
|
7
16
|
import {
|
|
8
17
|
renderHome,
|
|
@@ -102,9 +111,11 @@ export function createCacheKeyPrefix(versionInfo, limits) {
|
|
|
102
111
|
export function createApp({
|
|
103
112
|
runAnalysis,
|
|
104
113
|
enrichJob,
|
|
114
|
+
advisoryLookup,
|
|
105
115
|
versionInfo,
|
|
106
116
|
limits: overrides,
|
|
107
117
|
clock = systemClock,
|
|
118
|
+
registryFacts = {},
|
|
108
119
|
} = {}) {
|
|
109
120
|
if (typeof runAnalysis !== "function")
|
|
110
121
|
throw new TypeError("runAnalysis is required");
|
|
@@ -119,6 +130,7 @@ export function createApp({
|
|
|
119
130
|
);
|
|
120
131
|
if (enrichJob !== undefined && typeof enrichJob !== "function")
|
|
121
132
|
throw new TypeError("enrichJob must be a function");
|
|
133
|
+
const registryFactsHandler = createRegistryFactsHandler(registryFacts);
|
|
122
134
|
const limits = resolveLimits(overrides);
|
|
123
135
|
const version = Object.fromEntries(
|
|
124
136
|
["version", "commit", "tarballSha256"].map((key) => [
|
|
@@ -133,6 +145,10 @@ export function createApp({
|
|
|
133
145
|
clock,
|
|
134
146
|
});
|
|
135
147
|
const keyPrefix = createCacheKeyPrefix(versionInfo, limits);
|
|
148
|
+
const screenDependencies = createDependencyScreening({
|
|
149
|
+
lookup: advisoryLookup,
|
|
150
|
+
clock,
|
|
151
|
+
});
|
|
136
152
|
const queue = createQueue({
|
|
137
153
|
runAnalysis,
|
|
138
154
|
enrichJob,
|
|
@@ -141,6 +157,8 @@ export function createApp({
|
|
|
141
157
|
clock,
|
|
142
158
|
keyPrefix,
|
|
143
159
|
});
|
|
160
|
+
const advisories =
|
|
161
|
+
enrichJob && createAdvisoryService({ lookup: enrichJob, clock });
|
|
144
162
|
const server = createServer(
|
|
145
163
|
{ maxHeaderSize: 8192, headersTimeout: 10000, requestTimeout: 15000 },
|
|
146
164
|
(req, res) => {
|
|
@@ -172,8 +190,28 @@ export function createApp({
|
|
|
172
190
|
function error(status, title, message, headers) {
|
|
173
191
|
send(status, renderError({ title, message }), undefined, headers);
|
|
174
192
|
}
|
|
193
|
+
async function advisoryEndpoint(params) {
|
|
194
|
+
const spec =
|
|
195
|
+
params.size === 1 && parseNameAtExactVersion(params.get("package"));
|
|
196
|
+
if (!spec) return json(400, { error: "invalid-package-spec" });
|
|
197
|
+
if (!advisories)
|
|
198
|
+
return json(503, { error: "advisory-lookup-disabled" });
|
|
199
|
+
const result = advisories.request(spec);
|
|
200
|
+
if (!result.promise)
|
|
201
|
+
return json(
|
|
202
|
+
429,
|
|
203
|
+
{ error: "rate-limited", retryAfter: result.retryAfter },
|
|
204
|
+
{ "retry-after": String(result.retryAfter) },
|
|
205
|
+
);
|
|
206
|
+
return json(200, await result.promise, {
|
|
207
|
+
"x-unmask-reused": String(result.reused),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
175
210
|
async function handle() {
|
|
176
211
|
const path = req.url;
|
|
212
|
+
if (await registryFactsHandler(req, json)) return;
|
|
213
|
+
if (path === "/api/dependency-screening")
|
|
214
|
+
return screenDependencies(req, res, json);
|
|
177
215
|
if (path === "/check") {
|
|
178
216
|
if (req.method !== "POST")
|
|
179
217
|
return error(
|
|
@@ -250,6 +288,25 @@ export function createApp({
|
|
|
250
288
|
"x-unmask-reused": String(result.reused),
|
|
251
289
|
});
|
|
252
290
|
}
|
|
291
|
+
const decisionMatch =
|
|
292
|
+
/^\/r\/([a-f0-9]{32})\.decision\.json(\?[^#]*)?$/.exec(path);
|
|
293
|
+
if (decisionMatch) {
|
|
294
|
+
if (req.method !== "GET")
|
|
295
|
+
return json(405, { error: "method-unavailable" }, { allow: "GET" });
|
|
296
|
+
const decisionJob = queue.get(decisionMatch[1]);
|
|
297
|
+
// A job without a report keeps its exact-version advisory lookup inside advisoryResult.
|
|
298
|
+
const result = guardDecisionResponse(
|
|
299
|
+
decisionJob?.advisoryResult && !decisionJob.vulnerabilityCheck
|
|
300
|
+
? {
|
|
301
|
+
...decisionJob,
|
|
302
|
+
vulnerabilityCheck:
|
|
303
|
+
decisionJob.advisoryResult.vulnerabilityCheck,
|
|
304
|
+
}
|
|
305
|
+
: decisionJob,
|
|
306
|
+
{ search: decisionMatch[2] ?? "" },
|
|
307
|
+
);
|
|
308
|
+
return json(result.status, result.body, result.headers);
|
|
309
|
+
}
|
|
253
310
|
if (req.method !== "GET")
|
|
254
311
|
return error(
|
|
255
312
|
405,
|
|
@@ -257,9 +314,13 @@ export function createApp({
|
|
|
257
314
|
"Use the release form to start a check.",
|
|
258
315
|
{ allow: "GET" },
|
|
259
316
|
);
|
|
317
|
+
if (handleReportExploration({ path, getJob: queue.get, send })) return;
|
|
260
318
|
if (path === "/") return send(200, renderHome());
|
|
261
319
|
if (path === "/healthz") return json(200, { status: "ok" });
|
|
262
320
|
if (path === "/version") return json(200, version);
|
|
321
|
+
if (path.startsWith("/api/advisory?"))
|
|
322
|
+
return advisoryEndpoint(new URLSearchParams(path.slice(14)));
|
|
323
|
+
if (serveExport({ path, queue, versionInfo: version, send })) return;
|
|
263
324
|
const match = /^\/r\/([a-f0-9]{32})(\.details\.json|\.json)?$/.exec(
|
|
264
325
|
path,
|
|
265
326
|
);
|
|
@@ -305,7 +366,7 @@ export function createApp({
|
|
|
305
366
|
return send(200, renderCompleted(renderReport(job.report), job));
|
|
306
367
|
return send(
|
|
307
368
|
200,
|
|
308
|
-
renderStatus(job),
|
|
369
|
+
withAdvisoryResult(renderStatus(job), job),
|
|
309
370
|
undefined,
|
|
310
371
|
pending ? { "retry-after": "3" } : {},
|
|
311
372
|
);
|
|
@@ -319,6 +380,9 @@ export function createApp({
|
|
|
319
380
|
);
|
|
320
381
|
},
|
|
321
382
|
);
|
|
322
|
-
server.on("close", () =>
|
|
383
|
+
server.on("close", () => {
|
|
384
|
+
queue.close();
|
|
385
|
+
advisories?.close();
|
|
386
|
+
});
|
|
323
387
|
return server;
|
|
324
388
|
}
|