@swestash/agent-lens 0.5.0 → 0.5.1

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.
@@ -2319,7 +2319,7 @@ function classify(db, dirty) {
2319
2319
 
2320
2320
  // ../ingest/dist/detect.js
2321
2321
  import { createHash } from "crypto";
2322
- var DETECTOR_VERSION = 3;
2322
+ var DETECTOR_VERSION = 5;
2323
2323
  function evidence(s, max = 200) {
2324
2324
  const one = s.replace(/\s+/g, " ").trim();
2325
2325
  return one.length > max ? one.slice(0, max - 1) + "\u2026" : one;
@@ -2330,7 +2330,7 @@ function findingId(toolCallId, ruleId) {
2330
2330
  var SYSTEM_PATH = /(^|\s)(\/etc\/|\/usr\/|\/bin\/|\/sbin\/|\/boot\/|\/var\/|\/root\/|~\/\.ssh\/|~\/\.aws\/)/;
2331
2331
  var HOME_OR_ROOT_TARGET = /(\s|^)(~\/?|\/|\/\*|\$HOME\/?|\*)(\s|$)/;
2332
2332
  var SECRET_FILE = /(^|\/|\s)(\.env(\.[\w.-]+)?|id_rsa|id_ed25519|id_ecdsa|\.ssh\/[\w.-]+|\.aws\/credentials|\.gcp\/|gcloud\/|\.npmrc|\.pypirc|\.netrc|\.pem|\.p12|\.pfx|secrets?\.(ya?ml|json|txt)|credentials(\.json)?)\b/i;
2333
- var EXAMPLE_FILE = /\.(example|sample|template|dist)($|\.)/i;
2333
+ var EXAMPLE_FILE = /\.(example|sample|template|dist)(?![\w-])/i;
2334
2334
  var CONTENT_READ_CMD = /\b(cat|less|more|head|tail|bat|xxd|strings|base64|grep|egrep|rg|awk|sed|nl|tac|od|hexdump|cp|scp|sort)\b/i;
2335
2335
  var SECRET_VALUE_PATTERNS = [
2336
2336
  { re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/, label: "private-key", critical: true },
@@ -2341,18 +2341,41 @@ var SECRET_VALUE_PATTERNS = [
2341
2341
  { re: /\bsk-[A-Za-z0-9]{20,}\b/, label: "api-secret-key" }
2342
2342
  ];
2343
2343
  var isBash = (t) => t === "Bash";
2344
- var AGENT_CONFIG_PATH = /(^|\/)\.claude\//;
2344
+ var stripComments = (c) => c.replace(/(^|\s)#[^\n]*/g, "$1");
2345
+ var neutralizeEcho = (c) => c.replace(/((?:^|[;&|\n(])\s*)(?:echo|printf)\b(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^;&|\n])*/gi, "$1echo");
2346
+ var blankHeredoc = (c) => c.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\s*\2\b/g, "<<HEREDOC");
2347
+ var codeOf = (c) => neutralizeEcho(stripComments(blankHeredoc(c)));
2348
+ var blankQuoted = (c) => c.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'[^']*'/g, "''");
2349
+ var bareOf = (c) => blankQuoted(codeOf(c));
2350
+ function execGeneratedScript(cmd) {
2351
+ const targets = /* @__PURE__ */ new Set();
2352
+ for (const m of cmd.matchAll(/(?:>>?|\btee\b\s+(?:-a\s+)?)\s*["']?([A-Za-z0-9._/~-]+)/g)) {
2353
+ if (m[1] && !m[1].startsWith("/dev/"))
2354
+ targets.add(m[1]);
2355
+ }
2356
+ for (const t of targets) {
2357
+ const e = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2358
+ const interp = new RegExp(`\\b(?:sh|bash|zsh|ksh|dash|source)\\s+["']?${e}\\b|(?:^|[\\n;|&(])\\s*\\.\\s+["']?${e}\\b`);
2359
+ const hasPath = /^(\.\/|\/|~\/)/.test(t);
2360
+ const path = hasPath ? new RegExp(`(?:^|[\\n;|&(])\\s*${e}(?:\\s|$|[;&|<>])`) : new RegExp(`(?:^|[\\n;|&(])\\s*\\.\\/${e}\\b`);
2361
+ if (interp.test(cmd) || path.test(cmd))
2362
+ return t;
2363
+ }
2364
+ return null;
2365
+ }
2345
2366
  var TEMP_PATH = /^(\/tmp\/|\/var\/folders\/|\/private\/var\/folders\/)/;
2346
- function isAgentOwnedPath(p) {
2347
- if (AGENT_CONFIG_PATH.test(p) || TEMP_PATH.test(p))
2367
+ function isAgentOwnedPath(p, configDirs) {
2368
+ if (TEMP_PATH.test(p))
2369
+ return true;
2370
+ if (configDirs.some((d) => p === d || p.startsWith(d + "/")))
2348
2371
  return true;
2349
2372
  const tmp = process.env.TMPDIR;
2350
2373
  return !!tmp && p.startsWith(tmp.replace(/\/$/, "") + "/");
2351
2374
  }
2352
- function outsideProject(filePath, projectPath) {
2375
+ function outsideProject(filePath, projectPath, configDirs) {
2353
2376
  if (!filePath || !filePath.startsWith("/"))
2354
2377
  return false;
2355
- if (isAgentOwnedPath(filePath))
2378
+ if (isAgentOwnedPath(filePath, configDirs))
2356
2379
  return false;
2357
2380
  if (!projectPath)
2358
2381
  return false;
@@ -2368,14 +2391,14 @@ var RULES = [
2368
2391
  tools: ["Bash"],
2369
2392
  baseSeverity: "high",
2370
2393
  test(ctx) {
2371
- const c = ctx.command;
2394
+ const c = ctx.commandBare;
2372
2395
  if (!c)
2373
2396
  return null;
2374
2397
  const rf = /\brm\s+[^|;&\n]*(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r|--recursive[^|;&\n]*--force|--force[^|;&\n]*--recursive)/i;
2375
2398
  if (!rf.test(c))
2376
2399
  return null;
2377
2400
  const dangerousTarget = HOME_OR_ROOT_TARGET.test(c) || /\s\/(?:\s|$)/.test(c) || /\brm\b[^|;&\n]*\*/.test(c);
2378
- return { evidence: evidence(c), severity: dangerousTarget ? "critical" : "high", mods: { dangerous_target: dangerousTarget } };
2401
+ return { evidence: evidence(ctx.command), severity: dangerousTarget ? "critical" : "high", mods: { dangerous_target: dangerousTarget } };
2379
2402
  }
2380
2403
  },
2381
2404
  {
@@ -2386,10 +2409,10 @@ var RULES = [
2386
2409
  tools: ["Bash"],
2387
2410
  baseSeverity: "medium",
2388
2411
  test(ctx) {
2389
- const c = ctx.command;
2412
+ const c = ctx.commandBare;
2390
2413
  if (!c || !/\bgit\s+reset\s+--hard\b/i.test(c))
2391
2414
  return null;
2392
- return { evidence: evidence(c) };
2415
+ return { evidence: evidence(ctx.command) };
2393
2416
  }
2394
2417
  },
2395
2418
  {
@@ -2400,11 +2423,11 @@ var RULES = [
2400
2423
  tools: ["Bash"],
2401
2424
  baseSeverity: "medium",
2402
2425
  test(ctx) {
2403
- const c = ctx.command;
2426
+ const c = ctx.commandBare;
2404
2427
  if (!c || !/\bgit\s+push\b[^|;&\n]*(--force\b|--force-with-lease\b|\s-f\b)/i.test(c))
2405
2428
  return null;
2406
2429
  const mainBranch = /\b(main|master|release)\b/.test(c);
2407
- return { evidence: evidence(c), severity: mainBranch ? "high" : "medium", mods: { protected_branch: mainBranch } };
2430
+ return { evidence: evidence(ctx.command), severity: mainBranch ? "high" : "medium", mods: { protected_branch: mainBranch } };
2408
2431
  }
2409
2432
  },
2410
2433
  {
@@ -2415,10 +2438,11 @@ var RULES = [
2415
2438
  tools: "*",
2416
2439
  baseSeverity: "high",
2417
2440
  test(ctx) {
2418
- const hay = ctx.command ?? (typeof ctx.input?.query === "string" ? ctx.input.query : null);
2441
+ const query = typeof ctx.input?.query === "string" ? ctx.input.query : null;
2442
+ const hay = ctx.commandCode ?? query;
2419
2443
  if (!hay || !/\b(DROP\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE(\s+TABLE)?)\b/i.test(hay))
2420
2444
  return null;
2421
- return { evidence: evidence(hay) };
2445
+ return { evidence: evidence(ctx.command ?? query) };
2422
2446
  }
2423
2447
  },
2424
2448
  {
@@ -2429,10 +2453,10 @@ var RULES = [
2429
2453
  tools: ["Bash"],
2430
2454
  baseSeverity: "high",
2431
2455
  test(ctx) {
2432
- const c = ctx.command;
2456
+ const c = ctx.commandBare;
2433
2457
  if (!c || !/(\bdd\s+if=|\bmkfs(\.\w+)?\b|\bshred\b)/i.test(c))
2434
2458
  return null;
2435
- return { evidence: evidence(c) };
2459
+ return { evidence: evidence(ctx.command) };
2436
2460
  }
2437
2461
  },
2438
2462
  {
@@ -2460,12 +2484,18 @@ var RULES = [
2460
2484
  tools: ["Read", "Bash"],
2461
2485
  baseSeverity: "high",
2462
2486
  test(ctx) {
2463
- const hay = ctx.toolName === "Read" ? ctx.filePath : ctx.command;
2464
- if (!hay || !SECRET_FILE.test(hay) || EXAMPLE_FILE.test(hay))
2465
- return null;
2466
- if (isBash(ctx.toolName) && !CONTENT_READ_CMD.test(hay))
2487
+ if (isBash(ctx.toolName)) {
2488
+ const cmd = ctx.commandBare;
2489
+ if (!cmd)
2490
+ return null;
2491
+ const segments = cmd.split(/\|\||&&|[|;\n]/);
2492
+ const reads = segments.some((seg) => SECRET_FILE.test(seg) && !EXAMPLE_FILE.test(seg) && CONTENT_READ_CMD.test(seg));
2493
+ return reads ? { evidence: evidence(ctx.command) } : null;
2494
+ }
2495
+ const f = ctx.filePath;
2496
+ if (!f || !SECRET_FILE.test(f) || EXAMPLE_FILE.test(f))
2467
2497
  return null;
2468
- return { evidence: evidence(hay) };
2498
+ return { evidence: evidence(f) };
2469
2499
  }
2470
2500
  },
2471
2501
  {
@@ -2496,14 +2526,14 @@ var RULES = [
2496
2526
  tools: ["Bash"],
2497
2527
  baseSeverity: "critical",
2498
2528
  test(ctx) {
2499
- const c = ctx.command;
2529
+ const c = ctx.commandBare;
2500
2530
  if (!c)
2501
2531
  return null;
2502
2532
  if (!/(\||<)\s*(sudo\s+)?(curl|wget|nc|ncat|netcat|ssh|scp)\b/i.test(c))
2503
2533
  return null;
2504
2534
  if (!/\b(cat|tar|zip|gzip|base64|env|printenv|cat\s|<)\b/i.test(c))
2505
2535
  return null;
2506
- return { evidence: evidence(c) };
2536
+ return { evidence: evidence(ctx.command) };
2507
2537
  }
2508
2538
  },
2509
2539
  {
@@ -2514,7 +2544,7 @@ var RULES = [
2514
2544
  tools: ["Bash"],
2515
2545
  baseSeverity: "high",
2516
2546
  test(ctx) {
2517
- const c = ctx.command;
2547
+ const c = ctx.commandBare;
2518
2548
  if (!c)
2519
2549
  return null;
2520
2550
  const upload = /\b(curl|wget)\b[^|;&\n]*(-X\s*POST|--data\b|--data-\w+\b|\s-d\b|-T\b|--upload-file\b|-F\b|--form\b)/i.test(c);
@@ -2523,7 +2553,7 @@ var RULES = [
2523
2553
  const external = /https?:\/\/(?!(localhost|127\.0\.0\.1|0\.0\.0\.0))/i.test(c);
2524
2554
  const withFile = /(@[^\s'"]+|--upload-file|\s-T\b)/.test(c);
2525
2555
  return {
2526
- evidence: evidence(c),
2556
+ evidence: evidence(ctx.command),
2527
2557
  severity: withFile && external ? "critical" : "high",
2528
2558
  mods: { external, with_file: withFile }
2529
2559
  };
@@ -2537,12 +2567,12 @@ var RULES = [
2537
2567
  tools: ["Bash"],
2538
2568
  baseSeverity: "critical",
2539
2569
  test(ctx) {
2540
- const c = ctx.command;
2570
+ const c = ctx.commandBare;
2541
2571
  if (!c)
2542
2572
  return null;
2543
2573
  if (!/\b(nc|ncat|netcat)\b[^|;&\n]*(-e\b|-c\b|\d{2,5})|\/dev\/tcp\/|bash\s+-i\b/i.test(c))
2544
2574
  return null;
2545
- return { evidence: evidence(c) };
2575
+ return { evidence: evidence(ctx.command) };
2546
2576
  }
2547
2577
  },
2548
2578
  // --- Privilege escalation / guardrail bypass (OWASP LLM06 Excessive Agency)
@@ -2554,10 +2584,10 @@ var RULES = [
2554
2584
  tools: "*",
2555
2585
  baseSeverity: "high",
2556
2586
  test(ctx) {
2557
- const c = ctx.command;
2587
+ const c = ctx.commandBare;
2558
2588
  if (!c || !/--dangerously-skip-permissions\b|--yolo\b|--dangerously-skip\b/i.test(c))
2559
2589
  return null;
2560
- return { evidence: evidence(c) };
2590
+ return { evidence: evidence(ctx.command) };
2561
2591
  }
2562
2592
  },
2563
2593
  {
@@ -2568,10 +2598,10 @@ var RULES = [
2568
2598
  tools: ["Bash"],
2569
2599
  baseSeverity: "high",
2570
2600
  test(ctx) {
2571
- const c = ctx.command;
2601
+ const c = ctx.commandBare;
2572
2602
  if (!c || !/\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh|python\d?|node)\b/i.test(c))
2573
2603
  return null;
2574
- return { evidence: evidence(c) };
2604
+ return { evidence: evidence(ctx.command) };
2575
2605
  }
2576
2606
  },
2577
2607
  {
@@ -2583,10 +2613,28 @@ var RULES = [
2583
2613
  baseSeverity: "high",
2584
2614
  // root escalation removes a real safety barrier — higher than an in-project op
2585
2615
  test(ctx) {
2586
- const c = ctx.command;
2587
- if (!c || !/(^|\s|\|)sudo\s+/i.test(c))
2616
+ const c = ctx.commandBare;
2617
+ if (!c || !/(?:^|[\n;|&(])\s*sudo\b/i.test(c))
2618
+ return null;
2619
+ return { evidence: evidence(ctx.command) };
2620
+ }
2621
+ },
2622
+ {
2623
+ id: "privilege.exec_generated_script",
2624
+ category: "privilege-bypass",
2625
+ framework_ref: "OWASP LLM06",
2626
+ title: "Writes a shell script from inline output and executes it",
2627
+ tools: ["Bash"],
2628
+ baseSeverity: "high",
2629
+ test(ctx) {
2630
+ const raw = ctx.command;
2631
+ if (!raw)
2632
+ return null;
2633
+ const file = execGeneratedScript(stripComments(raw));
2634
+ if (!file)
2588
2635
  return null;
2589
- return { evidence: evidence(c) };
2636
+ const critical = /(?:^|["'\s;|&(])sudo\b|\brm\s+[^|;&\n]*-[a-z]*r[a-z]*f|\bchmod\s+0?777\b|\bmkfs|\bdd\s+if=/i.test(raw);
2637
+ return { evidence: evidence(raw), severity: critical ? "critical" : "high", mods: { script_file: file } };
2590
2638
  }
2591
2639
  },
2592
2640
  {
@@ -2597,10 +2645,10 @@ var RULES = [
2597
2645
  tools: ["Bash"],
2598
2646
  baseSeverity: "medium",
2599
2647
  test(ctx) {
2600
- const c = ctx.command;
2648
+ const c = ctx.commandBare;
2601
2649
  if (!c || !/\bchmod\s+(-[a-zA-Z]+\s+)*0?777\b/i.test(c))
2602
2650
  return null;
2603
- return { evidence: evidence(c) };
2651
+ return { evidence: evidence(ctx.command) };
2604
2652
  }
2605
2653
  },
2606
2654
  {
@@ -2611,12 +2659,12 @@ var RULES = [
2611
2659
  tools: ["Bash"],
2612
2660
  baseSeverity: "high",
2613
2661
  test(ctx) {
2614
- const c = ctx.command;
2662
+ const c = ctx.commandBare;
2615
2663
  if (!c)
2616
2664
  return null;
2617
2665
  if (!/\bcrontab\s+-|\bsystemctl\s+enable\b|\blaunchctl\s+load\b|>>\s*~\/\.(bashrc|zshrc|profile|bash_profile)\b|\/etc\/(cron|systemd|rc\.local)/i.test(c))
2618
2666
  return null;
2619
- return { evidence: evidence(c) };
2667
+ return { evidence: evidence(ctx.command) };
2620
2668
  }
2621
2669
  },
2622
2670
  {
@@ -2628,7 +2676,7 @@ var RULES = [
2628
2676
  baseSeverity: "medium",
2629
2677
  test(ctx) {
2630
2678
  const f = ctx.filePath;
2631
- if (!outsideProject(f, ctx.projectPath))
2679
+ if (!outsideProject(f, ctx.projectPath, ctx.ownedConfigDirs))
2632
2680
  return null;
2633
2681
  const system = !!f && SYSTEM_PATH.test(f);
2634
2682
  return { evidence: evidence(f), severity: system ? "high" : "medium", mods: { project: ctx.projectPath, system_path: system } };
@@ -2638,7 +2686,7 @@ var RULES = [
2638
2686
  function appliesTo(rule, toolName) {
2639
2687
  return rule.tools === "*" || rule.tools.includes(toolName);
2640
2688
  }
2641
- function buildContext(row, projectPath) {
2689
+ function buildContext(row, projectPath, ownedConfigDirs) {
2642
2690
  let input = null;
2643
2691
  try {
2644
2692
  input = row.input_json ? JSON.parse(row.input_json) : null;
@@ -2646,8 +2694,10 @@ function buildContext(row, projectPath) {
2646
2694
  input = null;
2647
2695
  }
2648
2696
  const command = typeof input?.command === "string" ? input.command : null;
2697
+ const commandCode = command != null ? codeOf(command) : null;
2698
+ const commandBare = command != null ? bareOf(command) : null;
2649
2699
  const filePath = typeof input?.file_path === "string" ? input.file_path : null;
2650
- return { toolName: row.tool_name, command, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath };
2700
+ return { toolName: row.tool_name, command, commandCode, commandBare, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath, ownedConfigDirs };
2651
2701
  }
2652
2702
  function detect(db, dirty) {
2653
2703
  const incremental = dirty != null;
@@ -2661,6 +2711,7 @@ function detect(db, dirty) {
2661
2711
  })(dirty);
2662
2712
  }
2663
2713
  const scope = incremental ? " WHERE tc.session_id IN (SELECT id FROM _dirty_sec)" : "";
2714
+ const ownedConfigDirs = db.prepare("SELECT config_dir FROM sources WHERE config_dir IS NOT NULL").all().map((r) => r.config_dir.replace(/\/$/, "")).filter(Boolean);
2664
2715
  const projPath = /* @__PURE__ */ new Map();
2665
2716
  for (const r of db.prepare(`SELECT s.id id, p.path path FROM sessions s JOIN projects p ON p.id = s.project_id${incremental ? " WHERE s.id IN (SELECT id FROM _dirty_sec)" : ""}`).all()) {
2666
2717
  projPath.set(r.id, r.path);
@@ -2674,7 +2725,7 @@ function detect(db, dirty) {
2674
2725
  const tx = db.transaction(() => {
2675
2726
  db.exec(delScope);
2676
2727
  for (const row of rows) {
2677
- const ctx = buildContext(row, projPath.get(row.session_id) ?? null);
2728
+ const ctx = buildContext(row, projPath.get(row.session_id) ?? null, ownedConfigDirs);
2678
2729
  for (const rule of RULES) {
2679
2730
  if (!appliesTo(rule, ctx.toolName))
2680
2731
  continue;