@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.
Files changed (56) hide show
  1. package/README.md +301 -85
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/batch.mjs +218 -0
  5. package/dist/cli/cache.mjs +488 -0
  6. package/dist/cli/compare.mjs +261 -0
  7. package/dist/cli/config.mjs +141 -0
  8. package/dist/cli/discovery.mjs +372 -0
  9. package/dist/cli/doctor.mjs +354 -0
  10. package/dist/cli/exceptions.mjs +170 -0
  11. package/dist/cli/file.mjs +271 -0
  12. package/dist/cli/files.mjs +141 -0
  13. package/dist/cli/history.mjs +232 -0
  14. package/dist/cli/hosted.mjs +251 -43
  15. package/dist/cli/info.mjs +125 -0
  16. package/dist/cli/init.mjs +57 -0
  17. package/dist/cli/install-guard.mjs +896 -0
  18. package/dist/cli/lockfile.mjs +330 -0
  19. package/dist/cli/main.mjs +101 -14
  20. package/dist/cli/preflight.mjs +388 -0
  21. package/dist/cli/sarif.mjs +100 -0
  22. package/dist/cli/scan.mjs +128 -0
  23. package/dist/cli/shell-integration.mjs +122 -0
  24. package/dist/cli/terminal.mjs +396 -0
  25. package/dist/cli/watch.mjs +233 -0
  26. package/dist/cli/why.mjs +256 -0
  27. package/dist/core/advisories.mjs +101 -24
  28. package/dist/core/advisory-cache.mjs +295 -0
  29. package/dist/core/behavior-signals.mjs +1088 -0
  30. package/dist/core/dependency-graph.mjs +332 -0
  31. package/dist/core/evidence-exports.mjs +469 -0
  32. package/dist/core/guard-config.mjs +463 -0
  33. package/dist/core/guard-policy.mjs +308 -0
  34. package/dist/core/package-details.mjs +42 -6
  35. package/dist/core/registry-facts.mjs +342 -0
  36. package/dist/core/report-exploration.mjs +328 -0
  37. package/dist/core/report.mjs +7 -0
  38. package/dist/core/sarif.mjs +556 -0
  39. package/dist/core/syntax.mjs +296 -15
  40. package/dist/model/client.mjs +204 -56
  41. package/dist/model/diff-evidence.mjs +177 -0
  42. package/dist/model/summary.mjs +183 -0
  43. package/dist/server/advisory-result.mjs +137 -0
  44. package/dist/server/app.mjs +66 -2
  45. package/dist/server/dependency-screening.mjs +386 -0
  46. package/dist/server/exports.mjs +63 -0
  47. package/dist/server/guard-decision.mjs +37 -0
  48. package/dist/server/main.mjs +25 -7
  49. package/dist/server/queue.mjs +95 -53
  50. package/dist/server/registry-facts.mjs +118 -0
  51. package/dist/server/report-exploration.mjs +87 -0
  52. package/dist/server/store.mjs +21 -1
  53. package/dist/version.json +1 -1
  54. package/dist/viewer/render.mjs +10 -2
  55. package/dist/viewer/shell.mjs +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,308 @@
1
+ import { parsePackageSpec } from "./acquire.mjs";
2
+ import { validateReport } from "./report.mjs";
3
+
4
+ export const GUARD_POLICY_VERSION = "guard-2";
5
+ export const GUARD_PROFILES = Object.freeze({
6
+ strict: Object.freeze({
7
+ unknownDecision: "block",
8
+ blockSeverity: "medium",
9
+ enforcement: "enforce",
10
+ }),
11
+ balanced: Object.freeze({
12
+ unknownDecision: "unknown",
13
+ blockSeverity: "high",
14
+ enforcement: "enforce",
15
+ }),
16
+ audit: Object.freeze({
17
+ unknownDecision: "unknown",
18
+ blockSeverity: "critical",
19
+ enforcement: "audit",
20
+ }),
21
+ });
22
+ export function getGuardProfile(profile = "strict") {
23
+ if (typeof profile !== "string" || !Object.hasOwn(GUARD_PROFILES, profile))
24
+ throw new TypeError("Unknown guard profile; use strict, balanced or audit");
25
+ return GUARD_PROFILES[profile];
26
+ }
27
+ const severityOrder = ["unknown", "low", "medium", "high", "critical"];
28
+ function advisorySeverity(record, identity) {
29
+ const values = [record.databaseSeverity];
30
+ if (Array.isArray(record.affected)) {
31
+ for (const affected of record.affected.slice(0, 8)) {
32
+ if (
33
+ affected?.package?.ecosystem === "npm" &&
34
+ affected.package.name === identity.name
35
+ )
36
+ values.push(affected.ecosystemSeverity);
37
+ }
38
+ }
39
+ let rank = 0;
40
+ for (const value of values) {
41
+ if (typeof value !== "string" || value.length > 64) continue;
42
+ const normalized =
43
+ value.toLowerCase() === "moderate" ? "medium" : value.toLowerCase();
44
+ rank = Math.max(rank, severityOrder.indexOf(normalized));
45
+ }
46
+ return severityOrder[rank];
47
+ }
48
+ const identifier = (value) =>
49
+ typeof value === "string" && /^[a-zA-Z0-9:_.-]{1,256}$/.test(value);
50
+ const samePackage = (value, expected) =>
51
+ value?.name === expected.name && value?.version === expected.version;
52
+ const historicalMalware = new Map([
53
+ [
54
+ "GHSA-pjwm-rvh2-c87w",
55
+ { name: "ua-parser-js", versions: ["0.7.29", "0.8.0", "1.0.0"] },
56
+ ],
57
+ ["GHSA-mh6f-8j2x-4483", { name: "event-stream", versions: ["3.3.6"] }],
58
+ ["GHSA-97m3-w2cp-4xx6", { name: "node-ipc", versions: ["10.1.1", "10.1.2"] }],
59
+ ]);
60
+ function malwareId(value, identity) {
61
+ if (/^MAL-\d{4}-\d+$/i.test(value)) return true;
62
+ const incident = historicalMalware.get(value);
63
+ return (
64
+ incident?.name === identity.name &&
65
+ incident.versions.includes(identity.version)
66
+ );
67
+ }
68
+
69
+ function validAdvisoryRecord(record) {
70
+ return (
71
+ identifier(record?.id) &&
72
+ Array.isArray(record.aliases) &&
73
+ record.aliases.length <= 16 &&
74
+ record.aliases.every(identifier) &&
75
+ (record.withdrawn === undefined ||
76
+ (typeof record.withdrawn === "string" &&
77
+ Number.isFinite(Date.parse(record.withdrawn))))
78
+ );
79
+ }
80
+
81
+ export function classifyGuardAdvisory(record, packageIdentity) {
82
+ let identity;
83
+ try {
84
+ if (packageIdentity?.ecosystem !== "npm")
85
+ throw new TypeError("Invalid ecosystem");
86
+ identity = parsePackageSpec(
87
+ `${packageIdentity.name}@${packageIdentity.version}`,
88
+ );
89
+ } catch {
90
+ throw new TypeError(
91
+ "Advisory classification requires an exact npm package identity",
92
+ );
93
+ }
94
+ if (!validAdvisoryRecord(record))
95
+ return {
96
+ id: identifier(record?.id) ? record.id : null,
97
+ status: "invalid",
98
+ knownMalware: false,
99
+ severity: "unknown",
100
+ };
101
+ const active = record.withdrawn === undefined;
102
+ return {
103
+ id: record.id,
104
+ status: active ? "active" : "withdrawn",
105
+ knownMalware:
106
+ active &&
107
+ [record.id, ...record.aliases].some((id) => malwareId(id, identity)),
108
+ severity: active ? advisorySeverity(record, identity) : "unknown",
109
+ };
110
+ }
111
+
112
+ export function evaluateGuardDecision(
113
+ { nameAtExactVersion, report, vulnerabilityCheck } = {},
114
+ { profile = "strict" } = {},
115
+ ) {
116
+ const policy = getGuardProfile(profile);
117
+ let identity;
118
+ try {
119
+ identity = parsePackageSpec(nameAtExactVersion);
120
+ } catch {
121
+ throw new TypeError("Guard requires an exact npm package identity");
122
+ }
123
+ const reasons = [];
124
+ let validReport = false;
125
+ if (report) {
126
+ try {
127
+ validateReport(report);
128
+ validReport = samePackage(report.inputs.claim, identity);
129
+ if (!validReport) reasons.push({ code: "report-identity-mismatch" });
130
+ } catch {
131
+ reasons.push({ code: "invalid-report" });
132
+ }
133
+ } else reasons.push({ code: "report-unavailable" });
134
+ const artifactComplete =
135
+ validReport &&
136
+ report.coverage.inventory.complete === true &&
137
+ report.coverage.complete === true &&
138
+ report.failures.length === 0;
139
+ const sourceComplete =
140
+ validReport &&
141
+ report.coverage.sourceAvailable === true &&
142
+ report.coverage.sourceComplete === true &&
143
+ report.source.complete === true &&
144
+ ["selected", "resolved"].includes(report.source.status) &&
145
+ !["unresolved", "conflicting"].includes(report.source.method);
146
+ if (!artifactComplete)
147
+ reasons.push({ code: "artifact-assessment-incomplete" });
148
+ if (!sourceComplete) reasons.push({ code: "source-assessment-incomplete" });
149
+ const check = vulnerabilityCheck;
150
+ const advisoryIdentity =
151
+ check?.source === "OSV" &&
152
+ check.schemaVersion === "1" &&
153
+ check.package?.ecosystem === "npm" &&
154
+ samePackage(check.package, identity);
155
+ const records =
156
+ advisoryIdentity &&
157
+ Array.isArray(check.advisories) &&
158
+ check.advisories.length <= 100
159
+ ? check.advisories
160
+ : [];
161
+ const validRecords = records.filter(validAdvisoryRecord);
162
+ const advisoriesComplete = Boolean(
163
+ advisoryIdentity &&
164
+ check.complete === true &&
165
+ typeof check.queriedAt === "string" &&
166
+ Number.isFinite(Date.parse(check.queriedAt)) &&
167
+ Number.isSafeInteger(check.pagesFetched) &&
168
+ check.pagesFetched > 0 &&
169
+ check.limits !== null &&
170
+ typeof check.limits === "object" &&
171
+ !Array.isArray(check.limits) &&
172
+ ["found", "none-reported"].includes(check.status) &&
173
+ Array.isArray(check.reasons) &&
174
+ check.reasons.length === 0 &&
175
+ Array.isArray(check.advisories) &&
176
+ records.length === check.advisories.length &&
177
+ validRecords.length === records.length &&
178
+ (check.status === "found" ? records.length > 0 : records.length === 0),
179
+ );
180
+ if (!advisoriesComplete)
181
+ reasons.push({ code: "advisory-assessment-incomplete" });
182
+ const active = validRecords.filter(
183
+ (record) => record.withdrawn === undefined,
184
+ );
185
+ const knownMalware = active
186
+ .filter(
187
+ (record) =>
188
+ classifyGuardAdvisory(record, { ecosystem: "npm", ...identity })
189
+ .knownMalware,
190
+ )
191
+ .map((record) => ({ id: record.id, aliases: [...record.aliases] }));
192
+ for (const record of knownMalware)
193
+ reasons.push({
194
+ code: "known-malware",
195
+ advisoryId: record.id,
196
+ aliases: record.aliases,
197
+ });
198
+ const findings = validReport ? report.policy.findings : [];
199
+ for (const finding of findings.slice(0, 100))
200
+ reasons.push({
201
+ code: "deterministic-finding",
202
+ ruleId: finding.ruleId,
203
+ evidenceIds: finding.evidenceIds.slice(0, 16),
204
+ });
205
+ const ordinary = active.filter(
206
+ (record) => !knownMalware.some((item) => item.id === record.id),
207
+ );
208
+ const severities = ordinary.map((record) => ({
209
+ id: record.id,
210
+ severity: advisorySeverity(record, identity),
211
+ }));
212
+ for (const record of severities)
213
+ reasons.push({
214
+ code: "known-vulnerability",
215
+ advisoryId: record.id,
216
+ severity: record.severity,
217
+ severitySource: "OSV-declaration",
218
+ });
219
+ const advisorySeverityComplete = severities.every(
220
+ (record) => record.severity !== "unknown",
221
+ );
222
+ const unknownSeverityBlock =
223
+ profile === "strict" && !advisorySeverityComplete;
224
+ if (unknownSeverityBlock) reasons.push({ code: "advisory-severity-unknown" });
225
+ const severityBlock = severities.some(
226
+ (record) =>
227
+ severityOrder.indexOf(record.severity) >=
228
+ severityOrder.indexOf(policy.blockSeverity),
229
+ );
230
+ const model = validReport ? report.model : null;
231
+ const invalidModel =
232
+ model?.status === "complete" &&
233
+ (!Array.isArray(model.hypotheses) ||
234
+ model.hypotheses.some(
235
+ (item) =>
236
+ !item ||
237
+ !["possible-transformation", "review-concern", "unknown"].includes(
238
+ item.category,
239
+ ),
240
+ ));
241
+ const modelStatus = invalidModel
242
+ ? "invalid-output"
243
+ : (model?.status ?? "unavailable");
244
+ const aiReviewConcern =
245
+ validReport &&
246
+ modelStatus === "complete" &&
247
+ Array.isArray(report.model.hypotheses) &&
248
+ report.model.hypotheses.some(
249
+ (hypothesis) => hypothesis?.category === "review-concern",
250
+ );
251
+ if (aiReviewConcern)
252
+ reasons.push({ code: "ai-review-concern", binding: false });
253
+ const complete = artifactComplete && sourceComplete && advisoriesComplete;
254
+ const decision =
255
+ knownMalware.length ||
256
+ findings.length ||
257
+ severityBlock ||
258
+ unknownSeverityBlock
259
+ ? "block"
260
+ : !complete
261
+ ? policy.unknownDecision
262
+ : ordinary.length || aiReviewConcern
263
+ ? "warn"
264
+ : "allow";
265
+ if (!complete && policy.unknownDecision === "block")
266
+ reasons.push({ code: "profile-unknown-blocked" });
267
+ if (severityBlock)
268
+ reasons.push({
269
+ code: "profile-severity-blocked",
270
+ threshold: policy.blockSeverity,
271
+ });
272
+ if (policy.enforcement === "audit")
273
+ reasons.push({ code: "audit-no-enforcement" });
274
+ return {
275
+ decisionVersion: "1",
276
+ policyVersion: GUARD_POLICY_VERSION,
277
+ profile,
278
+ appliedPolicy: { ...policy },
279
+ package: { ecosystem: "npm", ...identity },
280
+ artifact: validReport
281
+ ? {
282
+ sha256: report.inputs.artifact.sha256,
283
+ sha512: report.inputs.artifact.sha512,
284
+ }
285
+ : null,
286
+ decision,
287
+ permitted:
288
+ policy.enforcement === "audit" || ["allow", "warn"].includes(decision),
289
+ enforcement: policy.enforcement,
290
+ reasons,
291
+ knownMalware,
292
+ policyExitCode: validReport ? report.policy.exitCode : null,
293
+ assessment: {
294
+ complete,
295
+ artifactComplete,
296
+ sourceComplete,
297
+ advisoriesComplete,
298
+ advisorySeverityComplete,
299
+ modelStatus,
300
+ modelComplete: modelStatus === "complete",
301
+ },
302
+ limitations: [
303
+ "Allow means this bounded static assessment found no policy blocker; it is not a safety guarantee.",
304
+ "Assessment completeness covers the recorded artifact, source comparison and exact-version OSV lookup; model availability is reported separately and AI signals cannot permit or block installation.",
305
+ "Package execution, transitive dependencies, freshness after this check and future registry changes are not assessed.",
306
+ ],
307
+ };
308
+ }
@@ -16,6 +16,34 @@ const sorted = (value) => Object.keys(value).sort(order);
16
16
  const type = (value) =>
17
17
  value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
18
18
 
19
+ function aiAssessment(model, take) {
20
+ const value = model.assessment;
21
+ if (
22
+ model.status !== "complete" ||
23
+ value?.source !== "model" ||
24
+ typeof model.summary !== "string"
25
+ )
26
+ return {
27
+ status: model.status,
28
+ source: "none",
29
+ reason: /^[a-z-]{1,64}$/u.test(model.reason ?? "") ? model.reason : null,
30
+ note: "No AI assessment exists for this check. This status is deterministic, not model output, and is not a safety signal.",
31
+ };
32
+ const { paths, ...checkedScope } = value.checkedScope ?? {};
33
+ return {
34
+ status: "complete",
35
+ source: "model",
36
+ summary: model.summary,
37
+ uncertainty: value.uncertainty,
38
+ nextAction: value.nextAction,
39
+ note: "Advisory model output. It is not a safety signal.",
40
+ checkedScope: {
41
+ ...checkedScope,
42
+ paths: take(Array.isArray(paths) ? paths : []),
43
+ },
44
+ };
45
+ }
46
+
19
47
  export function buildPackageDetails(report, options = {}) {
20
48
  validateReport(report);
21
49
  const limits = { ...PACKAGE_DETAILS_LIMITS, ...options };
@@ -170,7 +198,8 @@ export function buildPackageDetails(report, options = {}) {
170
198
  cueRef: `/annotations/${index}`,
171
199
  })),
172
200
  ].filter(
173
- (cue) => typeof cue.ruleId === "string" && cue.ruleId.startsWith("syntax-"),
201
+ (cue) =>
202
+ typeof cue.ruleId === "string" && /^(syntax|behavior)-/.test(cue.ruleId),
174
203
  );
175
204
  observations.sort(
176
205
  (a, b) => order(a.ruleId, b.ruleId) || order(a.cueRef, b.cueRef),
@@ -203,7 +232,9 @@ export function buildPackageDetails(report, options = {}) {
203
232
  }
204
233
  const evidenceById = new Map(report.evidence.map((item) => [item.id, item]));
205
234
  const cueRefsByPath = new Map();
206
- for (const cue of observations)
235
+ for (const cue of observations.filter((item) =>
236
+ item.ruleId.startsWith("syntax-"),
237
+ ))
207
238
  for (const id of cue.evidenceIds ?? []) {
208
239
  const evidence = evidenceById.get(id);
209
240
  if (evidence?.side !== "artifact") continue;
@@ -505,7 +536,8 @@ export function buildPackageDetails(report, options = {}) {
505
536
  },
506
537
  },
507
538
  capabilities: {
508
- label: "observed syntax references; execution and bindings unproven",
539
+ label:
540
+ "recorded static syntax and behavior references; execution and bindings unproven",
509
541
  referenceFormat:
510
542
  "cueRef is a JSON pointer into the raw report; existing cue IDs, when present, are preserved. evidenceIds refer to raw-report evidence IDs.",
511
543
  groups: take(
@@ -540,6 +572,7 @@ export function buildPackageDetails(report, options = {}) {
540
572
  },
541
573
  ai: {
542
574
  advisory: true,
575
+ assessment: aiAssessment(report.model, take),
543
576
  selection: {
544
577
  selectedGroups: Array.isArray(report.model.evidenceGroups)
545
578
  ? report.model.evidenceGroups.length
@@ -618,9 +651,12 @@ export function buildPackageDetails(report, options = {}) {
618
651
  annotationRef: `/annotations/${index}`,
619
652
  }))
620
653
  .filter((item) =>
621
- ["syntax-scope", "syntax-not-inspected", "manifest-scope"].includes(
622
- item.type,
623
- ),
654
+ [
655
+ "syntax-scope",
656
+ "syntax-not-inspected",
657
+ "manifest-scope",
658
+ "behavior-scope",
659
+ ].includes(item.type),
624
660
  ),
625
661
  ),
626
662
  failures: take(report.failures),