@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,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,7 +1,14 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { createRegistryFactsHandler } from "./registry-facts.mjs";
|
|
3
|
+
import { guardDecisionResponse } from "./guard-decision.mjs";
|
|
3
4
|
import { buildPackageDetails } from "../core/package-details.mjs";
|
|
4
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";
|
|
5
12
|
import { createQueue, resolveLimits, systemClock } from "./queue.mjs";
|
|
6
13
|
import { createReportStore } from "./store.mjs";
|
|
7
14
|
import { handleReportExploration } from "./report-exploration.mjs";
|
|
@@ -104,6 +111,7 @@ export function createCacheKeyPrefix(versionInfo, limits) {
|
|
|
104
111
|
export function createApp({
|
|
105
112
|
runAnalysis,
|
|
106
113
|
enrichJob,
|
|
114
|
+
advisoryLookup,
|
|
107
115
|
versionInfo,
|
|
108
116
|
limits: overrides,
|
|
109
117
|
clock = systemClock,
|
|
@@ -137,6 +145,10 @@ export function createApp({
|
|
|
137
145
|
clock,
|
|
138
146
|
});
|
|
139
147
|
const keyPrefix = createCacheKeyPrefix(versionInfo, limits);
|
|
148
|
+
const screenDependencies = createDependencyScreening({
|
|
149
|
+
lookup: advisoryLookup,
|
|
150
|
+
clock,
|
|
151
|
+
});
|
|
140
152
|
const queue = createQueue({
|
|
141
153
|
runAnalysis,
|
|
142
154
|
enrichJob,
|
|
@@ -145,6 +157,8 @@ export function createApp({
|
|
|
145
157
|
clock,
|
|
146
158
|
keyPrefix,
|
|
147
159
|
});
|
|
160
|
+
const advisories =
|
|
161
|
+
enrichJob && createAdvisoryService({ lookup: enrichJob, clock });
|
|
148
162
|
const server = createServer(
|
|
149
163
|
{ maxHeaderSize: 8192, headersTimeout: 10000, requestTimeout: 15000 },
|
|
150
164
|
(req, res) => {
|
|
@@ -176,9 +190,28 @@ export function createApp({
|
|
|
176
190
|
function error(status, title, message, headers) {
|
|
177
191
|
send(status, renderError({ title, message }), undefined, headers);
|
|
178
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
|
+
}
|
|
179
210
|
async function handle() {
|
|
180
211
|
const path = req.url;
|
|
181
212
|
if (await registryFactsHandler(req, json)) return;
|
|
213
|
+
if (path === "/api/dependency-screening")
|
|
214
|
+
return screenDependencies(req, res, json);
|
|
182
215
|
if (path === "/check") {
|
|
183
216
|
if (req.method !== "POST")
|
|
184
217
|
return error(
|
|
@@ -255,6 +288,25 @@ export function createApp({
|
|
|
255
288
|
"x-unmask-reused": String(result.reused),
|
|
256
289
|
});
|
|
257
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
|
+
}
|
|
258
310
|
if (req.method !== "GET")
|
|
259
311
|
return error(
|
|
260
312
|
405,
|
|
@@ -266,6 +318,9 @@ export function createApp({
|
|
|
266
318
|
if (path === "/") return send(200, renderHome());
|
|
267
319
|
if (path === "/healthz") return json(200, { status: "ok" });
|
|
268
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;
|
|
269
324
|
const match = /^\/r\/([a-f0-9]{32})(\.details\.json|\.json)?$/.exec(
|
|
270
325
|
path,
|
|
271
326
|
);
|
|
@@ -311,7 +366,7 @@ export function createApp({
|
|
|
311
366
|
return send(200, renderCompleted(renderReport(job.report), job));
|
|
312
367
|
return send(
|
|
313
368
|
200,
|
|
314
|
-
renderStatus(job),
|
|
369
|
+
withAdvisoryResult(renderStatus(job), job),
|
|
315
370
|
undefined,
|
|
316
371
|
pending ? { "retry-after": "3" } : {},
|
|
317
372
|
);
|
|
@@ -325,6 +380,9 @@ export function createApp({
|
|
|
325
380
|
);
|
|
326
381
|
},
|
|
327
382
|
);
|
|
328
|
-
server.on("close", () =>
|
|
383
|
+
server.on("close", () => {
|
|
384
|
+
queue.close();
|
|
385
|
+
advisories?.close();
|
|
386
|
+
});
|
|
329
387
|
return server;
|
|
330
388
|
}
|