@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
@@ -1,6 +1,19 @@
1
1
  import { parse } from "acorn";
2
2
  import { createHash } from "node:crypto";
3
3
  import { validateReport } from "./report.mjs";
4
+ import {
5
+ BEHAVIOR_LIMITS,
6
+ javascriptSignal,
7
+ manifestScriptSpans,
8
+ destructiveSignals,
9
+ DESTRUCTIVE_LIMITS,
10
+ moduleAliases,
11
+ NETWORK_MODULES,
12
+ obfuscationSignals,
13
+ OBFUSCATION_LIMITS,
14
+ scriptSignals,
15
+ summaryFor,
16
+ } from "./behavior-signals.mjs";
4
17
 
5
18
  export const SYNTAX_LIMITS = Object.freeze({
6
19
  fileBytes: 2097152,
@@ -79,8 +92,26 @@ function nameOf(node) {
79
92
  }
80
93
  return "";
81
94
  }
82
- function capability(node) {
95
+ function aliasedName(callee, aliases) {
96
+ if (callee?.type === "Identifier") return aliases.get(callee.name) ?? "";
97
+ if (
98
+ callee?.type === "MemberExpression" &&
99
+ !callee.computed &&
100
+ callee.object.type === "Identifier" &&
101
+ aliases.has(callee.object.name)
102
+ )
103
+ return `${aliases.get(callee.object.name)}.${callee.property.name}`;
104
+ return "";
105
+ }
106
+ function capability(node, aliases) {
83
107
  const name = nameOf(node.callee);
108
+ const aliased = aliases?.size ? aliasedName(node.callee, aliases) : "";
109
+ if (
110
+ node.type === "CallExpression" &&
111
+ aliased &&
112
+ /^(https?\.(request|get)|net\.connect)$/.test(aliased)
113
+ )
114
+ return "network-call";
84
115
  if (
85
116
  node.type === "CallExpression" &&
86
117
  /^(fetch|globalThis.fetch|https?\.(request|get)|net\.connect)$/.test(name)
@@ -144,7 +175,92 @@ export function inspectJavaScript(artifact, source, overrides = {}) {
144
175
  const literals = new Map(),
145
176
  candidates = [],
146
177
  evidence = new Map();
147
- let sourceLiteralTruncated = false;
178
+ let sourceLiteralTruncated = false,
179
+ syntaxCount = 0;
180
+ const hookTargets = new Map();
181
+ const linked = new Map();
182
+ const link = (path, hooks, from) => {
183
+ const item = linked.get(path) ?? { hooks: new Set(), from: new Set() };
184
+ for (const hook of hooks) item.hooks.add(hook);
185
+ item.from.add(from);
186
+ if (linked.has(path) || linked.size < BEHAVIOR_LIMITS.linkedScripts)
187
+ linked.set(path, item);
188
+ };
189
+ const behaviorUse = { review: 0, HIGH: 0, perFileRule: new Map() };
190
+ const admitBehavior = (priority, path, ruleId) => {
191
+ const key = `${path}\0${ruleId}`;
192
+ const used = behaviorUse.perFileRule.get(key) || 0;
193
+ const tier = priority === "HIGH" ? "HIGH" : "review";
194
+ const cap =
195
+ tier === "HIGH" ? BEHAVIOR_LIMITS.highCues : BEHAVIOR_LIMITS.reviewCues;
196
+ if (
197
+ used >= 0 &&
198
+ behaviorUse[tier] < cap &&
199
+ (tier === "HIGH" || used < BEHAVIOR_LIMITS.reviewCuesPerFileRule)
200
+ ) {
201
+ behaviorUse[tier]++;
202
+ behaviorUse.perFileRule.set(key, used + 1);
203
+ return true;
204
+ }
205
+ if (used !== -1) {
206
+ behaviorUse.perFileRule.set(key, -1);
207
+ result.annotations.push({
208
+ type: "behavior-not-inspected",
209
+ side: "artifact",
210
+ path,
211
+ ruleId,
212
+ reason: "behavior-cue-cap",
213
+ });
214
+ }
215
+ return false;
216
+ };
217
+ const manifest = (artifact?.entries || []).find(
218
+ (entry) => entry.type === "file" && entry.path === "package.json",
219
+ );
220
+ let behaviorManifest = { status: "not-assessed", reason: "not-retained" };
221
+ if (Buffer.isBuffer(manifest?.content))
222
+ try {
223
+ const text = new TextDecoder("utf-8", {
224
+ fatal: true,
225
+ ignoreBOM: true,
226
+ }).decode(manifest.content);
227
+ const positions = offsets(text);
228
+ const parsed = manifestScriptSpans(text);
229
+ behaviorManifest = parsed.reason
230
+ ? { status: parsed.status, reason: parsed.reason }
231
+ : { status: parsed.status };
232
+ for (const item of parsed.hooks) {
233
+ const assessed = scriptSignals(item.script);
234
+ if (assessed.status !== "assessed")
235
+ result.annotations.push({
236
+ type: "behavior-not-inspected",
237
+ side: "artifact",
238
+ path: manifest.path,
239
+ hook: item.hook,
240
+ reason: assessed.reason,
241
+ });
242
+ for (const script of assessed.scripts || [])
243
+ link(script, [item.hook], `package.json#scripts.${item.hook}`);
244
+ for (const target of assessed.targets || [])
245
+ hookTargets.set(target, [
246
+ ...new Set([...(hookTargets.get(target) || []), item.hook]),
247
+ ]);
248
+ for (const { ruleId, priority, ...signal } of assessed.signals)
249
+ if (admitBehavior(priority, manifest.path, ruleId))
250
+ candidates.push({
251
+ behavior: { ruleId, priority, hook: item.hook, ...signal },
252
+ evidence: evidenceFor(
253
+ "artifact",
254
+ artifact,
255
+ manifest,
256
+ item,
257
+ positions,
258
+ ),
259
+ });
260
+ }
261
+ } catch {
262
+ behaviorManifest = { status: "not-assessed", reason: "invalid-utf8" };
263
+ }
148
264
  for (const [side, inventory] of [
149
265
  ["artifact", artifact],
150
266
  ["source", source],
@@ -219,7 +335,35 @@ export function inspectJavaScript(artifact, source, overrides = {}) {
219
335
  coverage.files++;
220
336
  coverage.bytes += entry.size;
221
337
  const positions = offsets(text);
222
- for (const node of nodes(ast)) {
338
+ const all = nodes(ast);
339
+ const networkAliases =
340
+ side === "artifact" ? moduleAliases(all, NETWORK_MODULES) : null;
341
+ if (side === "artifact") {
342
+ const hooks = hookTargets.get(entry.path);
343
+ const found = obfuscationSignals(all);
344
+ if (found.capped) skip("behavior-obfuscation-cap");
345
+ const destructive = destructiveSignals(all);
346
+ if (destructive.capped) skip("behavior-destructive-cap");
347
+ found.signals.push(...destructive.signals);
348
+ for (const {
349
+ ruleId,
350
+ priority,
351
+ node,
352
+ related,
353
+ detail,
354
+ } of found.signals) {
355
+ if (admitBehavior(priority, entry.path, ruleId))
356
+ candidates.push({
357
+ behavior: { ruleId, priority, ...detail },
358
+ hooks,
359
+ evidence: evidenceFor(side, inventory, entry, node, positions),
360
+ related: related.map((item) =>
361
+ evidenceFor(side, inventory, entry, item, positions),
362
+ ),
363
+ });
364
+ }
365
+ }
366
+ for (const node of all) {
223
367
  if (side === "source") {
224
368
  if (
225
369
  node.type === "Literal" &&
@@ -235,24 +379,128 @@ export function inspectJavaScript(artifact, source, overrides = {}) {
235
379
  }
236
380
  continue;
237
381
  }
238
- const kind = capability(node);
239
- if (!kind) continue;
240
- if (candidates.length >= limits.cues) {
382
+ const kind = capability(node, networkAliases);
383
+ const signal = javascriptSignal(node);
384
+ if (!kind && !signal) continue;
385
+ if (kind && syntaxCount >= limits.cues) {
241
386
  skip("syntax-cue-cap");
242
387
  break;
243
388
  }
244
- candidates.push({
245
- kind,
246
- evidence: evidenceFor(side, inventory, entry, node, positions),
247
- literal:
248
- node.arguments?.[0]?.type === "Literal" &&
249
- typeof node.arguments[0].value === "string"
250
- ? node.arguments[0].value
251
- : null,
252
- });
389
+ const hooks = hookTargets.get(entry.path);
390
+ syntaxCount += kind ? 1 : 0;
391
+ if (kind)
392
+ candidates.push({
393
+ kind,
394
+ hooks,
395
+ evidence: evidenceFor(side, inventory, entry, node, positions),
396
+ literal:
397
+ node.arguments?.[0]?.type === "Literal" &&
398
+ typeof node.arguments[0].value === "string"
399
+ ? node.arguments[0].value
400
+ : null,
401
+ });
402
+ if (signal && admitBehavior("review", entry.path, signal.ruleId)) {
403
+ const { ruleId, at, ...detail } = signal;
404
+ candidates.push({
405
+ behavior: { ruleId, priority: "review", ...detail },
406
+ hooks,
407
+ evidence: evidenceFor(
408
+ side,
409
+ inventory,
410
+ entry,
411
+ at || node,
412
+ positions,
413
+ ),
414
+ });
415
+ }
253
416
  }
254
417
  }
255
418
  }
419
+ for (const candidate of candidates)
420
+ if (
421
+ candidate.kind === "process-execution" &&
422
+ candidate.hooks &&
423
+ candidate.literal !== null
424
+ )
425
+ for (const script of scriptSignals(candidate.literal).scripts || [])
426
+ link(script, candidate.hooks, candidate.evidence.path);
427
+ for (const [path, { hooks, from }] of linked) {
428
+ const entry = artifact.entries.find(
429
+ (item) => item.type === "file" && item.path === path,
430
+ );
431
+ const unscanned = (reason) =>
432
+ result.annotations.push({
433
+ type: "behavior-not-inspected",
434
+ side: "artifact",
435
+ path,
436
+ reason,
437
+ });
438
+ if (!entry) {
439
+ unscanned("linked-script-missing");
440
+ continue;
441
+ }
442
+ if (!Buffer.isBuffer(entry.content)) {
443
+ unscanned("not-retained");
444
+ continue;
445
+ }
446
+ if (entry.size > BEHAVIOR_LIMITS.linkedScriptBytes) {
447
+ unscanned("linked-script-byte-cap");
448
+ continue;
449
+ }
450
+ let text;
451
+ try {
452
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
453
+ entry.content,
454
+ );
455
+ } catch {
456
+ unscanned("invalid-utf8");
457
+ continue;
458
+ }
459
+ const assessed = scriptSignals(text, {
460
+ maxBytes: BEHAVIOR_LIMITS.linkedScriptBytes,
461
+ maxTokens: BEHAVIOR_LIMITS.linkedScriptTokens,
462
+ });
463
+ if (assessed.status !== "assessed") {
464
+ unscanned(assessed.reason);
465
+ continue;
466
+ }
467
+ const positions = offsets(text);
468
+ const lines = [];
469
+ for (let start = 0; start < text.length;) {
470
+ const stop = text.indexOf("\n", start);
471
+ const end = stop === -1 ? text.length : stop;
472
+ lines.push({ start, end, lower: text.slice(start, end).toLowerCase() });
473
+ start = end + 1;
474
+ }
475
+ for (const { ruleId, priority, ...signal } of assessed.signals) {
476
+ const words = [
477
+ ...(signal.tools || []),
478
+ ...(signal.file ? [signal.file.toLowerCase()] : []),
479
+ ...(signal.paths || []),
480
+ ...(signal.variables || []).map((name) => name.toLowerCase()),
481
+ ...(signal.commands || []).map((command) => command.split(" ")[0]),
482
+ ];
483
+ const cited = lines
484
+ .filter((line) => words.some((word) => line.lower.includes(word)))
485
+ .slice(0, 3);
486
+ if (!cited.length || !admitBehavior(priority, path, ruleId)) continue;
487
+ const [first, ...rest] = cited.map((line) =>
488
+ evidenceFor("artifact", artifact, entry, line, positions),
489
+ );
490
+ candidates.push({
491
+ behavior: {
492
+ ruleId,
493
+ priority,
494
+ script: path,
495
+ linkedFrom: [...from].sort(),
496
+ ...signal,
497
+ },
498
+ hooks: [...hooks].sort(),
499
+ evidence: first,
500
+ related: rest,
501
+ });
502
+ }
503
+ }
256
504
  const stem = (value) => value.replace(/\.[cm]?[jt]sx?$/, "");
257
505
  const unknownSource = new Set(
258
506
  result.annotations
@@ -265,6 +513,27 @@ export function inspectJavaScript(artifact, source, overrides = {}) {
265
513
  for (const candidate of candidates) {
266
514
  const ids = [candidate.evidence.id];
267
515
  evidence.set(candidate.evidence.id, candidate.evidence);
516
+ const hookTarget = candidate.hooks
517
+ ? { installHookTarget: candidate.hooks }
518
+ : {};
519
+ for (const item of candidate.related || []) {
520
+ evidence.set(item.id, item);
521
+ if (!ids.includes(item.id)) ids.push(item.id);
522
+ }
523
+ if (candidate.behavior) {
524
+ const { ruleId, priority, ...signal } = candidate.behavior;
525
+ result.cues.push({
526
+ module: "behavior",
527
+ ruleId,
528
+ priority,
529
+ summary: summaryFor(ruleId),
530
+ confidence: "suspicion-not-proof",
531
+ signal,
532
+ ...hookTarget,
533
+ evidenceIds: ids,
534
+ });
535
+ continue;
536
+ }
268
537
  const match = candidate.literal !== null && literals.get(candidate.literal);
269
538
  if (match) {
270
539
  evidence.set(match.id, match);
@@ -291,10 +560,22 @@ export function inspectJavaScript(artifact, source, overrides = {}) {
291
560
  ? "not-assessed"
292
561
  : "not-found-in-inspected-source"
293
562
  : "source-unavailable",
563
+ ...hookTarget,
294
564
  evidenceIds: ids,
295
565
  });
296
566
  }
297
567
  result.evidence = [...evidence.values()];
568
+ result.annotations.push({
569
+ type: "behavior-scope",
570
+ manifest: behaviorManifest,
571
+ limits: {
572
+ ...BEHAVIOR_LIMITS,
573
+ obfuscation: { ...OBFUSCATION_LIMITS },
574
+ destructive: { ...DESTRUCTIVE_LIMITS },
575
+ },
576
+ limitation:
577
+ "Behavior cues are static text and syntax suspicions, not proof of execution or intent. Install hooks and package code were not run; aliases, computed names and runtime-constructed paths are not resolved.",
578
+ });
298
579
  result.annotations.push({
299
580
  type: "syntax-scope",
300
581
  parser: "acorn@8.19.0",