@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.
Files changed (50) hide show
  1. package/README.md +86 -4
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/cache.mjs +488 -0
  5. package/dist/cli/compare.mjs +261 -0
  6. package/dist/cli/config.mjs +141 -0
  7. package/dist/cli/discovery.mjs +372 -0
  8. package/dist/cli/doctor.mjs +354 -0
  9. package/dist/cli/exceptions.mjs +170 -0
  10. package/dist/cli/file.mjs +271 -0
  11. package/dist/cli/files.mjs +141 -0
  12. package/dist/cli/history.mjs +232 -0
  13. package/dist/cli/hosted.mjs +251 -43
  14. package/dist/cli/info.mjs +125 -0
  15. package/dist/cli/init.mjs +57 -0
  16. package/dist/cli/install-guard.mjs +896 -0
  17. package/dist/cli/lockfile.mjs +330 -0
  18. package/dist/cli/main.mjs +97 -14
  19. package/dist/cli/preflight.mjs +388 -0
  20. package/dist/cli/sarif.mjs +100 -0
  21. package/dist/cli/shell-integration.mjs +122 -0
  22. package/dist/cli/terminal.mjs +396 -0
  23. package/dist/cli/watch.mjs +233 -0
  24. package/dist/cli/why.mjs +256 -0
  25. package/dist/core/advisories.mjs +101 -24
  26. package/dist/core/advisory-cache.mjs +295 -0
  27. package/dist/core/behavior-signals.mjs +1088 -0
  28. package/dist/core/dependency-graph.mjs +332 -0
  29. package/dist/core/evidence-exports.mjs +469 -0
  30. package/dist/core/guard-config.mjs +463 -0
  31. package/dist/core/guard-policy.mjs +308 -0
  32. package/dist/core/package-details.mjs +42 -6
  33. package/dist/core/report.mjs +7 -0
  34. package/dist/core/sarif.mjs +556 -0
  35. package/dist/core/syntax.mjs +296 -15
  36. package/dist/model/client.mjs +204 -56
  37. package/dist/model/diff-evidence.mjs +177 -0
  38. package/dist/model/summary.mjs +183 -0
  39. package/dist/server/advisory-result.mjs +137 -0
  40. package/dist/server/app.mjs +60 -2
  41. package/dist/server/dependency-screening.mjs +386 -0
  42. package/dist/server/exports.mjs +63 -0
  43. package/dist/server/guard-decision.mjs +37 -0
  44. package/dist/server/main.mjs +25 -7
  45. package/dist/server/queue.mjs +95 -53
  46. package/dist/server/store.mjs +21 -1
  47. package/dist/version.json +1 -1
  48. package/dist/viewer/render.mjs +10 -2
  49. package/dist/viewer/shell.mjs +1 -1
  50. 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
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
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
+ }
@@ -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", () => queue.close());
383
+ server.on("close", () => {
384
+ queue.close();
385
+ advisories?.close();
386
+ });
329
387
  return server;
330
388
  }