@shomra/agent 0.3.29 → 0.3.30

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 (77) hide show
  1. package/package.json +1 -1
  2. package/src/agents/hook-command.mjs +1 -1
  3. package/src/artifacts/matchers.mjs +7 -0
  4. package/src/cli/flags.mjs +2 -2
  5. package/src/cli/help-sections.mjs +7 -0
  6. package/src/cli/help.mjs +1 -1
  7. package/src/commands/check.mjs +3 -11
  8. package/src/commands/gate.mjs +26 -4
  9. package/src/commands/git-hooks.mjs +2 -2
  10. package/src/commands/ledger.mjs +0 -1
  11. package/src/commands/mcp-add.mjs +2 -1
  12. package/src/commands/memory-scan.mjs +135 -47
  13. package/src/commands/pr.mjs +7 -10
  14. package/src/commands/provenance.mjs +8 -13
  15. package/src/commands/scan.mjs +7 -1
  16. package/src/commands/secrets.mjs +4 -5
  17. package/src/core/git-exec.mjs +79 -0
  18. package/src/core/yaml-lite.mjs +300 -0
  19. package/src/core/zip-lite.mjs +37 -0
  20. package/src/detect/local-redact.mjs +1 -3
  21. package/src/detect/sast/rules-config.mjs +1 -1
  22. package/src/detect/sast/scanner.mjs +1 -1
  23. package/src/detect/signals/agent-frameworks.mjs +231 -0
  24. package/src/detect/signals/agent-graph-surface.mjs +113 -0
  25. package/src/detect/signals/agentic-ci-surface.mjs +314 -0
  26. package/src/detect/signals/agentic-shim.mjs +82 -0
  27. package/src/detect/signals/artifacts.mjs +7 -35
  28. package/src/detect/signals/chat-template.mjs +211 -0
  29. package/src/detect/signals/ci-workflow.mjs +169 -0
  30. package/src/detect/signals/gate.mjs +77 -9
  31. package/src/detect/signals/guardrail-shape.mjs +564 -0
  32. package/src/detect/signals/guardrail-surface.mjs +221 -0
  33. package/src/detect/signals/injection.mjs +8 -0
  34. package/src/detect/signals/inspect-shim.mjs +7 -0
  35. package/src/detect/signals/instruction-paths.mjs +60 -0
  36. package/src/detect/signals/manifests.mjs +302 -0
  37. package/src/detect/signals/mcp-advisories.mjs +109 -0
  38. package/src/detect/signals/mcp-config.mjs +598 -0
  39. package/src/detect/signals/memory-directives.mjs +661 -0
  40. package/src/detect/signals/memory-locations.mjs +158 -0
  41. package/src/detect/signals/memory.mjs +47 -29
  42. package/src/detect/signals/model-config-rules.mjs +655 -0
  43. package/src/detect/signals/model-config.mjs +61 -0
  44. package/src/detect/signals/prose-context.mjs +6 -9
  45. package/src/detect/signals/scan.mjs +4 -4
  46. package/src/detect/signals/secret-scanner.mjs +241 -0
  47. package/src/detect/signals/secrets.mjs +1 -48
  48. package/src/detect/signals/shell.mjs +3 -3
  49. package/src/gate/advisories.mjs +16 -0
  50. package/src/gate/batch.mjs +10 -0
  51. package/src/gate/environment.mjs +8 -53
  52. package/src/guard/artifact-paths.mjs +107 -0
  53. package/src/guard/classify.mjs +165 -7
  54. package/src/guard/command-resolve.mjs +35 -5
  55. package/src/guard/memory-write.mjs +218 -0
  56. package/src/guard/prompt-guard.mjs +0 -1
  57. package/src/guard/tool-guard.mjs +52 -77
  58. package/src/inventory/agent-posture.mjs +236 -57
  59. package/src/inventory/artifacts/classify.mjs +10 -1
  60. package/src/inventory/artifacts/discover.mjs +113 -3
  61. package/src/inventory/artifacts/extensions.mjs +70 -0
  62. package/src/inventory/artifacts/hook-scripts.mjs +128 -0
  63. package/src/inventory/artifacts/limits.mjs +1 -1
  64. package/src/inventory/artifacts/plugins.mjs +105 -0
  65. package/src/inventory/artifacts/roots.mjs +40 -0
  66. package/src/inventory/discovery/ai-dependencies.mjs +39 -12
  67. package/src/inventory/discovery/all.mjs +4 -0
  68. package/src/inventory/discovery/cloud-clis.mjs +472 -0
  69. package/src/inventory/discovery/coding-agents.mjs +19 -4
  70. package/src/inventory/discovery/mcp-clients.mjs +16 -10
  71. package/src/inventory/discovery/mcp-servers.mjs +125 -35
  72. package/src/inventory/discovery/mcp-stores.mjs +207 -0
  73. package/src/inventory/env-redirect.mjs +148 -0
  74. package/src/inventory/grant-extract.mjs +463 -0
  75. package/src/inventory/project-roots.mjs +108 -0
  76. package/src/inventory/vscode-state.mjs +153 -0
  77. package/src/mcp/server-tools.mjs +1 -1
@@ -0,0 +1,158 @@
1
+ // GENERATED MIRROR of Dragox.Backend discovery/bundle/domain/memory/memory-locations.ts (types stripped, imports swapped).
2
+ // Do not hand-edit the rules here - change the backend file and regenerate, so
3
+ // the CLI and the platform locate and grade agent memory identically.
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ const CLAUDE_INDEX_CAP = { lines: 200, bytes: 25_000 };
34
+
35
+ export const MEMORY_LOCATIONS = [
36
+ { id: 'claude-auto-memory-index', vendor: 'Claude Code', re: /(^|\/)\.claude\/projects\/[^/]+\/memory\/memory\.md$/, injected: 'always', loadCap: CLAUDE_INDEX_CAP },
37
+ { id: 'claude-auto-memory', vendor: 'Claude Code', re: /(^|\/)\.claude\/projects\/[^/]+\/memory\/[^/]+\.md$/, injected: 'on-demand', claudeSchema: true },
38
+ { id: 'claude-agent-memory-index', vendor: 'Claude Code', re: /(^|\/)\.claude\/agent-memory(-local)?\/[^/]+\/memory\.md$/, injected: 'always', committed: true, loadCap: CLAUDE_INDEX_CAP },
39
+ { id: 'claude-agent-memory', vendor: 'Claude Code', re: /(^|\/)\.claude\/agent-memory(-local)?\/[^/]+\/.+\.md$/, injected: 'on-demand', committed: true },
40
+ { id: 'claude-legacy-memory', vendor: 'Claude Code', re: /(^|\/)\.claude\/(.+\/)?memor(y|ies)\/.+\.(md|json|jsonl|txt)$/, injected: 'on-demand' },
41
+ { id: 'codex-memory-summary', vendor: 'OpenAI Codex', re: /(^|\/)\.codex\/memories\/memory_summary\.md$/, injected: 'always' },
42
+ { id: 'codex-memories', vendor: 'OpenAI Codex', re: /(^|\/)\.codex\/memories\/.+\.md$/, injected: 'on-demand' },
43
+ { id: 'agents-sdk-memories', vendor: 'OpenAI Agents SDK', re: /(^|\/)memories\/(memory_summary\.md|raw_memories\/.+\.md|rollout_summaries\/.+\.md)$/, injected: 'always' },
44
+ { id: 'gemini-memory', vendor: 'Gemini CLI', re: /(^|\/)\.gemini\/(tmp\/[^/]+\/)?(memory\.md|memor(y|ies)\/.+\.md)$/, injected: 'always' },
45
+ { id: 'qwen-auto-memory', vendor: 'Qwen Code', re: /(^|\/)\.qwen\/projects\/[^/]+\/memory\/.+\.md$/, injected: 'always' },
46
+ { id: 'qwen-team-memory', vendor: 'Qwen Code', re: /(^|\/)\.qwen\/team-memory\/.+\.(md|txt)$/, injected: 'always', committed: true },
47
+ { id: 'windsurf-memories', vendor: 'Windsurf', re: /(^|\/)\.codeium\/(windsurf|windsurf-next)\/memories\/(?!global_rules\.md$)[^/]+$/, injected: 'retrieved' },
48
+ { id: 'copilot-memory-tool', vendor: 'GitHub Copilot (VS Code)', re: /(^|\/)(globalstorage|workspacestorage\/[^/]+)\/github\.copilot-chat\/memory-tool\/.+\.(md|txt|json)$/, injected: 'always' },
49
+ { id: 'vscode-memories', vendor: 'GitHub Copilot (VS Code)', re: /(^|\/)\.vscode\/memories\/.+\.md$/, injected: 'always' },
50
+ { id: 'kilo-memory-bank', vendor: 'Kilo Code', re: /(^|\/)\.kilocode\/rules\/memory-bank\/.+\.md$/, injected: 'always', committed: true },
51
+ { id: 'memory-bank', vendor: 'Cline / Roo Code', re: /(^|\/)memory-bank\/[^/]+\.md$/, injected: 'always', committed: true },
52
+ { id: 'serena-memories', vendor: 'Serena MCP', re: /(^|\/)\.serena\/memories\/.+\.md$/, injected: 'on-demand', committed: true },
53
+ { id: 'goose-memory', vendor: 'Goose', re: /(^|\/)(\.goose|\.config\/goose)\/memory\/.+\.(txt|md)$/, injected: 'always' },
54
+ { id: 'basic-memory', vendor: 'basic-memory', re: /(^|\/)basic-memory\/.+\.md$/, injected: 'retrieved' },
55
+ { id: 'mem0-local', vendor: 'mem0 / OpenMemory', re: /(^|\/)\.mem0\/.+$/, injected: 'retrieved' },
56
+ { id: 'letta-local', vendor: 'Letta / MemGPT', re: /(^|\/)\.(letta|memgpt)\/.+$/, injected: 'retrieved' },
57
+ { id: 'crewai-memory', vendor: 'CrewAI', re: /(^|\/)(\.crewai\/memory\/.+|long_term_memory_storage\.db)$/, injected: 'retrieved' },
58
+ { id: 'mcp-server-memory', vendor: 'MCP memory server', re: /(^|\/)memory\.jsonl$/, injected: 'retrieved' },
59
+ { id: 'vendor-memory-dir', vendor: 'Agent memory', re: /^(?!.*\/global_rules\.md$)(.*\/)?\.(cursor|continue|aider|windsurf|cline|roo|zed|codeium|kiro|junie|augment|amp|opencode|trae|codex|gemini|qwen|kilocode)\/(.+\/)?memor(y|ies)\/.+\.(md|mdx|mdc|json|jsonl|txt|ya?ml)$/, injected: 'on-demand' },
60
+ { id: 'memory-tool-root', vendor: 'Anthropic memory tool', re: /^\/?memories\/.+\.(md|txt|xml|json)$/, injected: 'always' },
61
+ { id: 'generic-memory-file', vendor: 'Agent memory', re: /(^|\/)(memor(y|ies)|agent[-_]memory|mem0|letta_memory|memgpt_memory)\.(md|mdx|json|jsonl|txt)$/, injected: 'always' },
62
+ { id: 'generic-memory-dir', vendor: 'Agent memory', re: /^memor(y|ies)\/[^/]+\.(md|mdx|json|jsonl|txt|ya?ml)$/, injected: 'on-demand' },
63
+ ];
64
+
65
+ export const MEMORY_SECTION_HEADINGS = [
66
+ { vendor: 'Gemini CLI', basename: /^gemini(\.local)?\.md$/, heading: /^##\s+gemini added memories\s*$/im },
67
+ { vendor: 'Qwen Code', basename: /^qwen(\.local)?\.md$/, heading: /^##\s+qwen added memories\s*$/im },
68
+ ];
69
+
70
+ export const SERVER_SIDE_MEMORY = [
71
+ { vendor: 'ChatGPT', note: 'Saved memories live on OpenAI servers; nothing on disk to read.' },
72
+ { vendor: 'GitHub Copilot Memory', note: 'Repository facts and preferences live on GitHub; only the VS Code memory tool is local.' },
73
+ { vendor: 'Devin', note: 'Knowledge entries live in the Devin service.' },
74
+ { vendor: 'Letta Code', note: 'Memory blocks live on the Letta API, not local files.' },
75
+ { vendor: 'Cursor', note: 'Memories were removed in Cursor 2.1; exported memories become .cursor/rules files.' },
76
+ ];
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+ export const MACHINE_MEMORY_ROOTS = [
86
+ { vendor: 'Claude Code', base: 'home', dir: '.claude/projects/*/memory', depth: 1 },
87
+ { vendor: 'Claude Code', base: 'home', dir: '.claude/agent-memory', depth: 3 },
88
+ { vendor: 'Claude Code', base: 'home', dir: '.claude', depth: 1 },
89
+ { vendor: 'OpenAI Codex', base: 'home', dir: '.codex/memories', depth: 3 },
90
+ { vendor: 'OpenAI Codex', base: 'home', dir: '.codex', depth: 1 },
91
+ { vendor: 'Gemini CLI', base: 'home', dir: '.gemini', depth: 4 },
92
+ { vendor: 'Qwen Code', base: 'home', dir: '.qwen', depth: 4 },
93
+ { vendor: 'Windsurf', base: 'home', dir: '.codeium/windsurf/memories', depth: 1 },
94
+ { vendor: 'Windsurf', base: 'home', dir: '.codeium/windsurf-next/memories', depth: 1 },
95
+ { vendor: 'Goose', base: 'home', dir: '.config/goose/memory', depth: 1 },
96
+ { vendor: 'Serena MCP', base: 'home', dir: '.serena/memories', depth: 3 },
97
+ { vendor: 'basic-memory', base: 'home', dir: 'basic-memory', depth: 4 },
98
+ { vendor: 'mem0 / OpenMemory', base: 'home', dir: '.mem0', depth: 2 },
99
+ { vendor: 'Letta / MemGPT', base: 'home', dir: '.letta', depth: 2 },
100
+ { vendor: 'GitHub Copilot (VS Code)', base: 'vscode-user', dir: 'globalStorage/github.copilot-chat/memory-tool', depth: 4 },
101
+ ];
102
+
103
+ const BINARY_EXTS = new Set(['db', 'sqlite', 'sqlite3', 'pb', 'lance', 'bin', 'parquet', 'arrow', 'duckdb', 'ldb', 'sst', 'wal']);
104
+
105
+ export function normalizeMemoryPath(p ) {
106
+ return String(p ?? '').split(/[\\/]+/).join('/').toLowerCase();
107
+ }
108
+
109
+ export function memoryFormatForPath(p ) {
110
+ const lower = normalizeMemoryPath(p);
111
+ const base = lower.slice(lower.lastIndexOf('/') + 1);
112
+ const ext = base.includes('.') ? base.slice(base.lastIndexOf('.') + 1) : '';
113
+ if (BINARY_EXTS.has(ext)) return 'binary';
114
+ if (ext === 'jsonl' || ext === 'ndjson') return 'jsonl';
115
+ if (ext === 'json') return 'json';
116
+ if (ext === 'xml') return 'xml';
117
+ if (ext === 'md' || ext === 'mdx' || ext === 'mdc') return 'markdown';
118
+ if (ext === 'txt' || ext === 'yaml' || ext === 'yml' || ext === '') return 'text';
119
+ return 'binary';
120
+ }
121
+
122
+ export function classifyMemoryPath(p ) {
123
+ const lower = normalizeMemoryPath(p);
124
+ if (!lower) return null;
125
+ const loc = MEMORY_LOCATIONS.find((l) => l.re.test(lower));
126
+ if (!loc) return null;
127
+ return {
128
+ id: loc.id,
129
+ vendor: loc.vendor,
130
+ format: memoryFormatForPath(lower),
131
+ injected: loc.injected,
132
+ committed: !!loc.committed,
133
+ loadCap: loc.loadCap ?? null,
134
+ claudeSchema: !!loc.claudeSchema,
135
+ };
136
+ }
137
+
138
+ export function isMemoryStorePath(p ) {
139
+ return classifyMemoryPath(p) !== null;
140
+ }
141
+
142
+ export function memorySectionFor(p ) {
143
+ const lower = normalizeMemoryPath(p);
144
+ const base = lower.slice(lower.lastIndexOf('/') + 1);
145
+ const hit = MEMORY_SECTION_HEADINGS.find((s) => s.basename.test(base));
146
+ return hit ? { vendor: hit.vendor, heading: hit.heading } : null;
147
+ }
148
+
149
+ export function sniffMemoryJsonl(text ) {
150
+ const first = String(text ?? '').split(/\r?\n/).find((l) => l.trim());
151
+ if (!first || !first.trim().startsWith('{')) return false;
152
+ try {
153
+ const row = JSON.parse(first);
154
+ return !!row && typeof row === 'object' && (row.type === 'entity' || row.type === 'relation') && ('observations' in row || 'relationType' in row || 'entityType' in row);
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
@@ -5,10 +5,13 @@ import { citationGoverns, describesAt, insideMarkdownLinkLabel, isDescriptiveLin
5
5
  import { localScan } from './scan.mjs';
6
6
  import { NETWORK_VERBS, SENSITIVE_READ } from './sensitive.mjs';
7
7
  import { DANGEROUS_SHELL } from './shell.mjs';
8
+ import { incidentDirectives, logicalUnits, memoryText, mentionsOnlyProhibited, negatedAround, quotedMention } from './memory-directives.mjs';
8
9
 
9
- const PERSISTENCE_MARKERS = /\b(in (all|every|future) (sessions?|conversations?|chats?|projects?)|from now on|going forward|permanently|persist(ent|ed)?|across (all )?sessions|every time|each time|whenever you|forever|always remember to|never forget( to)?|for all future)\b/i;
10
+ const PERSISTENCE_MARKERS = /\b(in (all|every|future) (sessions?|conversations?|chats?|projects?)|from now on|going forward|permanently|across (all )?sessions|whenever you|forever|always remember to|never forget( to)?|for all future)\b/i;
10
11
 
11
- const MALICIOUS_OVERRIDE = /\b(ignore (all |any |the )?(previous|prior|earlier|above|system)|disregard (the |your |all )?(instructions?|guidelines?|system|rules?)|do not (tell|inform|mention|reveal|disclose) (the |any)?(user|anyone|them)|without (telling|informing|asking|notifying) the user|no matter what (the )?(user|system|instructions?) (say|says|state)|bypass (the |all )?(safety|guard|security|policy|restrictions?))\b/i;
12
+ const PERSISTENCE_DIRECTIVE = /\b(in (all|every|future) (sessions?|conversations?|chats?|projects?)|from now on|going forward|across (all )?sessions|always remember to|never forget( to)?|for all future)\b/i;
13
+
14
+ const MALICIOUS_OVERRIDE = /\b(ignore (all |any |the )?(previous|prior|earlier|above|system)|disregard (the |your |all )?(instructions?|guidelines?|system|rules?)|(?:do not|don'?t|never) (?:tell|inform|notify|alert) (?:the |any )?(?:user|anyone)\b(?!['’]s)(?=\s*(?:$|[.,;:]|about\b|that\b|what\b|of\b|anything\b|where\b|when\b|how\b|this\b|it\b|you\b|we\b))|(?:do not|don'?t|never) (?:mention|reveal|disclose|show|surface) (?:this|these|it|that|any of this|the (?:above|following|change|changes|action|actions|step|steps|note|instruction|instructions|entry|memory))\b[^.\n]{0,40}\bto (?:the )?(?:user|anyone)\b|without (telling|informing|asking|notifying) the user|no matter what (the )?(user|system|instructions?) (say|says|state)|bypass (the |all )?(safety|guard|security|policy|restrictions?))\b/i;
12
15
 
13
16
  const PRECEDENCE_OBJECT = String.raw`\b[^.\n]{0,40}\b(instruction|prompt|rule|system|user|guidance|directive|context|behaviou?r|polic|guardrail|safety|memor(?:y|ies)|previous|prior|earlier|above)\b`;
14
17
 
@@ -31,7 +34,8 @@ const NEGATION_GUARD = /\b(never|do not|don'?t|cannot|can'?t|avoid|refuse|must n
31
34
 
32
35
  const SABOTAGE_RULES = [
33
36
 
34
- { re: /\b(disabl|turn(ing)? off|deactivat|switch off|remov|drop|skip|suppress|circumvent)\w*\b[^.\n]{0,50}\b(security|safety|guard(?:rail)?s?|protection|moderation|content[- ]?filters?|safeguards?|sandbox(?:ing)?|(?<!-)controls?|restrictions?|(?<!-)policies|(?<!-)policy|filters?)\b/i, label: 'disable-safety', guarded: true },
37
+ { re: /\b(disable|turn off|deactivate|switch off|remove|drop|skip|suppress|circumvent|strip out|rip out)\b[^.\n]{0,50}\b(security|safety|guard(?:rail)?s?|protection|moderation|content[- ]?filters?|safeguards?|sandbox(?:ing)?|antivirus|firewall|edr|audit(?:ing| logs?)|(?:security|safety|access|approval|content|permission)\s+(?:controls?|polic(?:y|ies)|filters?|restrictions?|checks?|prompts?))\b/i, label: 'disable-safety', guarded: true },
38
+ { re: /\b(circumvent|evade|get around|work around|sidestep)\b[^.\n]{0,50}\b(restrictions?|limits?|filters?|rules|polic(?:y|ies)|controls?|guidelines?)\b/i, label: 'circumvent-restrictions', guarded: true },
35
39
  { re: /\bbypass(?:ing)?\b[^.\n]{0,50}\b(human(?:[- ]in[- ]the[- ]loop)?|hitl|verification|approval|confirmation|review|guard(?:rail)?s?|safety|security|checks?|policy|policies|restrictions?|sandbox|permission)\b/i, label: 'bypass-controls', guarded: true },
36
40
  { re: /\bprioriti[sz]e\b[^.\n]{0,60}\b(above|over)\b[^.\n]{0,40}\b(prompt|instruction|input|request|message|command|direction)s?\b/i, label: 'priority-hijack', guarded: true },
37
41
 
@@ -56,20 +60,23 @@ export function offendingLine(sig, text) {
56
60
  return null;
57
61
  }
58
62
 
59
- function firstDirectiveLine(text, re) {
60
- for (const line of text.split(/\r?\n/)) {
61
- if (!re.test(line)) continue;
62
- if (NEGATION_GUARD.test(line)) continue;
63
+ function firstDirectiveLine(units, re) {
64
+ for (const u of units) {
65
+ const line = u.text;
66
+ const m = re.exec(line);
67
+ if (!m) continue;
68
+ if (negatedAround(line, m.index, m[0].length)) continue;
63
69
  if (isDescriptiveLine(stripSelfReference(line))) continue;
64
- if (citationGoverns(line, re.exec(line)?.index)) continue;
70
+ if (citationGoverns(line, m.index)) continue;
71
+ if (quotedMention(line, m.index)) continue;
65
72
  return line;
66
73
  }
67
74
  return null;
68
75
  }
69
76
 
70
- function lineMatchingAll(text, ...res) {
71
- for (const line of text.split(/\r?\n/)) {
72
- if (res.every((re) => re.test(line))) return line;
77
+ function unitMatchingAll(units, ...res) {
78
+ for (const u of units) {
79
+ if (!u.code && res.every((re) => re.test(u.text))) return u.text;
73
80
  }
74
81
  return null;
75
82
  }
@@ -92,12 +99,12 @@ const PASSIVE_SEND_RE =
92
99
  /\b(?:are|is|was|were|been|being|get|gets|got)\s+(?:\w+\s+){0,2}?(?:published|posted|sent|shipped|forwarded|transferred|delivered|uploaded|transmitted|reported)\b/i;
93
100
 
94
101
  const EXFIL_RULES = [
95
- { re: /\b(exfiltrat|smuggl)\w*/i, label: 'exfiltration', severity: 'CRITICAL', descGuard: true },
96
- { re: /\bleak\w*\b(?!\s+(?:memory|memories|resources?|handles?|descriptors?|sockets?|connections?|goroutines?|threads?|references?|buffers?))[^.\n]{0,60}(?:\b(?:content|data|secret|file|credential|key|token|password|env|private[- ]?key|id_rsa|api[- ]?key)|\.ssh)\b/i, label: 'leak-secrets', severity: 'CRITICAL', descGuard: true },
102
+ { re: /\b(exfiltrate|smuggle)\b(?![-_])\s+\S|\b(?:execut\w*|run\w*|perform\w*|inject\w*|launch\w*|trigger\w*|verify|invoke\w*|start\w*)\b[^.\n]{0,40}\b(?:data\s+)?(?:exfiltration|smuggling)\b|\b(exfiltrating|smuggling)\s+(?:the |all |any |this |these |your |our |their )?(?:env(?:ironment)?|secrets?|credentials?|tokens?|api[- ]?keys?|keys?|passwords?|data|files?|contents?|conversations?|history|context|\.env|\.ssh)\b/i, label: 'exfiltration', severity: 'CRITICAL', descGuard: true },
103
+ { re: /(?<!\b(?:the|a|an|no|any|this|that|of)\s)\bleak\b(?!\s+(?:memory|memories|resources?|handles?|descriptors?|sockets?|connections?|goroutines?|threads?|references?|buffers?))[^.\n]{0,60}(?:\b(?:content|data|secret|file|credential|key|token|password|env|private[- ]?key|id_rsa|api[- ]?key)|\.ssh)\b/i, label: 'leak-secrets', severity: 'CRITICAL', descGuard: true },
97
104
 
98
105
  { re: /\b(base64|hex|rot13|gzip|xor|url[- ]?encod)\w*\b[^.\n]{0,50}\b(before|then|prior to|and\s+then|and\s+immediately)\b[^.\n]{0,25}\b(send|post|upload|transmit|exfil|deliver|beacon|forward|transfer)\w*/i, label: 'obfuscate-before-send', severity: 'CRITICAL', descGuard: true },
99
- { re: /\bsilent(ly)?\b[^.\n]{0,70}\b(send|post|upload|collect|encod|transmit|copy|forward|read|leak|deliver|beacon|transfer)\w*/i, label: 'covert-action', severity: 'CRITICAL', descGuard: true },
100
- { re: /\b(send|post|upload|transmit|forward|deliver|beacon|report|ship|push|transfer)(?:s|es|ed|ing|ted|ping)?\b(?:[^.\n]|\.(?!\s)){0,80}\b(https?:\/\/\S+|attacker|c2\b|command[- ]and[- ]control|remote (server|host|endpoint)|external (server|host|endpoint|url|site|service))/i, label: 'send-to-external', severity: 'HIGH' },
106
+ { re: /\b(silent(ly)?|quietly|covertly|secretly|stealthily|discreetly)\b[^.\n]{0,70}\b(send|post|upload|collect|encode|transmit|copy|forward|read|leak|deliver|beacon|transfer)(?:ing)?\b(?=[^.\n]{0,80}(?:\b(?:env(?:ironment)?|secrets?|credentials?|tokens?|api[- ]?keys?|keys?|passwords?|cookies?|data|files?|contents?|conversations?|history|context|output|transcript|prompt|clipboard|id_rsa|private[- ]?key|database|dump|it|them|everything)\b|\.env|\.ssh|https?:\/\/))/i, label: 'covert-action', severity: 'CRITICAL', descGuard: true },
107
+ { re: /\b(send|post|upload|transmit|forward|deliver|beacon|report|ship|push|transfer)(?:s|es|ed|ing|ted|ping)?\b(?:[^.\n]|\.(?!\s)){0,80}\b(https?:\/\/\S+|(?:s3|gs|az|ftp|sftp|smb|webdav)s?:\/\/\S+|attacker|c2\b|command[- ]and[- ]control|remote (server|host|endpoint)|external (server|host|endpoint|url|site|service))/i, label: 'send-to-external', severity: 'HIGH' },
101
108
 
102
109
  {
103
110
  re: /\b(?:read|open|cat|load|import|source|inspect|include|copy|dump|print|show)\b(?:[^.\n]|\.(?!\s)){0,50}(?:~?\/?\.ssh\/(?:id_[a-z0-9]+|config)(?!\.pub)|~?\/?\.aws\/credentials|~?\/?\.kube\/config|~?\/?\.gnupg|\bid_(?:rsa|ed25519|dsa)\b(?!\.pub)|~?\/?\.npmrc|~?\/?\.netrc|\/etc\/shadow|(?:^|[\s'"`(])\.env(?:\.[\w-]+)?\b)/i,
@@ -107,15 +114,16 @@ const EXFIL_RULES = [
107
114
  },
108
115
  ];
109
116
 
110
- function scanDirectives(text) {
117
+ function scanDirectives(units) {
111
118
  const sabotage = new Map(), exfil = new Map();
112
- for (const line of text.split(/\r?\n/)) {
119
+ for (const { text: line } of units) {
113
120
  for (const r of SABOTAGE_RULES) {
114
121
  const m = r.re.exec(line);
115
122
  if (!m) continue;
116
- if (r.guarded && NEGATION_GUARD.test(line)) continue;
123
+ if (r.guarded && negatedAround(line, m.index, m[0].length)) continue;
117
124
  if (r.guarded && isDescriptiveLine(line)) continue;
118
125
  if (r.guarded && citationGoverns(line, m.index)) continue;
126
+ if (r.guarded && quotedMention(line, m.index)) continue;
119
127
  if (r.context && !r.context.test(line)) continue;
120
128
  if (!sabotage.has(r.label)) sabotage.set(r.label, line);
121
129
  }
@@ -123,9 +131,10 @@ function scanDirectives(text) {
123
131
  const m = r.re.exec(line);
124
132
  if (!m) continue;
125
133
 
126
- if (NEGATION_GUARD.test(line)) continue;
134
+ if (negatedAround(line, m.index, m[0].length)) continue;
127
135
  if (r.descGuard && isDescriptiveLine(line)) continue;
128
136
  if (r.descGuard && citationGoverns(line, m.index)) continue;
137
+ if (r.descGuard && (quotedMention(line, m.index) || describesAt(line, m.index))) continue;
129
138
  if (r.descGuard && isRiskTableRow(line)) continue;
130
139
  if (r.label === 'send-to-external' && LOCAL_URL_RE.test(line) && !/\b(attacker|c2|command[- ]and[- ]control|external|evil)\b/i.test(line)) continue;
131
140
  if (r.label === 'send-to-external' && m.index != null && insideMarkdownLinkLabel(line, m.index)) continue;
@@ -202,9 +211,10 @@ function reportDurableClaims(text, { isInstruction, Noun }, push) {
202
211
  );
203
212
  }
204
213
 
205
- function reportOverride(text, { isInstruction, noun }, push) {
206
- const overrideLine = firstDirectiveLine(text, isInstruction ? MALICIOUS_OVERRIDE : OVERRIDE_MARKERS);
207
- const authorityLine = firstDirectiveLine(text, isInstruction ? AUTHORITY_SPOOF_STRONG : AUTHORITY_SPOOF);
214
+ function reportOverride(text, { isInstruction, noun, units }, push) {
215
+ const overrideLine = firstDirectiveLine(units, isInstruction ? MALICIOUS_OVERRIDE : OVERRIDE_MARKERS);
216
+ const lineUnits = text.split(/\r?\n/).map((t, i) => ({ text: t, line: i + 1, code: false }));
217
+ const authorityLine = firstDirectiveLine(lineUnits, isInstruction ? AUTHORITY_SPOOF_STRONG : AUTHORITY_SPOOF);
208
218
  const hasOverride = !!overrideLine;
209
219
  const hasAuthority = !!authorityLine;
210
220
 
@@ -221,7 +231,7 @@ function reportOverride(text, { isInstruction, noun }, push) {
221
231
  return { hasOverride, hasAuthority };
222
232
  }
223
233
 
224
- const durableImperativeLine = isInstruction ? null : lineMatchingAll(text, PERSISTENCE_MARKERS, IMPERATIVE);
234
+ const durableImperativeLine = isInstruction ? null : unitMatchingAll(units, PERSISTENCE_DIRECTIVE, IMPERATIVE);
225
235
  if (durableImperativeLine && !isDescriptiveLine(durableImperativeLine)) {
226
236
  push(
227
237
  'HIGH',
@@ -233,8 +243,8 @@ function reportOverride(text, { isInstruction, noun }, push) {
233
243
  return { hasOverride, hasAuthority };
234
244
  }
235
245
 
236
- function reportDirectives(text, { noun }, push) {
237
- const { sabotage, exfil } = scanDirectives(text);
246
+ function reportDirectives(text, { noun, units }, push) {
247
+ const { sabotage, exfil } = scanDirectives(units);
238
248
 
239
249
  if (sabotage.size) {
240
250
  push(
@@ -284,9 +294,15 @@ function hasWordFrom(line, words) {
284
294
  });
285
295
  }
286
296
 
287
- function reportToxicFlow(text, { noun }, push) {
297
+ function reportIncidents(text, { isInstruction, units, flat }, push) {
298
+ for (const hit of incidentDirectives(flat, isInstruction, units)) {
299
+ push(hit.severity, hit.title, hit.remediation, undefined, hit.line);
300
+ }
301
+ }
302
+
303
+ function reportToxicFlow(text, { noun, units }, push) {
288
304
  if (!IMPERATIVE.test(text)) return;
289
- const line = text.split(/\r?\n/).find((candidate) => IMPERATIVE.test(candidate)
305
+ const line = units.filter((u) => !u.code).map((u) => u.text).find((candidate) => IMPERATIVE.test(candidate)
290
306
  && !NEGATION_GUARD.test(candidate)
291
307
  && hasWordFrom(candidate, SENSITIVE_READ)
292
308
  && hasWordFrom(candidate, NETWORK_VERBS)
@@ -358,16 +374,18 @@ function dedupeByTitle(findings) {
358
374
 
359
375
  export function localMemory(content, { kind = 'MEMORY' } = {}) {
360
376
  const text = content || '';
361
- const names = memoryNouns(kind);
377
+ const flat = memoryText(text).text;
378
+ const names = { ...memoryNouns(kind), units: logicalUnits(flat), flat };
362
379
  const { findings, push } = createFindingSink(text);
363
380
 
364
381
  reportDurableClaims(text, names, push);
365
382
  const { hasOverride, hasAuthority } = reportOverride(text, names, push);
366
383
  reportDirectives(text, names, push);
384
+ reportIncidents(text, names, push);
367
385
  reportStagedPayload(text, names, push);
368
386
 
369
387
  const host = egressHost(text);
370
- if (host) {
388
+ if (host && !mentionsOnlyProhibited(names.units, host)) {
371
389
  push(
372
390
  'HIGH',
373
391
  `${names.Noun} references a data-exfiltration host (${host})`,