@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,332 @@
1
+ import { parsePackageSpec } from "./acquire.mjs";
2
+
3
+ export const GRAPH_LIMITS = Object.freeze({
4
+ entries: 20000,
5
+ locations: 20,
6
+ edges: 200000,
7
+ });
8
+ export const PATH_LIMITS = Object.freeze({
9
+ paths: 20,
10
+ depth: 32,
11
+ visits: 20000,
12
+ });
13
+ const REGISTRY = "https://registry.npmjs.org/";
14
+
15
+ export class LockfileError extends Error {
16
+ constructor(reason) {
17
+ super(reason);
18
+ this.reason = reason;
19
+ }
20
+ }
21
+ const object = (value) =>
22
+ value !== null && typeof value === "object" && !Array.isArray(value);
23
+
24
+ function nameFromKey(key) {
25
+ const segments = key.split("/");
26
+ const start = segments.indexOf("node_modules");
27
+ if (
28
+ start < 0 ||
29
+ segments.slice(0, start).some((part) => ["", ".", ".."].includes(part))
30
+ )
31
+ return null;
32
+ const parts = [];
33
+ for (let i = start; i < segments.length;) {
34
+ if (segments[i] !== "node_modules") return null;
35
+ const scoped = segments[i + 1]?.startsWith("@");
36
+ const name = scoped
37
+ ? `${segments[i + 1]}/${segments[i + 2]}`
38
+ : segments[i + 1];
39
+ if (!name || (scoped && !segments[i + 2])) return null;
40
+ parts.push(name);
41
+ i += scoped ? 3 : 2;
42
+ }
43
+ return parts.length ? parts.at(-1) : null;
44
+ }
45
+
46
+ function exact(name, version) {
47
+ try {
48
+ return parsePackageSpec(`${name}@${version}`);
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function classify(key, entry) {
55
+ if (!object(entry)) return { reason: "invalid-entry" };
56
+ const name = nameFromKey(key);
57
+ // Only a key outside node_modules with no artifact claim can be local workspace code.
58
+ if (name === null)
59
+ return !key.split("/").includes("node_modules") &&
60
+ typeof entry.version === "string" &&
61
+ entry.resolved === undefined &&
62
+ entry.integrity === undefined &&
63
+ entry.inBundle !== true
64
+ ? { local: true, reason: "workspace-package" }
65
+ : { reason: "invalid-location" };
66
+ if (entry.link === true) return { name, local: true, reason: "linked-local" };
67
+ if (entry.inBundle === true) return { name, reason: "bundled-dependency" };
68
+ // An npm alias is screened as the real package only when its resolved tarball proves that exact identity.
69
+ const aliased = typeof entry.name === "string" && entry.name !== name;
70
+ const unbound = (reason) => ({
71
+ name,
72
+ reason: aliased ? "alias-identity" : reason,
73
+ });
74
+ if (typeof entry.version !== "string") return unbound("no-version");
75
+ const claim = exact(aliased ? entry.name : name, entry.version);
76
+ if (!claim) return unbound("non-exact-version");
77
+ if (typeof entry.resolved !== "string") return unbound("missing-resolved");
78
+ if (!entry.resolved.startsWith(REGISTRY))
79
+ return unbound("non-registry-resolved");
80
+ const base = claim.name.split("/").at(-1);
81
+ if (
82
+ entry.resolved !== `${REGISTRY}${claim.name}/-/${base}-${claim.version}.tgz`
83
+ )
84
+ return unbound("resolved-identity-mismatch");
85
+ return { name, claim, ...(aliased ? { alias: name } : {}) };
86
+ }
87
+
88
+ // Parses npm lockfile v2/v3 package entries into exact registry identities without trusting non-registry claims.
89
+ export function parseNpmLockfile(input, { limits: overrides = {} } = {}) {
90
+ const limits = { ...GRAPH_LIMITS, ...overrides };
91
+ let lock = input;
92
+ if (typeof input === "string") {
93
+ try {
94
+ lock = JSON.parse(input);
95
+ } catch {
96
+ throw new LockfileError("invalid-json");
97
+ }
98
+ }
99
+ if (!object(lock)) throw new LockfileError("invalid-lockfile");
100
+ if (![2, 3].includes(lock.lockfileVersion))
101
+ throw new LockfileError("unsupported-lockfile-version");
102
+ if (!object(lock.packages)) throw new LockfileError("missing-packages");
103
+ const keys = Object.keys(lock.packages);
104
+ if (keys.length > limits.entries) throw new LockfileError("entry-limit");
105
+ const root = lock.packages[""];
106
+ const identities = new Map();
107
+ const integrities = new Map();
108
+ const bySpec = new Map();
109
+ const rejected = [];
110
+ const local = [];
111
+ for (const key of keys) {
112
+ if (key === "") continue;
113
+ const entry = lock.packages[key];
114
+ const result = classify(key, entry);
115
+ const flags = {
116
+ dev: entry?.dev === true,
117
+ optional: entry?.optional === true || entry?.devOptional === true,
118
+ };
119
+ if (result.local) {
120
+ local.push({ location: key, reason: result.reason });
121
+ continue;
122
+ }
123
+ if (!result.claim) {
124
+ rejected.push({
125
+ location: key,
126
+ name: result.name ?? null,
127
+ reason: result.reason,
128
+ ...flags,
129
+ });
130
+ continue;
131
+ }
132
+ const spec = `${result.claim.name}@${result.claim.version}`;
133
+ const identity = identities.get(spec) ?? {
134
+ spec,
135
+ name: result.claim.name,
136
+ version: result.claim.version,
137
+ locations: [],
138
+ locationCount: 0,
139
+ dev: true,
140
+ optional: true,
141
+ };
142
+ if (result.alias) {
143
+ identity.installedAs ??= [];
144
+ if (
145
+ identity.installedAs.length < limits.locations &&
146
+ !identity.installedAs.includes(result.alias)
147
+ )
148
+ identity.installedAs.push(result.alias);
149
+ }
150
+ identity.locationCount++;
151
+ bySpec.set(spec, [...(bySpec.get(spec) ?? []), key]);
152
+ if (identity.locations.length < limits.locations)
153
+ identity.locations.push(key);
154
+ const integrity =
155
+ typeof entry.integrity === "string" &&
156
+ /^sha(?:256|384|512)-[A-Za-z0-9+/]{1,200}={0,2}$/.test(entry.integrity)
157
+ ? entry.integrity
158
+ : "";
159
+ integrities.set(spec, (integrities.get(spec) ?? new Set()).add(integrity));
160
+ identity.dev &&= flags.dev;
161
+ identity.optional &&= flags.optional;
162
+ identities.set(spec, identity);
163
+ }
164
+ return {
165
+ lockfileVersion: lock.lockfileVersion,
166
+ root: {
167
+ name: typeof root?.name === "string" ? root.name : null,
168
+ version: typeof root?.version === "string" ? root.version : null,
169
+ },
170
+ entries: keys.length - (Object.hasOwn(lock.packages, "") ? 1 : 0),
171
+ identities: [...identities.values()]
172
+ .map((identity) => {
173
+ const seen = [...integrities.get(identity.spec)];
174
+ const integrityStatus =
175
+ seen.length > 1 ? "conflicting" : seen[0] ? "consistent" : "missing";
176
+ return {
177
+ ...identity,
178
+ integrity: integrityStatus === "consistent" ? seen[0] : null,
179
+ integrityStatus,
180
+ };
181
+ })
182
+ .sort((a, b) => (a.spec < b.spec ? -1 : a.spec > b.spec ? 1 : 0)),
183
+ rejected,
184
+ local,
185
+ index: indexEdges(lock.packages, keys, bySpec, limits),
186
+ };
187
+ }
188
+
189
+ function parentLocation(location) {
190
+ const at = location.lastIndexOf("/node_modules/");
191
+ return at >= 0 ? location.slice(0, at) : "";
192
+ }
193
+
194
+ // Resolves each declared dependency with node_modules lookup from the dependent location upward, following workspace links.
195
+ function indexEdges(packages, keys, bySpec, limits) {
196
+ const reverse = new Map();
197
+ let edges = 0,
198
+ unresolved = 0,
199
+ truncated = false;
200
+ for (const from of keys) {
201
+ const entry = packages[from];
202
+ if (!object(entry)) continue;
203
+ const declared = [
204
+ entry.dependencies,
205
+ entry.optionalDependencies,
206
+ entry.peerDependencies,
207
+ from.startsWith("node_modules/") ? undefined : entry.devDependencies,
208
+ ];
209
+ const names = new Set(declared.filter(object).flatMap(Object.keys));
210
+ for (const name of names) {
211
+ if (++edges > limits.edges) {
212
+ truncated = true;
213
+ break;
214
+ }
215
+ let target = null;
216
+ for (let at = from; target === null; at = parentLocation(at)) {
217
+ const candidate = `${at ? `${at}/` : ""}node_modules/${name}`;
218
+ if (Object.hasOwn(packages, candidate)) target = candidate;
219
+ else if (at === "") break;
220
+ }
221
+ if (target === null) {
222
+ unresolved++;
223
+ continue;
224
+ }
225
+ const linked = packages[target];
226
+ if (
227
+ linked?.link === true &&
228
+ typeof linked.resolved === "string" &&
229
+ Object.hasOwn(packages, linked.resolved)
230
+ )
231
+ target = linked.resolved;
232
+ if (!reverse.has(target)) reverse.set(target, new Set());
233
+ reverse.get(target).add(from);
234
+ }
235
+ if (truncated) break;
236
+ }
237
+ return { reverse, bySpec, unresolved, truncated, packages };
238
+ }
239
+
240
+ function describe(packages, location) {
241
+ const entry = packages[location];
242
+ return {
243
+ location,
244
+ name:
245
+ typeof entry?.name === "string"
246
+ ? entry.name
247
+ : location === ""
248
+ ? null
249
+ : nameFromKey(location),
250
+ ...(location &&
251
+ typeof entry?.name === "string" &&
252
+ nameFromKey(location) !== null &&
253
+ nameFromKey(location) !== entry.name
254
+ ? { installedAs: nameFromKey(location) }
255
+ : {}),
256
+ version: typeof entry?.version === "string" ? entry.version : null,
257
+ };
258
+ }
259
+
260
+ // Enumerates bounded simple paths from the project root or a workspace to each target location.
261
+ function trace(graph, targets, overrides) {
262
+ const limits = { ...PATH_LIMITS, ...overrides };
263
+ const { reverse, packages } = graph.index;
264
+ const paths = [];
265
+ const reasons = new Set();
266
+ if (graph.index.truncated) reasons.add("edge-limit");
267
+ let visits = 0;
268
+ for (const target of targets) {
269
+ const stack = [[target]];
270
+ while (stack.length) {
271
+ if (paths.length >= limits.paths) {
272
+ reasons.add("path-limit");
273
+ break;
274
+ }
275
+ if (++visits > limits.visits) {
276
+ reasons.add("visit-limit");
277
+ break;
278
+ }
279
+ const chain = stack.pop();
280
+ const head = chain.at(-1);
281
+ const parents = reverse.get(head);
282
+ if (head === "" || (head !== target && nameFromKey(head) === null)) {
283
+ paths.push(chain.toReversed().map((at) => describe(packages, at)));
284
+ continue;
285
+ }
286
+ if (!parents?.size) {
287
+ reasons.add("unreachable-location");
288
+ continue;
289
+ }
290
+ if (chain.length > limits.depth) {
291
+ reasons.add("depth-limit");
292
+ continue;
293
+ }
294
+ const next = [...parents].filter((parent) => !chain.includes(parent));
295
+ if (!next.length) reasons.add("cycle-without-root");
296
+ for (const parent of next.sort().reverse())
297
+ stack.push([...chain, parent]);
298
+ }
299
+ }
300
+ return {
301
+ direct: paths.some((path) => path.length === 2 && path[0].location === ""),
302
+ paths,
303
+ complete: reasons.size === 0,
304
+ reasons: [...reasons].sort(),
305
+ };
306
+ }
307
+
308
+ export function explainDependencyPaths(
309
+ graph,
310
+ spec,
311
+ { limits: overrides = {} } = {},
312
+ ) {
313
+ const targets = graph.index.bySpec.get(spec) ?? [];
314
+ return {
315
+ spec,
316
+ locations: targets.length,
317
+ ...trace(graph, targets, overrides),
318
+ };
319
+ }
320
+
321
+ export function explainLockfileLocation(
322
+ graph,
323
+ location,
324
+ { limits: overrides = {} } = {},
325
+ ) {
326
+ const present = Object.hasOwn(graph.index.packages, location);
327
+ return {
328
+ location,
329
+ locations: present ? 1 : 0,
330
+ ...trace(graph, present ? [location] : [], overrides),
331
+ };
332
+ }