@shomra/agent 0.3.28 → 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 (84) 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/rules-javascript.mjs +16 -3
  23. package/src/detect/sast/rules-python.mjs +8 -6
  24. package/src/detect/sast/scanner.mjs +1 -1
  25. package/src/detect/signals/agent-frameworks.mjs +231 -0
  26. package/src/detect/signals/agent-graph-surface.mjs +113 -0
  27. package/src/detect/signals/agentic-ci-surface.mjs +314 -0
  28. package/src/detect/signals/agentic-shim.mjs +82 -0
  29. package/src/detect/signals/artifacts.mjs +8 -36
  30. package/src/detect/signals/autonomy.mjs +9 -1
  31. package/src/detect/signals/chat-template.mjs +211 -0
  32. package/src/detect/signals/ci-workflow.mjs +169 -0
  33. package/src/detect/signals/credential-harvest.mjs +1 -1
  34. package/src/detect/signals/execution-hijack.mjs +4 -2
  35. package/src/detect/signals/gate.mjs +84 -11
  36. package/src/detect/signals/guardrail-shape.mjs +564 -0
  37. package/src/detect/signals/guardrail-surface.mjs +221 -0
  38. package/src/detect/signals/injection.mjs +8 -0
  39. package/src/detect/signals/inspect-shim.mjs +7 -0
  40. package/src/detect/signals/instruction-paths.mjs +60 -0
  41. package/src/detect/signals/manifests.mjs +302 -0
  42. package/src/detect/signals/masking.mjs +14 -1
  43. package/src/detect/signals/mcp-advisories.mjs +109 -0
  44. package/src/detect/signals/mcp-config.mjs +598 -0
  45. package/src/detect/signals/memory-directives.mjs +661 -0
  46. package/src/detect/signals/memory-locations.mjs +158 -0
  47. package/src/detect/signals/memory.mjs +56 -31
  48. package/src/detect/signals/model-config-rules.mjs +655 -0
  49. package/src/detect/signals/model-config.mjs +61 -0
  50. package/src/detect/signals/packages.mjs +2 -2
  51. package/src/detect/signals/prose-context.mjs +6 -9
  52. package/src/detect/signals/scan.mjs +4 -4
  53. package/src/detect/signals/secret-scanner.mjs +241 -0
  54. package/src/detect/signals/secrets.mjs +1 -48
  55. package/src/detect/signals/shell.mjs +10 -10
  56. package/src/gate/advisories.mjs +16 -0
  57. package/src/gate/batch.mjs +10 -0
  58. package/src/gate/environment.mjs +8 -53
  59. package/src/guard/artifact-paths.mjs +107 -0
  60. package/src/guard/classify.mjs +165 -7
  61. package/src/guard/command-resolve.mjs +35 -5
  62. package/src/guard/memory-write.mjs +218 -0
  63. package/src/guard/prompt-guard.mjs +0 -1
  64. package/src/guard/tool-guard.mjs +52 -77
  65. package/src/inventory/agent-posture.mjs +236 -57
  66. package/src/inventory/artifacts/classify.mjs +10 -1
  67. package/src/inventory/artifacts/discover.mjs +113 -3
  68. package/src/inventory/artifacts/extensions.mjs +70 -0
  69. package/src/inventory/artifacts/hook-scripts.mjs +128 -0
  70. package/src/inventory/artifacts/limits.mjs +1 -1
  71. package/src/inventory/artifacts/plugins.mjs +105 -0
  72. package/src/inventory/artifacts/roots.mjs +40 -0
  73. package/src/inventory/discovery/ai-dependencies.mjs +39 -12
  74. package/src/inventory/discovery/all.mjs +4 -0
  75. package/src/inventory/discovery/cloud-clis.mjs +472 -0
  76. package/src/inventory/discovery/coding-agents.mjs +19 -4
  77. package/src/inventory/discovery/mcp-clients.mjs +16 -10
  78. package/src/inventory/discovery/mcp-servers.mjs +125 -35
  79. package/src/inventory/discovery/mcp-stores.mjs +207 -0
  80. package/src/inventory/env-redirect.mjs +148 -0
  81. package/src/inventory/grant-extract.mjs +463 -0
  82. package/src/inventory/project-roots.mjs +108 -0
  83. package/src/inventory/vscode-state.mjs +153 -0
  84. 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,12 +5,20 @@ 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;
12
13
 
13
- const PRECEDENCE_MARKERS = /\b(regardless of (what|any|your|the)|supersede?s?|takes? precedence|highest[- ]priority|overrid(e|ing|es)\b[^.\n]{0,30}\b(instruction|prompt|rule|system|user|guidance|directive|context|behaviou?r|polic|guardrail|safety))\b/i;
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;
15
+
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`;
17
+
18
+ const PRECEDENCE_MARKERS = new RegExp(
19
+ String.raw`\b(regardless of (what|any|your|the)|(supersede?s?|takes? precedence|highest[- ]priority|overrid(e|ing|es))${PRECEDENCE_OBJECT})`,
20
+ 'i',
21
+ );
14
22
 
15
23
  const OVERRIDE_MARKERS = new RegExp(`${MALICIOUS_OVERRIDE.source}|${PRECEDENCE_MARKERS.source}`, 'i');
16
24
 
@@ -26,7 +34,8 @@ const NEGATION_GUARD = /\b(never|do not|don'?t|cannot|can'?t|avoid|refuse|must n
26
34
 
27
35
  const SABOTAGE_RULES = [
28
36
 
29
- { 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 },
30
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 },
31
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 },
32
41
 
@@ -51,20 +60,23 @@ export function offendingLine(sig, text) {
51
60
  return null;
52
61
  }
53
62
 
54
- function firstDirectiveLine(text, re) {
55
- for (const line of text.split(/\r?\n/)) {
56
- if (!re.test(line)) continue;
57
- if (NEGATION_GUARD.test(line)) continue;
58
- if (isDescriptiveLine(line)) continue;
59
- if (citationGoverns(line, re.exec(line)?.index)) 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;
69
+ if (isDescriptiveLine(stripSelfReference(line))) continue;
70
+ if (citationGoverns(line, m.index)) continue;
71
+ if (quotedMention(line, m.index)) continue;
60
72
  return line;
61
73
  }
62
74
  return null;
63
75
  }
64
76
 
65
- function lineMatchingAll(text, ...res) {
66
- for (const line of text.split(/\r?\n/)) {
67
- 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;
68
80
  }
69
81
  return null;
70
82
  }
@@ -87,12 +99,12 @@ const PASSIVE_SEND_RE =
87
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;
88
100
 
89
101
  const EXFIL_RULES = [
90
- { re: /\b(exfiltrat|smuggl)\w*/i, label: 'exfiltration', severity: 'CRITICAL', descGuard: true },
91
- { 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 },
92
104
 
93
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 },
94
- { 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 },
95
- { 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' },
96
108
 
97
109
  {
98
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,
@@ -102,15 +114,16 @@ const EXFIL_RULES = [
102
114
  },
103
115
  ];
104
116
 
105
- function scanDirectives(text) {
117
+ function scanDirectives(units) {
106
118
  const sabotage = new Map(), exfil = new Map();
107
- for (const line of text.split(/\r?\n/)) {
119
+ for (const { text: line } of units) {
108
120
  for (const r of SABOTAGE_RULES) {
109
121
  const m = r.re.exec(line);
110
122
  if (!m) continue;
111
- if (r.guarded && NEGATION_GUARD.test(line)) continue;
123
+ if (r.guarded && negatedAround(line, m.index, m[0].length)) continue;
112
124
  if (r.guarded && isDescriptiveLine(line)) continue;
113
125
  if (r.guarded && citationGoverns(line, m.index)) continue;
126
+ if (r.guarded && quotedMention(line, m.index)) continue;
114
127
  if (r.context && !r.context.test(line)) continue;
115
128
  if (!sabotage.has(r.label)) sabotage.set(r.label, line);
116
129
  }
@@ -118,9 +131,10 @@ function scanDirectives(text) {
118
131
  const m = r.re.exec(line);
119
132
  if (!m) continue;
120
133
 
121
- if (NEGATION_GUARD.test(line)) continue;
134
+ if (negatedAround(line, m.index, m[0].length)) continue;
122
135
  if (r.descGuard && isDescriptiveLine(line)) continue;
123
136
  if (r.descGuard && citationGoverns(line, m.index)) continue;
137
+ if (r.descGuard && (quotedMention(line, m.index) || describesAt(line, m.index))) continue;
124
138
  if (r.descGuard && isRiskTableRow(line)) continue;
125
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;
126
140
  if (r.label === 'send-to-external' && m.index != null && insideMarkdownLinkLabel(line, m.index)) continue;
@@ -142,6 +156,8 @@ const SELF_REFERENCE =
142
156
  const SELF_RECREATE =
143
157
  /\b(re-?(add|writ(e|ing)|creat(e|ing)|insert(ing)?|instat(e|ing)|appl(y|ying)|introduc(e|ing))|restor(e|ing)|recreat(e|ing)|reinstat(e|ing)|re-?establish(ing)?|put .{0,20}back|add .{0,20}back)\b/i;
144
158
 
159
+ const stripSelfReference = (line) => String(line).replace(SELF_REFERENCE, ' ');
160
+
145
161
  const SELF_PROPAGATE =
146
162
  /\b(copy|copies|duplicat(e|ing)|replicat(e|ing)|propagat(e|ing)|carry (it |this )?over|mirror|append|add|includ(e|ing)|writ(e|ing)|sav(e|ing))\b[^.\n]{0,60}\b(every|each|all|any (new|other)|other|future|subsequent)\b[^.\n]{0,40}\b(session|conversation|chat|project|repo|repositor(y|ies)|workspace|memor(y|ies)|context|file|store)s?\b/i;
147
163
 
@@ -195,9 +211,10 @@ function reportDurableClaims(text, { isInstruction, Noun }, push) {
195
211
  );
196
212
  }
197
213
 
198
- function reportOverride(text, { isInstruction, noun }, push) {
199
- const overrideLine = firstDirectiveLine(text, isInstruction ? MALICIOUS_OVERRIDE : OVERRIDE_MARKERS);
200
- 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);
201
218
  const hasOverride = !!overrideLine;
202
219
  const hasAuthority = !!authorityLine;
203
220
 
@@ -214,7 +231,7 @@ function reportOverride(text, { isInstruction, noun }, push) {
214
231
  return { hasOverride, hasAuthority };
215
232
  }
216
233
 
217
- const durableImperativeLine = isInstruction ? null : lineMatchingAll(text, PERSISTENCE_MARKERS, IMPERATIVE);
234
+ const durableImperativeLine = isInstruction ? null : unitMatchingAll(units, PERSISTENCE_DIRECTIVE, IMPERATIVE);
218
235
  if (durableImperativeLine && !isDescriptiveLine(durableImperativeLine)) {
219
236
  push(
220
237
  'HIGH',
@@ -226,8 +243,8 @@ function reportOverride(text, { isInstruction, noun }, push) {
226
243
  return { hasOverride, hasAuthority };
227
244
  }
228
245
 
229
- function reportDirectives(text, { noun }, push) {
230
- const { sabotage, exfil } = scanDirectives(text);
246
+ function reportDirectives(text, { noun, units }, push) {
247
+ const { sabotage, exfil } = scanDirectives(units);
231
248
 
232
249
  if (sabotage.size) {
233
250
  push(
@@ -277,9 +294,15 @@ function hasWordFrom(line, words) {
277
294
  });
278
295
  }
279
296
 
280
- 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) {
281
304
  if (!IMPERATIVE.test(text)) return;
282
- 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)
283
306
  && !NEGATION_GUARD.test(candidate)
284
307
  && hasWordFrom(candidate, SENSITIVE_READ)
285
308
  && hasWordFrom(candidate, NETWORK_VERBS)
@@ -351,16 +374,18 @@ function dedupeByTitle(findings) {
351
374
 
352
375
  export function localMemory(content, { kind = 'MEMORY' } = {}) {
353
376
  const text = content || '';
354
- const names = memoryNouns(kind);
377
+ const flat = memoryText(text).text;
378
+ const names = { ...memoryNouns(kind), units: logicalUnits(flat), flat };
355
379
  const { findings, push } = createFindingSink(text);
356
380
 
357
381
  reportDurableClaims(text, names, push);
358
382
  const { hasOverride, hasAuthority } = reportOverride(text, names, push);
359
383
  reportDirectives(text, names, push);
384
+ reportIncidents(text, names, push);
360
385
  reportStagedPayload(text, names, push);
361
386
 
362
387
  const host = egressHost(text);
363
- if (host) {
388
+ if (host && !mentionsOnlyProhibited(names.units, host)) {
364
389
  push(
365
390
  'HIGH',
366
391
  `${names.Noun} references a data-exfiltration host (${host})`,