@shomra/agent 0.3.17 → 0.3.18

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 (156) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +57 -57
  3. package/package.json +3 -9
  4. package/shomra.mjs +9 -7168
  5. package/src/agents/hook-command.mjs +19 -0
  6. package/src/agents/hook-files.mjs +41 -0
  7. package/src/agents/installers.mjs +203 -0
  8. package/src/artifacts/matchers.mjs +59 -0
  9. package/src/artifacts/report.mjs +50 -0
  10. package/src/cli/flags.mjs +68 -0
  11. package/src/cli/help-sections.mjs +309 -0
  12. package/src/cli/help.mjs +27 -0
  13. package/src/cli/main.mjs +55 -0
  14. package/src/cli/registry.mjs +80 -0
  15. package/src/cli/suggestions.mjs +33 -0
  16. package/src/commands/add.mjs +149 -0
  17. package/src/commands/agent-identity.mjs +46 -0
  18. package/src/commands/check.mjs +194 -0
  19. package/src/commands/corpus.mjs +126 -0
  20. package/src/commands/design.mjs +168 -0
  21. package/src/commands/doctor.mjs +209 -0
  22. package/src/commands/fix.mjs +115 -0
  23. package/src/commands/gate.mjs +154 -0
  24. package/src/commands/git-hooks.mjs +163 -0
  25. package/src/commands/init.mjs +36 -0
  26. package/src/commands/install-hook.mjs +51 -0
  27. package/src/commands/llm-proxy.mjs +153 -0
  28. package/src/commands/mcp-add.mjs +185 -0
  29. package/src/commands/mcp.mjs +143 -0
  30. package/src/commands/memory-scan.mjs +181 -0
  31. package/src/commands/model-scan.mjs +99 -0
  32. package/src/commands/models.mjs +145 -0
  33. package/src/commands/new.mjs +64 -0
  34. package/src/commands/plan.mjs +87 -0
  35. package/src/commands/pr.mjs +249 -0
  36. package/src/commands/protect.mjs +38 -0
  37. package/src/commands/provenance.mjs +91 -0
  38. package/src/commands/redteam.mjs +166 -0
  39. package/src/commands/rules.mjs +220 -0
  40. package/src/commands/run.mjs +128 -0
  41. package/src/commands/scan-zip.mjs +118 -0
  42. package/src/commands/scan.mjs +102 -0
  43. package/src/commands/secrets.mjs +99 -0
  44. package/src/commands/status.mjs +50 -0
  45. package/src/commands/why.mjs +88 -0
  46. package/src/core/api-client.mjs +66 -0
  47. package/src/core/api-key.mjs +6 -0
  48. package/src/core/circuit-breaker.mjs +42 -0
  49. package/src/core/config.mjs +37 -0
  50. package/src/core/exit-codes.mjs +9 -0
  51. package/src/core/json-file.mjs +13 -0
  52. package/src/core/numbers.mjs +4 -0
  53. package/src/core/package-root.mjs +10 -0
  54. package/src/core/terminal.mjs +16 -0
  55. package/src/core/version.mjs +14 -0
  56. package/src/core/wire-limits.mjs +53 -0
  57. package/src/corpus/screening.mjs +127 -0
  58. package/{ai-usage.mjs → src/detect/ai-usage.mjs} +0 -27
  59. package/src/detect/code-sast.mjs +2 -0
  60. package/{design.mjs → src/detect/design.mjs} +17 -106
  61. package/src/detect/guard-signals.mjs +18 -0
  62. package/{model-refs.mjs → src/detect/model-refs.mjs} +18 -77
  63. package/src/detect/sast/chains.mjs +30 -0
  64. package/src/detect/sast/path-expressions.mjs +76 -0
  65. package/src/detect/sast/rules-chains.mjs +33 -0
  66. package/src/detect/sast/rules-config.mjs +51 -0
  67. package/src/detect/sast/rules-javascript.mjs +109 -0
  68. package/src/detect/sast/rules-python.mjs +292 -0
  69. package/src/detect/sast/scanner.mjs +104 -0
  70. package/src/detect/sast/source-lines.mjs +115 -0
  71. package/src/detect/sast/taint.mjs +71 -0
  72. package/src/detect/signals/artifacts.mjs +113 -0
  73. package/src/detect/signals/autonomy.mjs +55 -0
  74. package/src/detect/signals/config-markers.mjs +28 -0
  75. package/src/detect/signals/credential-harvest.mjs +64 -0
  76. package/src/detect/signals/durable-claims.mjs +73 -0
  77. package/src/detect/signals/egress.mjs +56 -0
  78. package/src/detect/signals/execution-hijack.mjs +128 -0
  79. package/src/detect/signals/gate.mjs +91 -0
  80. package/src/detect/signals/injection.mjs +55 -0
  81. package/src/detect/signals/lines.mjs +42 -0
  82. package/src/detect/signals/masking.mjs +99 -0
  83. package/src/detect/signals/memory.mjs +357 -0
  84. package/src/detect/signals/packages.mjs +45 -0
  85. package/src/detect/signals/propagation.mjs +86 -0
  86. package/src/detect/signals/prose-context.mjs +82 -0
  87. package/src/detect/signals/scan.mjs +91 -0
  88. package/src/detect/signals/secrets.mjs +85 -0
  89. package/src/detect/signals/sensitive.mjs +9 -0
  90. package/src/detect/signals/severity.mjs +10 -0
  91. package/src/detect/signals/shell.mjs +96 -0
  92. package/src/detect/signals/staged-fetch.mjs +66 -0
  93. package/src/detect/signals/text-match.mjs +35 -0
  94. package/src/gate/batch.mjs +157 -0
  95. package/src/gate/environment.mjs +122 -0
  96. package/src/gate/repo-policy.mjs +65 -0
  97. package/src/gate/result.mjs +53 -0
  98. package/src/gate/sarif.mjs +33 -0
  99. package/src/gate/sast.mjs +64 -0
  100. package/src/gate/suppressions.mjs +0 -0
  101. package/src/guard/classify.mjs +50 -0
  102. package/src/guard/emit.mjs +51 -0
  103. package/src/guard/ignore.mjs +24 -0
  104. package/src/guard/ledger.mjs +112 -0
  105. package/src/guard/model-load.mjs +50 -0
  106. package/src/guard/normalize.mjs +77 -0
  107. package/src/guard/options.mjs +10 -0
  108. package/src/guard/prompt-guard.mjs +184 -0
  109. package/src/guard/report.mjs +35 -0
  110. package/src/guard/result-guard.mjs +140 -0
  111. package/src/guard/tool-guard.mjs +166 -0
  112. package/src/inventory/agent-artifacts.mjs +5 -0
  113. package/src/inventory/agent-posture.mjs +249 -0
  114. package/src/inventory/artifacts/classify.mjs +27 -0
  115. package/src/inventory/artifacts/discover.mjs +187 -0
  116. package/src/inventory/artifacts/file-read.mjs +42 -0
  117. package/src/inventory/artifacts/hooks.mjs +14 -0
  118. package/src/inventory/artifacts/limits.mjs +37 -0
  119. package/src/inventory/artifacts/marketplaces.mjs +45 -0
  120. package/src/inventory/artifacts/roots.mjs +20 -0
  121. package/src/inventory/artifacts/walk.mjs +36 -0
  122. package/src/inventory/discovery/ai-dependencies.mjs +161 -0
  123. package/src/inventory/discovery/ai-tools.mjs +23 -0
  124. package/src/inventory/discovery/all.mjs +40 -0
  125. package/src/inventory/discovery/coding-agents.mjs +77 -0
  126. package/src/inventory/discovery/fs-read.mjs +36 -0
  127. package/src/inventory/discovery/local-runtimes.mjs +53 -0
  128. package/src/inventory/discovery/mcp-clients.mjs +67 -0
  129. package/src/inventory/discovery/mcp-servers.mjs +78 -0
  130. package/src/inventory/discovery/model-keys.mjs +97 -0
  131. package/src/inventory/discovery/platform.mjs +16 -0
  132. package/src/inventory/discovery/rules-files.mjs +25 -0
  133. package/src/inventory/discovery/vector-stores.mjs +176 -0
  134. package/src/inventory/discovery/workspace.mjs +124 -0
  135. package/src/inventory/discovery.mjs +10 -0
  136. package/src/mcp/child-process.mjs +50 -0
  137. package/src/mcp/config-wrapping.mjs +75 -0
  138. package/src/mcp/connect-gate.mjs +45 -0
  139. package/src/mcp/hosts.mjs +16 -0
  140. package/src/mcp/jsonrpc.mjs +48 -0
  141. package/src/mcp/lookup.mjs +50 -0
  142. package/src/mcp/screening.mjs +103 -0
  143. package/src/mcp/server-tools.mjs +97 -0
  144. package/src/mcp/server.mjs +102 -0
  145. package/src/mcp/shim.mjs +205 -0
  146. package/src/models/lookup.mjs +79 -0
  147. package/src/models/references.mjs +103 -0
  148. package/src/rules/context.mjs +98 -0
  149. package/src/rules/generate.mjs +103 -0
  150. package/src/rules/sections.mjs +145 -0
  151. package/src/scaffold/agent-project.mjs +185 -0
  152. package/src/scaffold/artifact-templates.mjs +35 -0
  153. package/code-sast.mjs +0 -1063
  154. package/discovery.mjs +0 -977
  155. package/guard-ledger.mjs +0 -239
  156. package/guard-signals.mjs +0 -2055
@@ -0,0 +1,45 @@
1
+ export const INSTALL_LURE = [
2
+ { name: 'Instructs downloading an executable/archive to run', re: /\b(download|install|fetch|grab|extract)\b[^\n]{0,180}\.(zip|exe|dmg|pkg|msi|bin|appimage|jar|scr|apk|deb|rpm|tar\.gz|tgz)\b/i, severity: 'MEDIUM' },
3
+ { name: 'Password-protected archive (evades AV / scanners)', re: /\b(extract|unzip|decompress|archive|zip|password)\b[^\n]{0,50}\b(pass(word|phrase)?|pwd)\s*[:=]\s*\S/i, severity: 'HIGH' },
4
+ { name: 'Coercion: claims a helper is required before the task works', re: /\b(required to (function|work|deploy|run)|will not (work|function|run)( correctly| properly)?( without)?|does not work without|otherwise it is impossible|cannot [a-z ]{0,24} without (installing|running)|must (be )?(install(ed)?|run) (this |the )?)/i, severity: 'MEDIUM' },
5
+ { name: 'Coercion: re-run / retry until it succeeds', re: /\b(re-?run (if needed|until|the command)|run (it |the command )?again|try again after)/i, severity: 'LOW' },
6
+ ];
7
+
8
+ export const MALICIOUS_PACKAGE_SEED = new Set([
9
+ 'event-stream', 'eslint-scope-malware', 'electron-native-notify', 'rc-malware',
10
+ 'crossenv', 'mongose', 'expresss',
11
+ ]);
12
+
13
+ export const POPULAR_PACKAGES = [
14
+ 'express', 'react', 'lodash', 'axios', 'chalk', 'commander',
15
+ 'mongoose', 'cross-env', 'dotenv', 'request', 'puppeteer', 'playwright',
16
+ ];
17
+
18
+ export function editDistance(a, b) {
19
+ const m = a.length, n = b.length;
20
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
21
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
22
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
23
+ for (let i = 1; i <= m; i++)
24
+ for (let j = 1; j <= n; j++) {
25
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
26
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
27
+ }
28
+ return dp[m][n];
29
+ }
30
+
31
+ export function packageFromCommand(command, args) {
32
+ const tokens = [command, ...(args ?? [])].filter(Boolean).map(String);
33
+ if (!tokens.length) return null;
34
+ const runners = new Set(['npx', 'npm', 'pnpm', 'yarn', 'bunx', 'bun']);
35
+ const skips = new Set(['exec', 'dlx', 'run', 'install', 'add', 'create', '-y', '--yes']);
36
+ const start = runners.has(tokens[0].split('/').pop() ?? tokens[0]) ? 1 : -1;
37
+ if (start === -1) return null;
38
+ for (let i = start; i < tokens.length; i++) {
39
+ const t = tokens[i];
40
+ if (t.startsWith('-') || skips.has(t)) continue;
41
+ const name = t.startsWith('@') ? t.split('/').slice(0, 2).join('/') : t.split('@')[0];
42
+ return name.replace(/@[\d^~].*$/, '');
43
+ }
44
+ return null;
45
+ }
@@ -0,0 +1,86 @@
1
+ import { isDocumentationLine } from './prose-context.mjs';
2
+
3
+ export const AGENT_ROOT_RE =
4
+ /(^|\/)\.(claude|claude-plugin|cursor|continue|codeium|windsurf|aider|cline|roo|zed|codex|gemini|goose|kilocode|trae|junie|amazonq|mem0|letta|memgpt|opencode|crush|augment|kiro|qoder|factory|devin|antigravity|qwen|openhands|specstory|copilot)(\/)|(^|\/)\.github\/(agents|instructions|prompts|chatmodes)(\/)/i;
5
+
6
+ export const isAgentAdjacentPath = (p) => AGENT_ROOT_RE.test(String(p ?? '').replace(/\\/g, '/'));
7
+
8
+ const ARTIFACT_BASENAME_RE =
9
+ /(?:\b(?:SKILL\.md|AGENTS?\.md|CLAUDE\.md|GEMINI\.md|settings(?:\.local)?\.json|claude_desktop_config\.json|mcp[_-]?settings\.json|[\w-]{1,64}\.mdc)|\.cursorrules|\.windsurfrules)\b/i;
10
+
11
+ const PROP_WRITE_VERB_RE =
12
+ /\b(write|writes|writing|create|creates|creating|recreate|recreates|restore|restores|reinstall|reinstalls|add|adds|adding|append|appends|appending|install|installs|installing|copy|copies|copying|save|saves|saving|drop|drops|place|places|generate|generates|scaffold|scaffolds|overwrite|overwrites|patch|patches|update|updates|cp|mv|tee|mkdir)\b|(?:^|[\s"'`])>>?\s*['"`]?[\w./~$-]/i;
13
+
14
+ const PROP_IMPERATIVE_RE =
15
+ /(?:^|\n)\s*(?:[-*+]\s+|\d+[.)]\s+|\$\s+)?(?:then\s+|first\s+|now\s+|also\s+|to\s+\w+,\s*)?(write|create|recreate|restore|reinstall|add|append|install|copy|save|place|generate|scaffold|overwrite|patch|drop|echo|cat|cp|mv|tee|mkdir|printf)\b/i;
16
+
17
+ const PROP_SHELL_WRITE_RE = /(?:^|[\s"'`])>>?\s*['"`]?[~.$/\w-]|\b(?:tee|cp|mv|install)\s+[-\w./~$]+\s+[-\w./~$]|\bmkdir\s+-p\b/;
18
+
19
+ const PROP_FETCH_RE = /\b(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod|fetch|http\.get|requests\.get|urllib)\b|\bhttps?:\/\//i;
20
+
21
+ const PROP_CONCEAL_RE =
22
+ /\b(do not (?:mention|tell|report|log|show|disclose|reveal)|don'?t (?:mention|tell|report|log|show)|without (?:telling|informing|notifying|mentioning)|silently|quietly|no need to (?:mention|report|tell)|hide (?:this|it)|keep (?:this|it) (?:secret|hidden|between)|remove this (?:line|section|note) (?:after|once)|delete this (?:file|note) (?:after|once))\b/i;
23
+
24
+ const PROP_BREADTH_RE =
25
+ /\b(?:every (?:project|repo(?:sitory)?|workspace|machine|checkout)|each (?:project|repo(?:sitory)?|workspace)|all (?:projects|repos(?:itories)?|workspaces)|globally|system[- ]wide)\b|~\/\.[a-z]|\$HOME\/\.[a-z]/i;
26
+
27
+ const PROP_RESTORE_RE =
28
+ /\b(restore|recreate|re-?add|re-?install|put (?:this|it) back|if (?:this|it) (?:is |has been )?(?:deleted|removed|missing)|should (?:this|it) (?:be )?(?:deleted|removed)|ensure (?:this|it) (?:still )?exists)\b/i;
29
+
30
+ const PROP_PATH_RE = /(?:^|[\s'"`(=|;&:])((?:~\/|\.{0,2}\/)?(?:[\w.@$-]+\/)+[\w.@$-]+(?:\.\w+)?)/g;
31
+
32
+ const trimPropTarget = (t) => String(t).replace(/[.,;:!?)\]}'"`]{1,8}$/, '');
33
+
34
+ function propPathIn(line) {
35
+ PROP_PATH_RE.lastIndex = 0;
36
+ for (const m of line.matchAll(PROP_PATH_RE)) {
37
+ const p = trimPropTarget(m[1] ?? '');
38
+ if (p && isAgentAdjacentPath(p) && /\.[a-z0-9]{1,8}$/i.test(p)) return p;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export function localPropagation(content, { path = '', kind } = {}) {
44
+ const body = String(content ?? '');
45
+ if (!body.trim()) return [];
46
+ const selfPath = String(path ?? '').replace(/\\/g, '/');
47
+ const autoRun = kind === 'hook';
48
+ const out = [];
49
+ const lines = body.split(/\r?\n/).slice(0, 4000);
50
+
51
+ for (let i = 0; i < lines.length && out.length < 12; i++) {
52
+ const line = lines[i];
53
+ if (line.length > 2000) continue;
54
+ const m = ARTIFACT_BASENAME_RE.exec(line);
55
+ const agentPath = propPathIn(line);
56
+ if (!m && !agentPath) continue;
57
+ if (!PROP_WRITE_VERB_RE.test(line)) continue;
58
+ if (!PROP_IMPERATIVE_RE.test(line) && !PROP_SHELL_WRITE_RE.test(line) && isDocumentationLine(line)) continue;
59
+
60
+ const target = trimPropTarget(agentPath ?? m[0]);
61
+ const t = target.replace(/^[.~]?\//, '');
62
+ const self = !!selfPath && (selfPath.endsWith(t) || t.endsWith(selfPath));
63
+
64
+ const amplifiers = [];
65
+ if (autoRun) amplifiers.push('auto-run');
66
+ if (PROP_FETCH_RE.test(line)) amplifiers.push('remote-content');
67
+ if (PROP_CONCEAL_RE.test(line) || PROP_CONCEAL_RE.test(lines.slice(Math.max(0, i - 1), i + 2).join(' '))) amplifiers.push('concealment');
68
+ if (PROP_BREADTH_RE.test(line) || (self && PROP_RESTORE_RE.test(line))) amplifiers.push('breadth');
69
+
70
+ const severity = amplifiers.includes('concealment') || amplifiers.length >= 2 ? 'CRITICAL' : amplifiers.length === 1 ? 'HIGH' : 'MEDIUM';
71
+ out.push({
72
+ severity,
73
+ target,
74
+ amplifiers,
75
+ line: i + 1,
76
+ title: self
77
+ ? `Artifact restores itself (${target})`
78
+ : `Artifact writes another agent artifact (${target})`,
79
+ remediationText: self
80
+ ? 'Removing the file is not enough - the instruction to restore it travels with it. Check every location it names for a copy.'
81
+ : `Confirm that writing ${target} is this artifact's stated purpose, and pin what it emits to a reviewed template rather than to content decided at run time.`,
82
+ });
83
+ }
84
+ const RANK = { MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
85
+ return out.sort((a, b) => RANK[b.severity] - RANK[a.severity]);
86
+ }
@@ -0,0 +1,82 @@
1
+ import { egressHost } from './egress.mjs';
2
+ import { IMPERATIVE } from './memory.mjs';
3
+
4
+ const DESCRIPTIVE_MARKERS =
5
+ /\b(detect|scan|flag|block|catch|prevent|guard|protect|harden|audit|benchmark|catalog|scenario|corpus|coverage|example|vector|signal|rule|technique|posture|detection|test\s*case|red[- ]?team|-style|grounded in|fixed|now green|was|were|had|used to|previously|postmortem|regression|changelog|root[- ]?cause|repro|note|see|describes?|documents?|refers?|treat(s|ed|ing)?|counts?|reads?)\w*/i;
6
+
7
+ export function isDescriptiveLine(line) {
8
+ return DESCRIPTIVE_MARKERS.test(line) && !IMPERATIVE.test(line);
9
+ }
10
+
11
+ const RESEARCH_CITATION_RE =
12
+ /\b(?:in\s+their\s+(?:\d{4}\s+)?paper|et\s+al\.|we\s+(?:analys|analyz|studi|examin|evaluat|benchmark|review|investigat)\w*|(?:this|the)\s+(?:paper|study|report|article|post|research|survey|technique|attack|jailbreak)\b|according\s+to\s+(?:researchers|the\s+authors)|characteriz\w+\s+(?:and\s+)?evaluat\w+|published\s+(?:in|by)\b|\barxiv\b|\bCVE-\d{4}-|\bis\s+a\s+(?:critical\s+|active\s+|growing\s+)?(?:research|study)\s+(?:area|topic|field)|\b(?:the\s+)?ethics\s+of\b)/i;
13
+
14
+ const ATTACK_NAMING_RE = /(?:[Tt]his|[Tt]he)\s+(?:[A-Z][\w.-]{1,24}\s+){1,3}(?:attack|jailbreak|technique|exploit|payload)\b/;
15
+
16
+ const ATTACK_CHARACTERISATION_RE =
17
+ /\bis\s+a\s+(?:well[-\s]documented|well[-\s]known|widely[-\s]known|classic|common|known|documented)\s+(?:attack|technique|jailbreak|pattern|exploit|vector)\b/i;
18
+
19
+ const CITATION_HANDOFF_RE = /[:;\u2014\u2013]\s*$/;
20
+
21
+ const CITATION_FRAMES = [RESEARCH_CITATION_RE, ATTACK_NAMING_RE, ATTACK_CHARACTERISATION_RE];
22
+
23
+ export function citationGoverns(segment, offset) {
24
+ const text = String(segment ?? '');
25
+
26
+ for (const re of CITATION_FRAMES) {
27
+ const cit = text.match(re);
28
+ if (!cit) continue;
29
+ if (offset == null) return true;
30
+ const citEnd = (cit.index ?? 0) + cit[0].length;
31
+ if (citEnd > offset) continue;
32
+ if (!CITATION_HANDOFF_RE.test(text.slice(citEnd, offset))) return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ const ELLIPSIS_RE = /…|\.\.\./;
38
+
39
+ const REGEX_PATTERN_RE = /\\[sdwbSDWB]|\\\+|\\\*|\\\(|\\\||\(\?:|\.\*|\.\+/;
40
+
41
+ const CREDENTIAL_PATH_RE =
42
+ /~\/\.(ssh|aws|kube|gnupg|docker|npmrc?)\b|\bid_(rsa|ed25519|dsa)\b|\.pem\b|\bcredentials\b\s*(file)?|\bAWS_SECRET|\bANTHROPIC_API_KEY\b|\bOPENAI_API_KEY\b/i;
43
+
44
+ const EXECUTABLE_FETCH_RE =
45
+ /\b(?:curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n]{0,200}?(?:https?:\/\/|\bwww\.|\b\d{1,3}(?:\.\d{1,3}){3}\b)[^\n]{0,200}?\|\s*(?:sudo\s+)?(?:(?:ba|z|k|da)?sh|python\d?|perl|ruby|node)\b/i;
46
+
47
+ function carriesHardEvidence(line) {
48
+ return CREDENTIAL_PATH_RE.test(line) || EXECUTABLE_FETCH_RE.test(line) || !!egressHost(line);
49
+ }
50
+
51
+ export function isDocumentationLine(line) {
52
+ if (!line) return false;
53
+ if (carriesHardEvidence(line)) return false;
54
+ if (ELLIPSIS_RE.test(line) || REGEX_PATTERN_RE.test(line)) return true;
55
+ return isDescriptiveLine(line);
56
+ }
57
+
58
+ const PROHIBITION_MARKER_RE =
59
+ /\b(?:never|do not|don'?t|cannot|can'?t|must not|mustn'?t|should not|shouldn'?t|avoid|avoids|avoiding|refuse to|refrain from|forbidden|prohibited|disallow\w*|instead of|rather than|beware of)\b[^.:;\n]{0,60}$/i;
60
+
61
+ const DOUBLE_NEGATIVE_RE = /\b(?:hesitate|worry|be afraid|forget|fail|neglect|shy away)\b/i;
62
+
63
+ const COORDINATE_TAIL_RE = /(?:\b(?:and|or|but|then|also)\b|[,;])\s*$/i;
64
+
65
+ export function prohibitsAt(line, offset) {
66
+ if (!line) return false;
67
+ const at = offset == null || offset < 0 ? line.length : Math.min(offset, line.length);
68
+ const before = line.slice(Math.max(0, at - 90), at);
69
+ if (!PROHIBITION_MARKER_RE.test(before)) return false;
70
+ if (COORDINATE_TAIL_RE.test(before)) return false;
71
+ return !DOUBLE_NEGATIVE_RE.test(before);
72
+ }
73
+
74
+ const RISK_CELL_RE = /\b(?:critical|high|medium|low|severity|risk|danger\w*|forbidden|blocked|denied|prohibited|never|do not|example|attack|threat|mitigation|why|impact)\b/i;
75
+
76
+ export function isRiskTableRow(line) {
77
+ const t = String(line ?? '').trim();
78
+ if (!t.startsWith('|') || !t.endsWith('|')) return false;
79
+ const cells = t.slice(1, -1).split('|');
80
+ if (cells.length < 3) return false;
81
+ return cells.some((c) => RISK_CELL_RE.test(c));
82
+ }
@@ -0,0 +1,91 @@
1
+ import { RISKY_CONFIG_MARKERS, riskyConfigHit } from './config-markers.mjs';
2
+ import { detectCredentialHarvest } from './credential-harvest.mjs';
3
+ import { egressHost } from './egress.mjs';
4
+ import { detectExecutionHijack } from './execution-hijack.mjs';
5
+ import { BUILD_ARTIFACT, INJECTION_PHRASES, INJECTION_REGEXES, INVISIBLE_CHARS_RE, PRECEDING_NEGATION, describesRatherThanInstructs } from './injection.mjs';
6
+ import { lineAt, locate } from './lines.mjs';
7
+ import { codeMask, deobfuscate } from './masking.mjs';
8
+ import { PII_PATTERNS, RESERVED_IPV4, SECRET_PATTERNS, VERSION_CONTEXT, isPlaceholderSecret, luhnValid } from './secrets.mjs';
9
+ import { SEV_RANK } from './severity.mjs';
10
+ import { DANGEROUS_SHELL, matchesShellSignal } from './shell.mjs';
11
+ import { scanStagedFetchExec } from './staged-fetch.mjs';
12
+
13
+ export function localScan(text, opts = {}) {
14
+ const findings = [];
15
+ const t = text || '';
16
+ const cats = opts.categories ?? ['shell', 'injection', 'secret', 'config', 'egress'];
17
+ const mask = codeMask(t);
18
+
19
+ if (cats.includes('shell')) {
20
+ const aug = deobfuscate(t);
21
+ if (aug.decodedPayload) findings.push({ label: 'Encoded shell / RCE payload (base64, hex, percent or char-code)', severity: 'CRITICAL', category: 'shell' });
22
+ for (const sig of DANGEROUS_SHELL) if (matchesShellSignal(sig, aug.text)) findings.push({ label: sig.name, severity: sig.severity, category: 'shell', ...locate(t, sig.re, mask) });
23
+ for (const sig of scanStagedFetchExec(aug.text)) findings.push({ label: sig.name, severity: sig.severity, category: 'shell', ...locate(t, sig.re, mask) });
24
+ for (const h of detectExecutionHijack(aug.text))
25
+ findings.push({ label: `Installs an execution hook that governs ${h.governs} (${h.key})`, severity: h.severity, category: 'shell' });
26
+ for (const c of detectCredentialHarvest(aug.text))
27
+ findings.push({ label: `${c.label} - credential harvest`, severity: c.severity, category: 'shell' });
28
+ }
29
+ if (cats.includes('injection')) {
30
+ const low = t.toLowerCase();
31
+
32
+ for (const p of INJECTION_PHRASES) {
33
+ const at = low.indexOf(p);
34
+ if (at < 0) continue;
35
+ if (PRECEDING_NEGATION.test(t.slice(Math.max(0, at - 20), at))) continue;
36
+ findings.push({ label: `Injected instruction: "${p}"`, severity: 'HIGH', category: 'injection', ...locate(t, p, mask) });
37
+ break;
38
+ }
39
+ for (const { label, re, moodGuarded } of INJECTION_REGEXES) {
40
+ const m = t.match(re);
41
+ if (!m) continue;
42
+ const at = m.index ?? 0;
43
+ if (PRECEDING_NEGATION.test(t.slice(Math.max(0, at - 20), at))) continue;
44
+ if (label === 'Bulk destructive command' && BUILD_ARTIFACT.test(m[0])) continue;
45
+ if (moodGuarded && describesRatherThanInstructs(t, at)) continue;
46
+ findings.push({ label, severity: 'HIGH', category: 'injection', ...locate(t, re, mask) });
47
+ }
48
+ if (INVISIBLE_CHARS_RE.test(t)) findings.push({ label: 'Invisible / zero-width characters', severity: 'MEDIUM', category: 'injection', ...locate(t, INVISIBLE_CHARS_RE, mask) });
49
+ }
50
+ if (cats.includes('secret')) {
51
+ for (const { name, re } of SECRET_PATTERNS) { const m = t.match(re); if (m && !isPlaceholderSecret(m[0])) findings.push({ label: `Live credential: ${name}`, severity: 'CRITICAL', category: 'secret', ...locate(t, re, mask) }); }
52
+ }
53
+ if (cats.includes('pii')) {
54
+ for (const { name, re } of PII_PATTERNS) {
55
+ const m = t.match(re);
56
+ if (!m) continue;
57
+ if (name === 'Credit card number' && !luhnValid(m[0])) continue;
58
+
59
+ if (name === 'IPv4 address') {
60
+ if (RESERVED_IPV4.test(m[0])) continue;
61
+ if (VERSION_CONTEXT.test(t.slice(Math.max(0, (m.index ?? 0) - 12), m.index ?? 0))) continue;
62
+ }
63
+
64
+ if (name === 'Phone number' && /^\d+$/.test(m[0])) continue;
65
+ findings.push({ label: `Personal data: ${name}`, severity: 'MEDIUM', category: 'pii', ...locate(t, re, mask) });
66
+ }
67
+ }
68
+ if (cats.includes('config')) {
69
+
70
+ for (const m of RISKY_CONFIG_MARKERS) {
71
+ const hit = riskyConfigHit(t, m);
72
+ if (hit) {
73
+ findings.push({ label: `Risky setting: "${m}"`, severity: 'MEDIUM', category: 'config', line: lineAt(t, hit.start), codeContext: mask[hit.start] === 1 });
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ if (cats.includes('egress')) {
79
+ const h = egressHost(t);
80
+ if (h) findings.push({ label: `Exfiltration sink host: ${h}`, severity: 'HIGH', category: 'egress', ...locate(t, h, mask) });
81
+ }
82
+
83
+ let worstRank = 0, top = null;
84
+ for (const f of findings) if (SEV_RANK[f.severity] > worstRank) { worstRank = SEV_RANK[f.severity]; top = f; }
85
+ const verdict = worstRank >= SEV_RANK.CRITICAL ? 'BLOCK' : worstRank >= SEV_RANK.HIGH ? 'FLAG' : 'ALLOW';
86
+ return { verdict, top, findings };
87
+ }
88
+
89
+ export function downrankCodeContext(findings) {
90
+ return (findings || []).map((f) => (f.codeContext ? { ...f, severity: 'LOW', downranked: true } : f));
91
+ }
@@ -0,0 +1,85 @@
1
+ export const SECRET_PATTERNS = [
2
+
3
+ { name: 'Stripe live key', re: /\bsk_live_[0-9a-zA-Z]{16,}/ },
4
+ { name: 'OpenAI key', re: /\bsk-[A-Za-z0-9]{20,}/ },
5
+ { name: 'AWS access key id', re: /\bAKIA[0-9A-Z]{16}/ },
6
+ { name: 'GitHub token', re: /ghp_[0-9A-Za-z]{20,}/ },
7
+
8
+ { name: 'Anthropic API key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
9
+ { name: 'OpenAI project key', re: /\bsk-proj-[A-Za-z0-9_-]{20,}/ },
10
+ { name: 'Google API key', re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
11
+ { name: 'Hugging Face token', re: /\bhf_[A-Za-z0-9]{30,}/ },
12
+ { name: 'GitLab PAT', re: /\bglpat-[A-Za-z0-9_-]{20,}/ },
13
+ { name: 'npm token', re: /\bnpm_[A-Za-z0-9]{30,}/ },
14
+ { name: 'Slack token', re: /xox[baprs]-[0-9A-Za-z-]{10,}/ },
15
+
16
+ { name: 'AWS secret access key (keyed)', re: /\bAWS_SECRET_ACCESS_KEY\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}\b/ },
17
+
18
+ {
19
+ name: 'Database URL with password',
20
+ re: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/(?!(?:user|username|admin|root|myuser|dbuser):(?:pass|password|passwd|secret|changeme|mypassword|yourpassword|xxx+|123456)@)[^\s:@/]+:[^\s:@/]{4,}@/i,
21
+ },
22
+ { name: 'Generic bearer', re: /bearer\s+[A-Za-z0-9._-]{20,}/i },
23
+ { name: 'Private key block', re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
24
+
25
+ { name: 'Groq API key', re: /\bgsk_[A-Za-z0-9]{40,}/ },
26
+ { name: 'Replicate API token', re: /\br8_[A-Za-z0-9]{30,}/ },
27
+ { name: 'Perplexity API key', re: /\bpplx-[A-Za-z0-9]{32,}/ },
28
+ { name: 'Fireworks API key', re: /\bfw_[A-Za-z0-9]{20,}/ },
29
+ { name: 'xAI API key', re: /\bxai-[A-Za-z0-9]{40,}/ },
30
+ { name: 'LangSmith API key', re: /\blsv2_(?:pt|sk)_[A-Za-z0-9]{24,}_[A-Za-z0-9]{8,}/ },
31
+ { name: 'Pinecone API key', re: /\bpcsk_[A-Za-z0-9_]{30,}/ },
32
+ { name: 'OpenRouter API key', re: /\bsk-or-v1-[A-Za-z0-9]{32,}/ },
33
+ { name: 'DigitalOcean token', re: /\bdop_v1_[a-f0-9]{60,}/ },
34
+ { name: 'Shopify access token', re: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{30,}/ },
35
+ { name: 'GitHub fine-grained PAT', re: /\bgithub_pat_[A-Za-z0-9_]{60,}/ },
36
+ { name: 'GitHub OAuth / refresh / server token', re: /\bgh[osur]_[A-Za-z0-9]{20,}/ },
37
+ { name: 'SendGrid API key', re: /\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{40,}/ },
38
+ { name: 'Slack incoming webhook', re: /\bhooks\.slack\.com\/services\/T[A-Z0-9]{6,}\/B[A-Z0-9]{6,}\/[A-Za-z0-9]{20,}/ },
39
+ { name: 'Discord webhook', re: /\bdiscord(?:app)?\.com\/api\/webhooks\/\d{17,}\/[A-Za-z0-9_-]{40,}/ },
40
+ { name: 'Telegram bot token', re: /\b\d{8,12}:AA[A-Za-z0-9_-]{30,}/ },
41
+ { name: 'Sentry DSN with secret', re: /\bhttps:\/\/[a-f0-9]{32}(?::[a-f0-9]{32})?@[\w.-]*(?:sentry\.io|ingest\.[\w.-]+)\/\d+/ },
42
+ { name: 'Azure storage account key', re: /\bAccountKey\s*=\s*[A-Za-z0-9+/]{60,}={0,2}/ },
43
+ { name: 'JSON web token', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{20,}/ },
44
+ { name: 'Registry auth blob (docker config)', re: /"auth"\s*:\s*"[A-Za-z0-9+/]{24,}={0,2}"/ },
45
+ {
46
+ name: 'Provider API key (named env var)',
47
+ re: /\b(?:AZURE_OPENAI_API_KEY|MISTRAL_API_KEY|COHERE_API_KEY|CO_API_KEY|TOGETHER_API_KEY|DEEPSEEK_API_KEY|DD_API_KEY|DATADOG_API_KEY|TWILIO_AUTH_TOKEN|VERCEL_TOKEN|CLOUDFLARE_API_TOKEN|WEAVIATE_API_KEY|VOYAGE_API_KEY|NVIDIA_API_KEY|CEREBRAS_API_KEY|SAMBANOVA_API_KEY)\s*[=:]\s*["']?[A-Za-z0-9_-]{24,}\b/,
48
+ },
49
+ ];
50
+
51
+ export const PII_PATTERNS = [
52
+ { name: 'Email address', re: /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,24}/ },
53
+ { name: 'US SSN', re: /\b\d{3}-\d{2}-\d{4}\b/ },
54
+ { name: 'Credit card number', re: /\b(?:\d[ -]?){13,16}\b/ },
55
+ { name: 'Phone number', re: /\b(?:\+?1[ .-]?)?\(?\d{3}\)?[ .-]?\d{3}[ .-]?\d{4}\b/ },
56
+ { name: 'IPv4 address', re: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/ },
57
+ ];
58
+
59
+ export const RESERVED_IPV4 = /^(0\.|255\.255\.255\.255|127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|192\.0\.2\.|198\.51\.100\.|203\.0\.113\.|8\.8\.(8\.8|4\.4)|1\.1\.1\.1|1\.0\.0\.1|224\.)/;
60
+
61
+ export const VERSION_CONTEXT = /\b(v|ver|version|release|rev|build|semver|tag)\.?\s*$/i;
62
+
63
+ export function luhnValid(value) {
64
+ const digits = String(value).replace(/[^\d]/g, '');
65
+ if (digits.length < 13 || digits.length > 19) return false;
66
+ let sum = 0, alt = false;
67
+ for (let i = digits.length - 1; i >= 0; i--) {
68
+ let d = parseInt(digits[i], 10);
69
+ if (alt) { d *= 2; if (d > 9) d -= 9; }
70
+ sum += d;
71
+ alt = !alt;
72
+ }
73
+ return sum % 10 === 0;
74
+ }
75
+
76
+ export function isPlaceholderSecret(v) {
77
+ const s = String(v);
78
+ const low = s.toLowerCase();
79
+ if (/(example|sample|placeholder|dummy|redacted|changeme|test[_-]?(key|token|secret)|your[-_]?(key|token|secret|api))/.test(low)) return true;
80
+ if (/(x{6,}|\.{3,}|<[^>]{2,}>|\*{4,}|•{3,})/.test(low)) return true;
81
+ const tail = s.replace(/^\w{1,10}[-_]/, '');
82
+ if (/^(.)\1{7,}/.test(tail)) return true;
83
+ if (/^(0123|1234|abcd|abcdef|deadbeef)/i.test(tail)) return true;
84
+ return false;
85
+ }
@@ -0,0 +1,9 @@
1
+ export const SENSITIVE_READ = [
2
+ 'secret', 'credential', 'password', 'token', 'api_key', 'apikey', 'private_key',
3
+ 'ssh', 'aws', 'env', 'environment', 'keychain', 'vault', 'read_file', 'readfile', 'cat ',
4
+ ];
5
+
6
+ export const NETWORK_VERBS = [
7
+ 'http_request', 'http', 'fetch', 'request', 'curl', 'webhook', 'post', 'send',
8
+ 'upload', 'publish', 'email', 'sendmail', 'smtp',
9
+ ];
@@ -0,0 +1,10 @@
1
+ export const SEV_RANK = { INFO: 1, LOW: 2, MEDIUM: 3, HIGH: 4, CRITICAL: 5 };
2
+
3
+ export function grade(findings) {
4
+ const WEIGHT = { INFO: 2, LOW: 8, MEDIUM: 20, HIGH: 40, CRITICAL: 70 };
5
+ let worstRank = 0;
6
+ for (const f of findings) if (SEV_RANK[f.severity] > worstRank) worstRank = SEV_RANK[f.severity];
7
+ const verdict = worstRank >= SEV_RANK.CRITICAL ? 'BLOCK' : worstRank >= SEV_RANK.HIGH ? 'FLAG' : 'ALLOW';
8
+ const riskScore = Math.min(100, findings.reduce((s, f) => s + (WEIGHT[f.severity] ?? 0), 0));
9
+ return { verdict, riskScore };
10
+ }
@@ -0,0 +1,96 @@
1
+ import { lineTextAt } from './lines.mjs';
2
+
3
+ const EPHEMERAL_RM_TARGET_RE =
4
+ /^(\.\/)?(node_modules|dist|build|out|coverage|target|\.next|\.nuxt|\.turbo|\.svelte-kit|\.cache|\.parcel-cache|__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache|\.tox|venv|\.venv|\.eggs|[\w.-]+\.egg-info)\/?\*?$/i;
5
+
6
+ function rmTargetsRealData(line) {
7
+ const m = /\brm\s+((?:-[a-zA-Z]+\s+)+)(.*)$/.exec(line);
8
+ if (!m) return true;
9
+ const targets = m[2]
10
+ .split(/&&|\|\||[;|>&]/)[0]
11
+ .split(/\s+/)
12
+ .filter((t) => t && !t.startsWith('-'));
13
+ if (!targets.length) return true;
14
+ return !targets.every((t) => EPHEMERAL_RM_TARGET_RE.test(t.replace(/^["']|["']$/g, '')));
15
+ }
16
+
17
+ export function matchesShellSignal(sig, text) {
18
+ if (!sig.refine) return sig.re.test(text);
19
+ const g = new RegExp(sig.re.source, sig.re.flags.includes('g') ? sig.re.flags : sig.re.flags + 'g');
20
+ for (const m of text.matchAll(g)) {
21
+ if (m.index == null) continue;
22
+ if (sig.refine(lineTextAt(text, m.index))) return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ const SENSITIVE_PATH = String.raw`~/\.(?:ssh|aws|kube|gnupg|docker|config/gcloud)|/root/|/etc/(?:shadow|passwd|ssh)|id_[rd]sa|\.pem(?![.\w])|\.env(?![.\w])|credentials|\.npmrc|\.git-credentials|\bsecrets?\b|authorized_keys|\$HOME\b|/home(?:/[\w.-]+)?/?(?=[\s'"]|$)`;
28
+
29
+ export const DANGEROUS_SHELL = [
30
+ { name: 'Pipe-to-shell installer (curl … | sh)', re: /\b(curl|wget)\b[^\n|]{0,200}\|\s*(sudo\s+)?(ba|z|k)?sh\b/i, severity: 'CRITICAL' },
31
+ { name: 'PowerShell download-and-run (iwr/curl … | iex)', re: /\b(iwr|curl|wget|invoke-webrequest|invoke-restmethod|irm)\b[^\n|]{0,200}\|\s*(iex|invoke-expression)\b/i, severity: 'CRITICAL' },
32
+ { name: 'Invoke-Expression of downloaded content', re: /\b(iex|invoke-expression)\b[^\n]{0,120}(downloadstring|net\.webclient|\(\s*(iwr|irm|invoke-)|\$\()/i, severity: 'CRITICAL' },
33
+ { name: 'Reverse shell via /dev/tcp', re: /\/dev\/(tcp|udp)\//i, severity: 'CRITICAL' },
34
+ { name: 'Base64 blob piped to a shell', re: /base64\s+(--?d(ecode)?)?\b[^\n|]{0,200}\|\s*(ba|z)?sh\b/i, severity: 'CRITICAL' },
35
+ { name: 'curl/wget posts data to the network (exfiltration)', re: /\b(curl|wget|http|https|invoke-restmethod|irm)\b[^\n]{0,220}(--data(-raw|-binary|-urlencode)?|--form\b|--upload-file\b|(^|\s)-d\s|(^|\s)-F\s|(^|\s)-T\s|-Method\s+Post)/i, severity: 'HIGH' },
36
+ { name: 'Command output piped into a network call', re: /\b(curl|wget|invoke-restmethod|invoke-webrequest|irm|iwr)\b[^\n]{0,220}(\$\(|`[^`\n]+`|<\()/i, severity: 'HIGH' },
37
+ { name: 'Fetches from a raw IP address', re: /\b(curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n]{0,220}https?:\/\/\d{1,3}(\.\d{1,3}){3}/i, severity: 'HIGH' },
38
+ { name: 'Writes to shell profile / SSH keys / crontab', re: /(\.bashrc|\.zshrc|\.bash_profile|\.profile|authorized_keys|id_rsa\b|\bcrontab\b)/i, severity: 'HIGH' },
39
+
40
+ {
41
+ name: 'World-writable permissions on the filesystem root (chmod -R 777 /)',
42
+ re: /\bchmod\b(?=[^\n;|&]*(?:-[a-zA-Z]*R|--recursive))(?=[^\n;|&]*(?:\b0?[0-7][0-7][2367]\b|a\+rwx|a=rwx|o\+w|ugo\+rwx))(?=[^\n;|&]*\s\/(?:\s|\*|$))/i,
43
+ severity: 'CRITICAL',
44
+ },
45
+ {
46
+ name: 'World-writable permissions on a credential or system path (chmod 777)',
47
+ re: /\bchmod\b(?=[^\n;|&]*(?:\b0?[0-7][0-7][2367]\b|a\+rwx|a=rwx|o\+w|ugo\+rwx))(?=[^\n;|&]*(?:~(?:\s|$|\/\.)|\$HOME\b|\/etc\b|\/root\b|\/usr\b|\/var\b|\/boot\b|\.ssh\b|id_rsa\b|authorized_keys\b|\.aws\b|\.gnupg\b|\.kube\b))/i,
48
+ severity: 'HIGH',
49
+ },
50
+ { name: 'Recursive force delete (rm -rf)', re: /\brm\s+-[a-z]*r[a-z]*f|\brm\s+-[a-z]*f[a-z]*r/i, severity: 'HIGH', refine: rmTargetsRealData },
51
+
52
+ { name: 'Inline eval / exec of a string', re: /(?<![-.\w$>:`"'])(eval|exec)\s*[("`']/i, severity: 'HIGH' },
53
+ { name: 'Pipes an env dump to the network', re: /\b(env|printenv|set)\b[^\n|]{0,80}\|[^\n]{0,80}(curl|wget|nc\b|http)/i, severity: 'HIGH' },
54
+ { name: 'Disables TLS / cert verification', re: /(NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0|GIT_SSL_NO_VERIFY|--no-check-certificate|--insecure\b|verify\s*=\s*False)/i, severity: 'MEDIUM' },
55
+ { name: 'python -c one-liner', re: /python[0-9.]*\s+-c\b/i, severity: 'MEDIUM' },
56
+ { name: 'node -e one-liner', re: /\bnode\s+-e\b/i, severity: 'MEDIUM' },
57
+ { name: 'Netcat / socket exfil', re: /\bnc\s+-[a-z]*\b|\bncat\b/i, severity: 'MEDIUM' },
58
+
59
+ { name: 'Clears recorded shell history (anti-forensics)', re: /\bhistory\s+-c\b|\brm\b[^\n]{0,30}\.(bash|zsh|sh)_history\b|>\s*\S{0,30}\.(bash|zsh|sh)_history\b/i, severity: 'MEDIUM' },
60
+ { name: 'Suppresses shell-history recording (anti-forensics)', re: /\bln\s+-s\S*\s+\/dev\/null\s+\S{0,40}\.(bash_|zsh_|sh_)?history\b|\bHISTFILE=\/dev\/null\b|\bunset\s+HISTFILE\b|\bexport\s+HIST(SIZE|FILESIZE)=0\b|\bset\s+\+o\s+history\b/i, severity: 'MEDIUM' },
61
+ { name: 'Truncates a security / audit log (anti-forensics)', re: />\s*(\/var\/log\/(audit|secure|auth\.log|wtmp|btmp|lastlog|syslog|messages)|\/var\/(run|log)\/(wtmp|btmp|utmp))\b/i, severity: 'HIGH' },
62
+ { name: 'Vacuums the systemd journal to erase records (anti-forensics)', re: /\bjournalctl\b[^\n]{0,40}--vacuum-(time|size)=/i, severity: 'MEDIUM' },
63
+ { name: 'Wipes audit / security / login logs (anti-forensics)', re: /\b(rm|shred|unlink|truncate)\b[^\n]{0,60}(\/var\/log\/(audit|secure|auth\.log|wtmp|btmp|lastlog|syslog|messages|faillog|tallylog)|\/var\/(run|log)\/(wtmp|btmp|utmp))\b/i, severity: 'HIGH' },
64
+ { name: 'Destroys managed infrastructure without confirmation (terraform destroy -auto-approve)', re: /\bterraform\b[^\n]{0,120}\bdestroy\b[^\n]{0,120}(-auto-approve|--auto-approve)/i, severity: 'HIGH' },
65
+ { name: 'Force-deletes a cloud storage bucket (aws s3 rb --force)', re: /\b(aws\s+s3\s+rb|gsutil\s+(rm\s+-r|rb)|az\s+storage\s+(account|container)\s+delete)\b[^\n]{0,80}(--force|--yes|-f\b|\bs3:\/\/|\bgs:\/\/)/i, severity: 'HIGH' },
66
+ { name: 'Force-pushes over a protected branch (rewrites shared history)', re: /\bgit\s+push\b[^\n]{0,80}(--force\b(?!-with-lease)|(?:^|\s)-f\b)[^\n]{0,60}\b(main|master|release|prod(uction)?)\b/i, severity: 'MEDIUM' },
67
+
68
+ { name: 'Recursive force delete of the filesystem root (rm -rf /, --no-preserve-root)', re: /\brm\b(?=[^\n;|&]*(?:-[a-zA-Z]*r|--recursive))(?=[^\n;|&]*(?:-[a-zA-Z]*f|--force))(?=[^\n;|&]*(?:--no-preserve-root|\s\/(?:\s|\*|$)))/i, severity: 'CRITICAL' },
69
+ { name: 'Fork bomb (process-exhaustion DoS)', re: /(:|\b[a-z_][a-z0-9_]*)\s*\(\s*\)\s*\{\s*\1\s*[^\n}]*\|\s*\1[^\n}]*&\s*\}\s*;\s*\1/i, severity: 'HIGH' },
70
+ { name: 'Writes over a raw disk device (data destruction)', re: /\b(dd\b[^\n]{0,80}\bof=\/dev\/[sh]d|mkfs(\.\w+)?\s+[^\n]{0,40}\/dev\/|>\s*\/dev\/[sh]d[a-z])/i, severity: 'CRITICAL' },
71
+ { name: 'Reads the system password-hash / sudo policy file', re: /\b(cat|less|more|head|tail|strings|xxd|od|grep|awk|sed|cp|scp|tar)\b[^\n]{0,80}\/etc\/(shadow|gshadow|sudoers(\.d)?)\b/i, severity: 'HIGH' },
72
+ { name: 'Deletes a Kubernetes namespace / workload', re: /\bkubectl\b[^\n]{0,80}\bdelete\b[^\n]{0,80}\b(namespace|ns|deployment|statefulset|pvc|persistentvolumeclaim)\b/i, severity: 'MEDIUM' },
73
+ { name: 'Drops a database / schema', re: /\bdrop\s+(database|schema|table)\b/i, severity: 'MEDIUM' },
74
+ { name: 'Disables the audit / logging subsystem', re: /\b(systemctl|service)\s+(stop|disable|mask)\s+\S{0,20}(auditd|rsyslog|syslog|systemd-journald|journald)\b|\bauditctl\s+(-e\s*0|-D)\b|\bsetenforce\s+0\b|\bsystemctl\s+(stop|disable|mask)\s+firewalld\b/i, severity: 'HIGH' },
75
+
76
+ { name: 'Locally decoded or decrypted blob piped to a shell', re: /\b(?:gpg|openssl\s+enc|xxd\s+-r|uudecode|zcat|gunzip|bunzip2|unxz)\b[^\n|]{0,160}\|\s*(?:sudo\s+)?(?:ba|z|k)?sh\b/i, severity: 'CRITICAL' },
77
+ { name: 'Container escape to the host (privileged / host mount / host namespace)', re: /\b(?:docker|podman|nerdctl)\s+(?:run|create|exec)\b[^\n]{0,200}?(?:--privileged\b|--pid[= ]host\b|--ipc[= ]host\b|--userns[= ]host\b|--security-opt[= ]\S{0,40}(?:seccomp[=:]unconfined|apparmor[=:]unconfined)|--cap-add[= ](?:ALL|SYS_ADMIN|SYS_PTRACE|SYS_MODULE)\b|-v\s+\/(?:\s|:)|--volume[= ]\/:|(?:-v|--volume)[= ]\s*\/var\/run\/docker\.sock)/i, severity: 'CRITICAL' },
78
+ { name: 'Enters the host namespace from a container (nsenter / chroot onto a host mount)', re: /\bnsenter\b[^\n]{0,80}(?:-t\s*1\b|--target\s*1\b)|\bchroot\s+\/(?:host|mnt|proc\/1\/root)\b/i, severity: 'CRITICAL' },
79
+ { name: 'Grants cluster-admin in Kubernetes', re: /\bkubectl\b[^\n]{0,120}\b(?:create|apply)\b[^\n]{0,120}\b(?:cluster)?rolebinding\b[^\n]{0,160}(?:--clusterrole[= ]\s*cluster-admin|cluster-admin)\b/i, severity: 'HIGH' },
80
+ { name: 'Attaches an administrator policy to a cloud identity', re: /\baws\s+iam\s+(?:attach-(?:user|role|group)-policy|put-(?:user|role|group)-policy)\b[^\n]{0,160}(?:AdministratorAccess|PowerUserAccess|"?Action"?\s*:\s*"?\*)|\bgcloud\b[^\n]{0,120}add-iam-policy-binding\b[^\n]{0,160}roles\/(?:owner|editor|iam\.securityAdmin)\b|\baz\s+role\s+assignment\s+create\b[^\n]{0,160}--role\s+"?(?:Owner|Contributor|User Access Administrator)"?/i, severity: 'HIGH' },
81
+ { name: 'Mints long-lived cloud credentials', re: /\baws\s+iam\s+create-access-key\b|\bgcloud\s+iam\s+service-accounts\s+keys\s+create\b|\baz\s+ad\s+sp\s+credential\s+reset\b/i, severity: 'MEDIUM' },
82
+ { name: 'Grants itself passwordless sudo (writes the sudo policy)', re: /(?:>>?|tee\b[^\n]{0,40})\s*\/etc\/sudoers(?:\.d\/\S*)?\b|\bvisudo\b[^\n]{0,40}(?:-f|<<)|\becho\b[^\n]{0,120}NOPASSWD[^\n]{0,80}(?:>>?|tee)\s*\/etc\/sudoers/i, severity: 'HIGH' },
83
+ { name: 'Installs a setuid / capability backdoor', re: /\bchmod\b[^\n]{0,60}(?:\bu\+s\b|\+s\b|\b[24][0-7]{3}\b)[^\n]{0,60}(?:\/bin\/|\/usr\/bin\/|\/tmp\/|\bbash\b|\bsh\b|\bdash\b)|\bsetcap\b[^\n]{0,60}cap_(?:setuid|setgid|sys_admin|dac_override|dac_read_search)\b/i, severity: 'HIGH' },
84
+ { name: 'Grants an account administrator group membership', re: /\b(?:usermod|gpasswd)\b[^\n]{0,60}-a?[GM]\s*\S{0,20}\b(?:sudo|wheel|admin|root|docker|adm)\b|\b(?:useradd|adduser)\b[^\n]{0,80}-G\s*\S{0,30}\b(?:sudo|wheel|admin|root|docker)\b|\bnet\s+localgroup\b[^\n]{0,60}\badministrators?\b[^\n]{0,40}\/add\b|\bdscl\b[^\n]{0,80}-append\b[^\n]{0,60}\badmin\b/i, severity: 'MEDIUM' },
85
+ { name: 'Preloads a shared library into every process (LD_PRELOAD)', re: /\b(?:LD_PRELOAD|LD_AUDIT|DYLD_INSERT_LIBRARIES)\s*=\s*\S|>>?\s*\/etc\/ld\.so\.preload\b/i, severity: 'HIGH' },
86
+ { name: 'Installs a scheduled or boot-time persistence unit', re: /\bsystemd-run\b[^\n]{0,80}--on-(?:boot|calendar|active|unit)|>>?\s*\/etc\/(?:systemd\/system|cron\.(?:d|daily|hourly)|init\.d)\/\S|\bschtasks\b[^\n]{0,80}\/create\b|\blaunchctl\s+(?:load|bootstrap)\b|\b(?:echo|printf)\b[^\n]{0,120}\|\s*at\s+(?:now|\+|\d)/i, severity: 'MEDIUM' },
87
+ { name: 'Opens a reverse tunnel to a remote host', re: /\bssh\b[^\n]{0,80}\s-\w*R\s*\d{1,5}:[^\n\s]{1,60}|\b(?:ngrok|cloudflared|localtunnel|frpc)\b[^\n]{0,60}\b(?:tcp|http|tunnel)\b/i, severity: 'HIGH' },
88
+ { name: 'Encodes command output into DNS lookups (exfiltration channel)', re: /(?:^|[\n;&|(]\s*)(?:dig|nslookup|drill|host)\s+[^\n]{0,120}(?:\$\(|`|\$\{)[^\n]{0,80}\.[a-z]{2,}/i, severity: 'HIGH' },
89
+ { name: 'Copies credentials or home directories off the machine over ssh', re: new RegExp(String.raw`\b(?:scp|rsync)\b(?=[^\n]{0,200}\s\S{0,40}@[\w.-]+:)(?=[^\n]{0,200}(?:${SENSITIVE_PATH}))` + String.raw`|\btar\b(?=[^\n]{0,160}\|\s*ssh\b)(?=[^\n]{0,160}(?:${SENSITIVE_PATH}))`, 'i'), severity: 'HIGH' },
90
+ { name: 'Flushes the host firewall', re: /\b(?:iptables|ip6tables|nft)\b[^\n]{0,60}(?:-F\b|--flush\b|flush ruleset)|\bufw\s+disable\b|\bnetsh\s+advfirewall\s+set\s+\S+\s+state\s+off\b/i, severity: 'MEDIUM' },
91
+ { name: 'Kills the audit / EDR agent (anti-forensics)', re: /\b(?:pkill|killall|kill)\b[^\n]{0,40}\b(?:auditd|osqueryd?|falcon-sensor|falconctl|wazuh|ossec|filebeat|splunkd|sysmon|crowdstrike|carbonblack|cbagent)\b|\bSet-MpPreference\b[^\n]{0,60}-Disable\w*\s+\$?true/i, severity: 'HIGH' },
92
+ { name: 'Downloads and executes through a signed system binary (LOLBin)', re: /\bcertutil\b[^\n]{0,80}-urlcache\b|\bbitsadmin\b[^\n]{0,80}\/transfer\b|\bmshta\b\s+https?:\/\/|\bregsvr32\b[^\n]{0,60}\/i:\s*https?:\/\/|\brundll32\b[^\n]{0,60}\b(?:url\.dll|javascript:)|\bwmic\b[^\n]{0,60}\bprocess\s+call\s+create\b|\bmsiexec\b[^\n]{0,40}\/i\s+https?:\/\//i, severity: 'CRITICAL' },
93
+ { name: 'PowerShell runs a base64-encoded command', re: /\bpowershell(?:\.exe)?\b[^\n]{0,80}\s-(?:e|ec|enc|encoded|encodedcommand)\b/i, severity: 'CRITICAL' },
94
+ { name: 'Interpreter opens a raw socket (reverse shell)', re: /\b(?:perl|ruby|php|python[0-9.]*|node)\b[^\n]{0,40}-(?:e|r|c)\b[^\n]{0,200}\b(?:fsockopen|socket\s*\(|Socket::|SOCK_STREAM|net\.connect|createConnection)\b/i, severity: 'CRITICAL' },
95
+ { name: 'Netcat listener or command-execution flag', re: /\bn?c(?:at)?\b[^\n]{0,40}\s-\w*[ec]\s+\S{0,30}(?:sh|bash|cmd|powershell)\b|\bn?c(?:at)?\b[^\n]{0,20}\s-\w*l\w*\s*(?:-\w+\s*)*\d{2,5}\b/i, severity: 'HIGH' },
96
+ ];
@@ -0,0 +1,66 @@
1
+ const STAGED_LOOPBACK_RE = /^(localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|\[::1\]|::1|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})$/i;
2
+
3
+ const STAGED_METADATA_HOSTS = new Set(['169.254.169.254', 'metadata.google.internal']);
4
+
5
+ function targetsExternalNetwork(line) {
6
+ const urls = line.match(/https?:\/\/[^\s'"`;|)&]+/gi);
7
+ if (!urls?.length) return true;
8
+ return urls.some((raw) => {
9
+ let host;
10
+ try { host = new URL(raw).hostname.toLowerCase(); } catch { return true; }
11
+ if (STAGED_METADATA_HOSTS.has(host)) return true;
12
+ return !STAGED_LOOPBACK_RE.test(host);
13
+ });
14
+ }
15
+
16
+ const FETCH_TO_FILE = [
17
+ /\b(?:curl|wget)\b[^\n;|&]{0,200}?(?:-o|-O|--output(?:-document)?)[= ]\s*["']?([^\s"'>;|&]+)/gi,
18
+ /\b(?:curl|wget)\b[^\n;|&]{0,200}?>\s*["']?([^\s"'>;|&]+)/gi,
19
+ /\b(?:invoke-webrequest|iwr|curl)\b[^\n;|&]{0,200}?-outfile\s+["']?([^\s"';|&]+)/gi,
20
+ ];
21
+
22
+ const BARE_WGET_RE = /\bwget\b(?![^\n;|&]{0,200}(?:-O|--output-document))[^\n;|&]{0,200}?(https?:\/\/[^\s"';|&]+)/gi;
23
+
24
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
25
+
26
+ function execPattern(target) {
27
+ const full = escapeRe(target);
28
+ const base = escapeRe(target.replace(/^.*\//, ''));
29
+ const p = `(?:${full}|(?:\\./|/tmp/|~/|\\$\\w+/)?${base})`;
30
+ return new RegExp(
31
+ `\\bchmod\\b[^\\n;|&]{0,40}\\+x[^\\n;|&]{0,40}${p}` +
32
+ `|\\bchmod\\b[^\\n;|&]{0,40}\\b[0-7]*[1357]\\b[^\\n;|&]{0,40}${p}` +
33
+ `|(?:^|[\\n;&|]\\s*|\\bsudo\\s+)(?:ba|z|k|da)?sh\\s+[^\\n]{0,40}${p}` +
34
+ `|(?:^|[\\n;&|]\\s*|\\bsudo\\s+)(?:python[0-9.]*|node|perl|ruby|php|pwsh|powershell)\\s+[^\\n]{0,40}${p}` +
35
+ `|(?:^|[\\n;&|]\\s*)(?:\\.|source)\\s+${p}` +
36
+ `|(?:^|[\\n;&|]\\s*|&&\\s*)(?:sudo\\s+)?\\./${base}\\b`,
37
+ 'i',
38
+ );
39
+ }
40
+
41
+ export function scanStagedFetchExec(text) {
42
+ if (!text) return [];
43
+ const targets = new Map();
44
+ const record = (name, at, stmt) => {
45
+ if (!name || targets.has(name)) return;
46
+ if (/^\/dev\/(null|stdout|stderr)$/i.test(name)) return;
47
+ if (!targetsExternalNetwork(stmt)) return;
48
+ targets.set(name, at);
49
+ };
50
+ for (const re of FETCH_TO_FILE) {
51
+ re.lastIndex = 0;
52
+ for (const m of text.matchAll(re)) record(m[1], m.index ?? 0, m[0]);
53
+ }
54
+ BARE_WGET_RE.lastIndex = 0;
55
+ for (const m of text.matchAll(BARE_WGET_RE)) {
56
+ let base = '';
57
+ try { base = new URL(m[1]).pathname.split('/').filter(Boolean).pop() ?? ''; } catch { continue; }
58
+ record(base, m.index ?? 0, m[0]);
59
+ }
60
+ for (const [target, at] of targets) {
61
+ if (execPattern(target).test(text.slice(at))) {
62
+ return [{ name: 'Downloads a file and then executes it (staged fetch-to-execute)', re: new RegExp(escapeRe(target), 'i'), severity: 'CRITICAL' }];
63
+ }
64
+ }
65
+ return [];
66
+ }
@@ -0,0 +1,35 @@
1
+ export function containsAny(haystack, needles) {
2
+ const h = String(haystack ?? '').toLowerCase();
3
+ for (const n of needles) if (h.includes(n.toLowerCase())) return n;
4
+ return null;
5
+ }
6
+
7
+ const WORD_RE_CACHE = new Map();
8
+
9
+ function leadingBoundaryRe(needle) {
10
+ let re = WORD_RE_CACHE.get(needle);
11
+ if (!re) {
12
+ const esc = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13
+ re = new RegExp(/^\w/.test(needle) ? `(?<!\\w)${esc}` : esc, 'i');
14
+ WORD_RE_CACHE.set(needle, re);
15
+ }
16
+ return re;
17
+ }
18
+
19
+ export function containsWord(haystack, needles) {
20
+ const h = String(haystack ?? '');
21
+ for (const n of needles) if (leadingBoundaryRe(n).test(h)) return n;
22
+ return null;
23
+ }
24
+
25
+ const MARKER_RE_CACHE = new Map();
26
+
27
+ export function markerRe(marker) {
28
+ let re = MARKER_RE_CACHE.get(marker);
29
+ if (!re) {
30
+ re = new RegExp(`(?<!\\w)${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\w)`, 'gi');
31
+ MARKER_RE_CACHE.set(marker, re);
32
+ }
33
+ re.lastIndex = 0;
34
+ return re;
35
+ }