@swestash/agent-lens 0.5.0 → 0.5.2
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.
- package/dist/agent-lens.js +153 -56
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/{CopyButton-WFGbfzdo.js → CopyButton-hsiW2B6o.js} +1 -1
- package/web/assets/{Dashboard-B0wpOYPt.js → Dashboard-CPfpfHm8.js} +1 -1
- package/web/assets/SecurityView-CvWWSbuL.js +1 -0
- package/web/assets/SessionView-BLTA0iKW.js +12 -0
- package/web/assets/{SkillView-COo5sndg.js → SkillView-DVoLJAU0.js} +1 -1
- package/web/assets/{SkillsView-C6f11mVw.js → SkillsView-c6k5JsWR.js} +1 -1
- package/web/assets/{WorkflowView-B8jF6aE7.js → WorkflowView-juGHkf05.js} +1 -1
- package/web/assets/{index-CnYz-f0y.js → index-BXp9oWUn.js} +1 -1
- package/web/assets/index-BfFCjgfs.css +1 -0
- package/web/assets/{index-DTfBIYUK.js → index-D-kuW17t.js} +2 -2
- package/web/assets/{severity-DoLHU1bt.js → severity-ZaGgvxL_.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/SecurityView-D9MKBBkp.js +0 -1
- package/web/assets/SessionView-BkOEhK9L.js +0 -12
- package/web/assets/index-DfoRqKEF.css +0 -1
package/dist/agent-lens.js
CHANGED
|
@@ -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 =
|
|
2322
|
+
var DETECTOR_VERSION = 6;
|
|
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)(
|
|
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,23 +2341,73 @@ 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
|
|
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 (
|
|
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;
|
|
2359
2382
|
return filePath !== projectPath && !filePath.startsWith(projectPath.replace(/\/$/, "") + "/");
|
|
2360
2383
|
}
|
|
2384
|
+
var SCOPE_RANK = { unknown: 0, loopback: 1, internal: 2, external: 3 };
|
|
2385
|
+
function netTargetScope(rawCommand) {
|
|
2386
|
+
const s = rawCommand.replace(/(?:-H|--header)\s+(['"]).*?\1/gi, " ").replace(/(?:-H|--header)\s+[^\s'"]+/gi, " ");
|
|
2387
|
+
const authorities = [];
|
|
2388
|
+
for (const m of s.matchAll(/\bhttps?:\/\/(\[[0-9a-fA-F:]+\]|[^/\s"'<>|]+)/gi))
|
|
2389
|
+
authorities.push(m[1]);
|
|
2390
|
+
for (const m of s.matchAll(/(?:^|[\s"'=(])(\[[0-9a-fA-F:]+\]|[A-Za-z0-9.-]+):\d{2,5}(?=[/\s"']|$)/g))
|
|
2391
|
+
authorities.push(m[1]);
|
|
2392
|
+
let scope = "unknown";
|
|
2393
|
+
for (let host of authorities) {
|
|
2394
|
+
host = host.replace(/^\[|\]$/g, "").replace(/^[^@/]*@/, "").toLowerCase();
|
|
2395
|
+
if (!host.includes("::"))
|
|
2396
|
+
host = host.replace(/:\d+$/, "");
|
|
2397
|
+
let hs;
|
|
2398
|
+
if (host === "localhost" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || host === "0.0.0.0" || host === "::1")
|
|
2399
|
+
hs = "loopback";
|
|
2400
|
+
else if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || // RFC1918
|
|
2401
|
+
/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host) || /^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host) || /^169\.254\.\d{1,3}\.\d{1,3}$/.test(host) || // link-local
|
|
2402
|
+
host.endsWith(".local") || host.endsWith(".internal") || !host.includes("."))
|
|
2403
|
+
hs = "internal";
|
|
2404
|
+
else
|
|
2405
|
+
hs = "external";
|
|
2406
|
+
if (SCOPE_RANK[hs] > SCOPE_RANK[scope])
|
|
2407
|
+
scope = hs;
|
|
2408
|
+
}
|
|
2409
|
+
return scope;
|
|
2410
|
+
}
|
|
2361
2411
|
var RULES = [
|
|
2362
2412
|
// --- Destructive / data-loss (OWASP ASI02 Tool Misuse) -------------------
|
|
2363
2413
|
{
|
|
@@ -2368,14 +2418,14 @@ var RULES = [
|
|
|
2368
2418
|
tools: ["Bash"],
|
|
2369
2419
|
baseSeverity: "high",
|
|
2370
2420
|
test(ctx) {
|
|
2371
|
-
const c = ctx.
|
|
2421
|
+
const c = ctx.commandBare;
|
|
2372
2422
|
if (!c)
|
|
2373
2423
|
return null;
|
|
2374
2424
|
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
2425
|
if (!rf.test(c))
|
|
2376
2426
|
return null;
|
|
2377
2427
|
const dangerousTarget = HOME_OR_ROOT_TARGET.test(c) || /\s\/(?:\s|$)/.test(c) || /\brm\b[^|;&\n]*\*/.test(c);
|
|
2378
|
-
return { evidence: evidence(
|
|
2428
|
+
return { evidence: evidence(ctx.command), severity: dangerousTarget ? "critical" : "high", mods: { dangerous_target: dangerousTarget } };
|
|
2379
2429
|
}
|
|
2380
2430
|
},
|
|
2381
2431
|
{
|
|
@@ -2384,12 +2434,13 @@ var RULES = [
|
|
|
2384
2434
|
framework_ref: "OWASP ASI02",
|
|
2385
2435
|
title: "Discards local changes (git reset --hard)",
|
|
2386
2436
|
tools: ["Bash"],
|
|
2387
|
-
baseSeverity: "
|
|
2437
|
+
baseSeverity: "low",
|
|
2438
|
+
// routine dev op — only uncommitted working-tree changes are lost; commits survive in reflog
|
|
2388
2439
|
test(ctx) {
|
|
2389
|
-
const c = ctx.
|
|
2440
|
+
const c = ctx.commandBare;
|
|
2390
2441
|
if (!c || !/\bgit\s+reset\s+--hard\b/i.test(c))
|
|
2391
2442
|
return null;
|
|
2392
|
-
return { evidence: evidence(
|
|
2443
|
+
return { evidence: evidence(ctx.command) };
|
|
2393
2444
|
}
|
|
2394
2445
|
},
|
|
2395
2446
|
{
|
|
@@ -2398,13 +2449,14 @@ var RULES = [
|
|
|
2398
2449
|
framework_ref: "OWASP ASI02",
|
|
2399
2450
|
title: "Force-push rewrites remote history",
|
|
2400
2451
|
tools: ["Bash"],
|
|
2401
|
-
baseSeverity: "
|
|
2452
|
+
baseSeverity: "low",
|
|
2453
|
+
// feature-branch force-push is routine; only a protected branch (below) is high
|
|
2402
2454
|
test(ctx) {
|
|
2403
|
-
const c = ctx.
|
|
2455
|
+
const c = ctx.commandBare;
|
|
2404
2456
|
if (!c || !/\bgit\s+push\b[^|;&\n]*(--force\b|--force-with-lease\b|\s-f\b)/i.test(c))
|
|
2405
2457
|
return null;
|
|
2406
2458
|
const mainBranch = /\b(main|master|release)\b/.test(c);
|
|
2407
|
-
return { evidence: evidence(
|
|
2459
|
+
return { evidence: evidence(ctx.command), severity: mainBranch ? "high" : "low", mods: { protected_branch: mainBranch } };
|
|
2408
2460
|
}
|
|
2409
2461
|
},
|
|
2410
2462
|
{
|
|
@@ -2415,10 +2467,11 @@ var RULES = [
|
|
|
2415
2467
|
tools: "*",
|
|
2416
2468
|
baseSeverity: "high",
|
|
2417
2469
|
test(ctx) {
|
|
2418
|
-
const
|
|
2470
|
+
const query = typeof ctx.input?.query === "string" ? ctx.input.query : null;
|
|
2471
|
+
const hay = ctx.commandCode ?? query;
|
|
2419
2472
|
if (!hay || !/\b(DROP\s+(TABLE|DATABASE|SCHEMA)|TRUNCATE(\s+TABLE)?)\b/i.test(hay))
|
|
2420
2473
|
return null;
|
|
2421
|
-
return { evidence: evidence(
|
|
2474
|
+
return { evidence: evidence(ctx.command ?? query) };
|
|
2422
2475
|
}
|
|
2423
2476
|
},
|
|
2424
2477
|
{
|
|
@@ -2429,10 +2482,10 @@ var RULES = [
|
|
|
2429
2482
|
tools: ["Bash"],
|
|
2430
2483
|
baseSeverity: "high",
|
|
2431
2484
|
test(ctx) {
|
|
2432
|
-
const c = ctx.
|
|
2485
|
+
const c = ctx.commandBare;
|
|
2433
2486
|
if (!c || !/(\bdd\s+if=|\bmkfs(\.\w+)?\b|\bshred\b)/i.test(c))
|
|
2434
2487
|
return null;
|
|
2435
|
-
return { evidence: evidence(
|
|
2488
|
+
return { evidence: evidence(ctx.command) };
|
|
2436
2489
|
}
|
|
2437
2490
|
},
|
|
2438
2491
|
{
|
|
@@ -2441,14 +2494,16 @@ var RULES = [
|
|
|
2441
2494
|
framework_ref: "OWASP ASI02",
|
|
2442
2495
|
title: "Overwrites a critical project file",
|
|
2443
2496
|
tools: ["Write", "Edit", "MultiEdit"],
|
|
2444
|
-
baseSeverity: "
|
|
2497
|
+
baseSeverity: "low",
|
|
2445
2498
|
test(ctx) {
|
|
2446
2499
|
const f = ctx.filePath;
|
|
2447
2500
|
if (!f)
|
|
2448
2501
|
return null;
|
|
2449
|
-
|
|
2502
|
+
const lock = /(^|\/)(package-lock\.json|pnpm-lock\.ya?ml|yarn\.lock|Cargo\.lock|go\.sum)\b/.test(f);
|
|
2503
|
+
const ci = /(^|\/)(\.github\/workflows\/|\.gitlab-ci\.ya?ml)\b/.test(f);
|
|
2504
|
+
if (!lock && !ci)
|
|
2450
2505
|
return null;
|
|
2451
|
-
return { evidence: evidence(f) };
|
|
2506
|
+
return { evidence: evidence(f), severity: ci ? "medium" : "low", mods: { kind: ci ? "ci-config" : "lockfile" } };
|
|
2452
2507
|
}
|
|
2453
2508
|
},
|
|
2454
2509
|
// --- Credential & secret access ------------------------------------------
|
|
@@ -2460,12 +2515,18 @@ var RULES = [
|
|
|
2460
2515
|
tools: ["Read", "Bash"],
|
|
2461
2516
|
baseSeverity: "high",
|
|
2462
2517
|
test(ctx) {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2518
|
+
if (isBash(ctx.toolName)) {
|
|
2519
|
+
const cmd = ctx.commandBare;
|
|
2520
|
+
if (!cmd)
|
|
2521
|
+
return null;
|
|
2522
|
+
const segments = cmd.split(/\|\||&&|[|;\n]/);
|
|
2523
|
+
const reads = segments.some((seg) => SECRET_FILE.test(seg) && !EXAMPLE_FILE.test(seg) && CONTENT_READ_CMD.test(seg));
|
|
2524
|
+
return reads ? { evidence: evidence(ctx.command) } : null;
|
|
2525
|
+
}
|
|
2526
|
+
const f = ctx.filePath;
|
|
2527
|
+
if (!f || !SECRET_FILE.test(f) || EXAMPLE_FILE.test(f))
|
|
2467
2528
|
return null;
|
|
2468
|
-
return { evidence: evidence(
|
|
2529
|
+
return { evidence: evidence(f) };
|
|
2469
2530
|
}
|
|
2470
2531
|
},
|
|
2471
2532
|
{
|
|
@@ -2496,36 +2557,43 @@ var RULES = [
|
|
|
2496
2557
|
tools: ["Bash"],
|
|
2497
2558
|
baseSeverity: "critical",
|
|
2498
2559
|
test(ctx) {
|
|
2499
|
-
const c = ctx.
|
|
2560
|
+
const c = ctx.commandBare;
|
|
2500
2561
|
if (!c)
|
|
2501
2562
|
return null;
|
|
2502
2563
|
if (!/(\||<)\s*(sudo\s+)?(curl|wget|nc|ncat|netcat|ssh|scp)\b/i.test(c))
|
|
2503
2564
|
return null;
|
|
2504
2565
|
if (!/\b(cat|tar|zip|gzip|base64|env|printenv|cat\s|<)\b/i.test(c))
|
|
2505
2566
|
return null;
|
|
2506
|
-
return { evidence: evidence(
|
|
2567
|
+
return { evidence: evidence(ctx.command) };
|
|
2507
2568
|
}
|
|
2508
2569
|
},
|
|
2509
2570
|
{
|
|
2510
2571
|
id: "exfil.network_upload",
|
|
2511
2572
|
category: "exfiltration",
|
|
2512
2573
|
framework_ref: "MITRE ATLAS AML.T0086",
|
|
2513
|
-
title: "Uploads data to
|
|
2574
|
+
title: "Uploads data to a network host",
|
|
2514
2575
|
tools: ["Bash"],
|
|
2515
2576
|
baseSeverity: "high",
|
|
2516
2577
|
test(ctx) {
|
|
2517
|
-
const c = ctx.
|
|
2578
|
+
const c = ctx.commandBare;
|
|
2518
2579
|
if (!c)
|
|
2519
2580
|
return null;
|
|
2520
|
-
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)
|
|
2581
|
+
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)/.test(c);
|
|
2521
2582
|
if (!upload)
|
|
2522
2583
|
return null;
|
|
2523
|
-
const external = /https?:\/\/(?!(localhost|127\.0\.0\.1|0\.0\.0\.0))/i.test(c);
|
|
2524
2584
|
const withFile = /(@[^\s'"]+|--upload-file|\s-T\b)/.test(c);
|
|
2585
|
+
const scope = netTargetScope(ctx.command);
|
|
2586
|
+
let severity;
|
|
2587
|
+
if (scope === "external")
|
|
2588
|
+
severity = withFile ? "critical" : "high";
|
|
2589
|
+
else if (scope === "unknown")
|
|
2590
|
+
severity = "high";
|
|
2591
|
+
else
|
|
2592
|
+
severity = withFile ? bumpSeverity(scope === "loopback" ? "info" : "low", 1) : scope === "loopback" ? "info" : "low";
|
|
2525
2593
|
return {
|
|
2526
|
-
evidence: evidence(
|
|
2527
|
-
severity
|
|
2528
|
-
mods: {
|
|
2594
|
+
evidence: evidence(ctx.command),
|
|
2595
|
+
severity,
|
|
2596
|
+
mods: { target_scope: scope, with_file: withFile }
|
|
2529
2597
|
};
|
|
2530
2598
|
}
|
|
2531
2599
|
},
|
|
@@ -2537,12 +2605,12 @@ var RULES = [
|
|
|
2537
2605
|
tools: ["Bash"],
|
|
2538
2606
|
baseSeverity: "critical",
|
|
2539
2607
|
test(ctx) {
|
|
2540
|
-
const c = ctx.
|
|
2608
|
+
const c = ctx.commandBare;
|
|
2541
2609
|
if (!c)
|
|
2542
2610
|
return null;
|
|
2543
2611
|
if (!/\b(nc|ncat|netcat)\b[^|;&\n]*(-e\b|-c\b|\d{2,5})|\/dev\/tcp\/|bash\s+-i\b/i.test(c))
|
|
2544
2612
|
return null;
|
|
2545
|
-
return { evidence: evidence(
|
|
2613
|
+
return { evidence: evidence(ctx.command) };
|
|
2546
2614
|
}
|
|
2547
2615
|
},
|
|
2548
2616
|
// --- Privilege escalation / guardrail bypass (OWASP LLM06 Excessive Agency)
|
|
@@ -2554,10 +2622,10 @@ var RULES = [
|
|
|
2554
2622
|
tools: "*",
|
|
2555
2623
|
baseSeverity: "high",
|
|
2556
2624
|
test(ctx) {
|
|
2557
|
-
const c = ctx.
|
|
2625
|
+
const c = ctx.commandBare;
|
|
2558
2626
|
if (!c || !/--dangerously-skip-permissions\b|--yolo\b|--dangerously-skip\b/i.test(c))
|
|
2559
2627
|
return null;
|
|
2560
|
-
return { evidence: evidence(
|
|
2628
|
+
return { evidence: evidence(ctx.command) };
|
|
2561
2629
|
}
|
|
2562
2630
|
},
|
|
2563
2631
|
{
|
|
@@ -2568,10 +2636,17 @@ var RULES = [
|
|
|
2568
2636
|
tools: ["Bash"],
|
|
2569
2637
|
baseSeverity: "high",
|
|
2570
2638
|
test(ctx) {
|
|
2571
|
-
const c = ctx.
|
|
2572
|
-
if (!c
|
|
2639
|
+
const c = ctx.commandBare;
|
|
2640
|
+
if (!c)
|
|
2573
2641
|
return null;
|
|
2574
|
-
|
|
2642
|
+
const m = /\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(sh|bash|zsh|python\d?|node)\b([^|;&]*)/i.exec(c);
|
|
2643
|
+
if (!m)
|
|
2644
|
+
return null;
|
|
2645
|
+
const interp = m[1].toLowerCase();
|
|
2646
|
+
const args = m[2] || "";
|
|
2647
|
+
if (/^(?:node|python)/.test(interp) && /(?:^|\s)-(?:e|c|m|p)\b|(?:^|\s)--(?:eval|command|module|print)\b/.test(args))
|
|
2648
|
+
return null;
|
|
2649
|
+
return { evidence: evidence(ctx.command) };
|
|
2575
2650
|
}
|
|
2576
2651
|
},
|
|
2577
2652
|
{
|
|
@@ -2583,10 +2658,28 @@ var RULES = [
|
|
|
2583
2658
|
baseSeverity: "high",
|
|
2584
2659
|
// root escalation removes a real safety barrier — higher than an in-project op
|
|
2585
2660
|
test(ctx) {
|
|
2586
|
-
const c = ctx.
|
|
2587
|
-
if (!c || !/(
|
|
2661
|
+
const c = ctx.commandBare;
|
|
2662
|
+
if (!c || !/(?:^|[\n;|&(])\s*sudo\b/i.test(c))
|
|
2663
|
+
return null;
|
|
2664
|
+
return { evidence: evidence(ctx.command) };
|
|
2665
|
+
}
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
id: "privilege.exec_generated_script",
|
|
2669
|
+
category: "privilege-bypass",
|
|
2670
|
+
framework_ref: "OWASP LLM06",
|
|
2671
|
+
title: "Writes a shell script from inline output and executes it",
|
|
2672
|
+
tools: ["Bash"],
|
|
2673
|
+
baseSeverity: "high",
|
|
2674
|
+
test(ctx) {
|
|
2675
|
+
const raw = ctx.command;
|
|
2676
|
+
if (!raw)
|
|
2677
|
+
return null;
|
|
2678
|
+
const file = execGeneratedScript(stripComments(raw));
|
|
2679
|
+
if (!file)
|
|
2588
2680
|
return null;
|
|
2589
|
-
|
|
2681
|
+
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);
|
|
2682
|
+
return { evidence: evidence(raw), severity: critical ? "critical" : "high", mods: { script_file: file } };
|
|
2590
2683
|
}
|
|
2591
2684
|
},
|
|
2592
2685
|
{
|
|
@@ -2597,10 +2690,10 @@ var RULES = [
|
|
|
2597
2690
|
tools: ["Bash"],
|
|
2598
2691
|
baseSeverity: "medium",
|
|
2599
2692
|
test(ctx) {
|
|
2600
|
-
const c = ctx.
|
|
2693
|
+
const c = ctx.commandBare;
|
|
2601
2694
|
if (!c || !/\bchmod\s+(-[a-zA-Z]+\s+)*0?777\b/i.test(c))
|
|
2602
2695
|
return null;
|
|
2603
|
-
return { evidence: evidence(
|
|
2696
|
+
return { evidence: evidence(ctx.command) };
|
|
2604
2697
|
}
|
|
2605
2698
|
},
|
|
2606
2699
|
{
|
|
@@ -2611,12 +2704,12 @@ var RULES = [
|
|
|
2611
2704
|
tools: ["Bash"],
|
|
2612
2705
|
baseSeverity: "high",
|
|
2613
2706
|
test(ctx) {
|
|
2614
|
-
const c = ctx.
|
|
2707
|
+
const c = ctx.commandBare;
|
|
2615
2708
|
if (!c)
|
|
2616
2709
|
return null;
|
|
2617
2710
|
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
2711
|
return null;
|
|
2619
|
-
return { evidence: evidence(
|
|
2712
|
+
return { evidence: evidence(ctx.command) };
|
|
2620
2713
|
}
|
|
2621
2714
|
},
|
|
2622
2715
|
{
|
|
@@ -2625,20 +2718,21 @@ var RULES = [
|
|
|
2625
2718
|
framework_ref: "OWASP LLM06",
|
|
2626
2719
|
title: "Writes a file outside the working project",
|
|
2627
2720
|
tools: ["Write", "Edit", "MultiEdit"],
|
|
2628
|
-
baseSeverity: "
|
|
2721
|
+
baseSeverity: "low",
|
|
2722
|
+
// writing to /tmp, scratch dirs, adjacent repos is routine; only a system path (below) is high
|
|
2629
2723
|
test(ctx) {
|
|
2630
2724
|
const f = ctx.filePath;
|
|
2631
|
-
if (!outsideProject(f, ctx.projectPath))
|
|
2725
|
+
if (!outsideProject(f, ctx.projectPath, ctx.ownedConfigDirs))
|
|
2632
2726
|
return null;
|
|
2633
2727
|
const system = !!f && SYSTEM_PATH.test(f);
|
|
2634
|
-
return { evidence: evidence(f), severity: system ? "high" : "
|
|
2728
|
+
return { evidence: evidence(f), severity: system ? "high" : "low", mods: { project: ctx.projectPath, system_path: system } };
|
|
2635
2729
|
}
|
|
2636
2730
|
}
|
|
2637
2731
|
];
|
|
2638
2732
|
function appliesTo(rule, toolName) {
|
|
2639
2733
|
return rule.tools === "*" || rule.tools.includes(toolName);
|
|
2640
2734
|
}
|
|
2641
|
-
function buildContext(row, projectPath) {
|
|
2735
|
+
function buildContext(row, projectPath, ownedConfigDirs) {
|
|
2642
2736
|
let input = null;
|
|
2643
2737
|
try {
|
|
2644
2738
|
input = row.input_json ? JSON.parse(row.input_json) : null;
|
|
@@ -2646,8 +2740,10 @@ function buildContext(row, projectPath) {
|
|
|
2646
2740
|
input = null;
|
|
2647
2741
|
}
|
|
2648
2742
|
const command = typeof input?.command === "string" ? input.command : null;
|
|
2743
|
+
const commandCode = command != null ? codeOf(command) : null;
|
|
2744
|
+
const commandBare = command != null ? bareOf(command) : null;
|
|
2649
2745
|
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 };
|
|
2746
|
+
return { toolName: row.tool_name, command, commandCode, commandBare, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath, ownedConfigDirs };
|
|
2651
2747
|
}
|
|
2652
2748
|
function detect(db, dirty) {
|
|
2653
2749
|
const incremental = dirty != null;
|
|
@@ -2661,6 +2757,7 @@ function detect(db, dirty) {
|
|
|
2661
2757
|
})(dirty);
|
|
2662
2758
|
}
|
|
2663
2759
|
const scope = incremental ? " WHERE tc.session_id IN (SELECT id FROM _dirty_sec)" : "";
|
|
2760
|
+
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
2761
|
const projPath = /* @__PURE__ */ new Map();
|
|
2665
2762
|
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
2763
|
projPath.set(r.id, r.path);
|
|
@@ -2674,7 +2771,7 @@ function detect(db, dirty) {
|
|
|
2674
2771
|
const tx = db.transaction(() => {
|
|
2675
2772
|
db.exec(delScope);
|
|
2676
2773
|
for (const row of rows) {
|
|
2677
|
-
const ctx = buildContext(row, projPath.get(row.session_id) ?? null);
|
|
2774
|
+
const ctx = buildContext(row, projPath.get(row.session_id) ?? null, ownedConfigDirs);
|
|
2678
2775
|
for (const rule of RULES) {
|
|
2679
2776
|
if (!appliesTo(rule, ctx.toolName))
|
|
2680
2777
|
continue;
|