codegate-ai 0.1.0

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 (147) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +390 -0
  3. package/dist/cli-prompts.d.ts +6 -0
  4. package/dist/cli-prompts.js +94 -0
  5. package/dist/cli.d.ts +64 -0
  6. package/dist/cli.js +443 -0
  7. package/dist/commands/run-policy.d.ts +27 -0
  8. package/dist/commands/run-policy.js +39 -0
  9. package/dist/commands/scan-command/helpers.d.ts +28 -0
  10. package/dist/commands/scan-command/helpers.js +233 -0
  11. package/dist/commands/scan-command.d.ts +90 -0
  12. package/dist/commands/scan-command.js +403 -0
  13. package/dist/commands/undo.d.ts +5 -0
  14. package/dist/commands/undo.js +14 -0
  15. package/dist/config.d.ts +50 -0
  16. package/dist/config.js +187 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +1 -0
  19. package/dist/knowledge-base/claude-code.json +152 -0
  20. package/dist/knowledge-base/cline.json +224 -0
  21. package/dist/knowledge-base/codex.json +162 -0
  22. package/dist/knowledge-base/copilot.json +132 -0
  23. package/dist/knowledge-base/cursor.json +134 -0
  24. package/dist/knowledge-base/gemini-cli.json +112 -0
  25. package/dist/knowledge-base/jetbrains-junie.json +208 -0
  26. package/dist/knowledge-base/kiro.json +102 -0
  27. package/dist/knowledge-base/opencode.json +128 -0
  28. package/dist/knowledge-base/roo-code.json +116 -0
  29. package/dist/knowledge-base/schema.json +77 -0
  30. package/dist/knowledge-base/windsurf.json +80 -0
  31. package/dist/knowledge-base/zed.json +88 -0
  32. package/dist/layer1-discovery/config-parser.d.ts +12 -0
  33. package/dist/layer1-discovery/config-parser.js +52 -0
  34. package/dist/layer1-discovery/file-walker.d.ts +13 -0
  35. package/dist/layer1-discovery/file-walker.js +77 -0
  36. package/dist/layer1-discovery/knowledge-base.d.ts +36 -0
  37. package/dist/layer1-discovery/knowledge-base.js +58 -0
  38. package/dist/layer1-discovery/tool-detector.d.ts +20 -0
  39. package/dist/layer1-discovery/tool-detector.js +138 -0
  40. package/dist/layer2-static/detectors/command-exec.d.ts +11 -0
  41. package/dist/layer2-static/detectors/command-exec.js +343 -0
  42. package/dist/layer2-static/detectors/consent-bypass.d.ts +8 -0
  43. package/dist/layer2-static/detectors/consent-bypass.js +330 -0
  44. package/dist/layer2-static/detectors/env-override.d.ts +8 -0
  45. package/dist/layer2-static/detectors/env-override.js +132 -0
  46. package/dist/layer2-static/detectors/git-hooks.d.ts +11 -0
  47. package/dist/layer2-static/detectors/git-hooks.js +61 -0
  48. package/dist/layer2-static/detectors/ide-settings.d.ts +8 -0
  49. package/dist/layer2-static/detectors/ide-settings.js +66 -0
  50. package/dist/layer2-static/detectors/plugin-manifest.d.ts +9 -0
  51. package/dist/layer2-static/detectors/plugin-manifest.js +1943 -0
  52. package/dist/layer2-static/detectors/rule-file.d.ts +7 -0
  53. package/dist/layer2-static/detectors/rule-file.js +299 -0
  54. package/dist/layer2-static/detectors/symlink.d.ts +9 -0
  55. package/dist/layer2-static/detectors/symlink.js +45 -0
  56. package/dist/layer2-static/engine.d.ts +28 -0
  57. package/dist/layer2-static/engine.js +83 -0
  58. package/dist/layer2-static/evidence.d.ts +12 -0
  59. package/dist/layer2-static/evidence.js +128 -0
  60. package/dist/layer2-static/rule-engine.d.ts +24 -0
  61. package/dist/layer2-static/rule-engine.js +138 -0
  62. package/dist/layer2-static/state/scan-state.d.ts +32 -0
  63. package/dist/layer2-static/state/scan-state.js +296 -0
  64. package/dist/layer3-dynamic/command-builder.d.ts +15 -0
  65. package/dist/layer3-dynamic/command-builder.js +39 -0
  66. package/dist/layer3-dynamic/local-text-analysis.d.ts +19 -0
  67. package/dist/layer3-dynamic/local-text-analysis.js +73 -0
  68. package/dist/layer3-dynamic/meta-agent.d.ts +17 -0
  69. package/dist/layer3-dynamic/meta-agent.js +33 -0
  70. package/dist/layer3-dynamic/prompt-templates/local-text-analysis.md +32 -0
  71. package/dist/layer3-dynamic/prompt-templates/security-analysis.md +13 -0
  72. package/dist/layer3-dynamic/prompt-templates/tool-poisoning.md +15 -0
  73. package/dist/layer3-dynamic/resource-fetcher.d.ts +25 -0
  74. package/dist/layer3-dynamic/resource-fetcher.js +119 -0
  75. package/dist/layer3-dynamic/sandbox.d.ts +13 -0
  76. package/dist/layer3-dynamic/sandbox.js +40 -0
  77. package/dist/layer3-dynamic/tool-description-acquisition.d.ts +22 -0
  78. package/dist/layer3-dynamic/tool-description-acquisition.js +76 -0
  79. package/dist/layer3-dynamic/tool-description-scanner.d.ts +11 -0
  80. package/dist/layer3-dynamic/tool-description-scanner.js +53 -0
  81. package/dist/layer3-dynamic/toxic-flow.d.ts +12 -0
  82. package/dist/layer3-dynamic/toxic-flow.js +57 -0
  83. package/dist/layer4-remediation/actions/quarantine.d.ts +1 -0
  84. package/dist/layer4-remediation/actions/quarantine.js +8 -0
  85. package/dist/layer4-remediation/actions/remove-field.d.ts +5 -0
  86. package/dist/layer4-remediation/actions/remove-field.js +53 -0
  87. package/dist/layer4-remediation/actions/replace-value.d.ts +5 -0
  88. package/dist/layer4-remediation/actions/replace-value.js +26 -0
  89. package/dist/layer4-remediation/actions/strip-unicode.d.ts +5 -0
  90. package/dist/layer4-remediation/actions/strip-unicode.js +8 -0
  91. package/dist/layer4-remediation/backup-manager.d.ts +32 -0
  92. package/dist/layer4-remediation/backup-manager.js +138 -0
  93. package/dist/layer4-remediation/diff-generator.d.ts +6 -0
  94. package/dist/layer4-remediation/diff-generator.js +29 -0
  95. package/dist/layer4-remediation/remediation-runner.d.ts +36 -0
  96. package/dist/layer4-remediation/remediation-runner.js +230 -0
  97. package/dist/layer4-remediation/remediator.d.ts +36 -0
  98. package/dist/layer4-remediation/remediator.js +117 -0
  99. package/dist/path-display.d.ts +1 -0
  100. package/dist/path-display.js +20 -0
  101. package/dist/pipeline.d.ts +34 -0
  102. package/dist/pipeline.js +259 -0
  103. package/dist/report-summary.d.ts +6 -0
  104. package/dist/report-summary.js +48 -0
  105. package/dist/reporter/html.d.ts +2 -0
  106. package/dist/reporter/html.js +103 -0
  107. package/dist/reporter/json.d.ts +2 -0
  108. package/dist/reporter/json.js +3 -0
  109. package/dist/reporter/markdown.d.ts +2 -0
  110. package/dist/reporter/markdown.js +52 -0
  111. package/dist/reporter/sarif.d.ts +2 -0
  112. package/dist/reporter/sarif.js +84 -0
  113. package/dist/reporter/terminal.d.ts +5 -0
  114. package/dist/reporter/terminal.js +94 -0
  115. package/dist/runtime/signal-handlers.d.ts +10 -0
  116. package/dist/runtime/signal-handlers.js +17 -0
  117. package/dist/scan-target/helpers.d.ts +20 -0
  118. package/dist/scan-target/helpers.js +268 -0
  119. package/dist/scan-target/staging.d.ts +5 -0
  120. package/dist/scan-target/staging.js +114 -0
  121. package/dist/scan-target/types.d.ts +18 -0
  122. package/dist/scan-target/types.js +1 -0
  123. package/dist/scan-target.d.ts +3 -0
  124. package/dist/scan-target.js +31 -0
  125. package/dist/scan.d.ts +54 -0
  126. package/dist/scan.js +593 -0
  127. package/dist/tui/app.d.ts +10 -0
  128. package/dist/tui/app.js +21 -0
  129. package/dist/tui/theme.d.ts +8 -0
  130. package/dist/tui/theme.js +7 -0
  131. package/dist/tui/views/dashboard.d.ts +6 -0
  132. package/dist/tui/views/dashboard.js +8 -0
  133. package/dist/tui/views/deep-scan-consent.d.ts +5 -0
  134. package/dist/tui/views/deep-scan-consent.js +6 -0
  135. package/dist/tui/views/progress.d.ts +4 -0
  136. package/dist/tui/views/progress.js +6 -0
  137. package/dist/tui/views/summary.d.ts +5 -0
  138. package/dist/tui/views/summary.js +16 -0
  139. package/dist/types/discovery.d.ts +12 -0
  140. package/dist/types/discovery.js +1 -0
  141. package/dist/types/finding.d.ts +46 -0
  142. package/dist/types/finding.js +15 -0
  143. package/dist/types/report.d.ts +25 -0
  144. package/dist/types/report.js +23 -0
  145. package/dist/wrapper.d.ts +35 -0
  146. package/dist/wrapper.js +220 -0
  147. package/package.json +97 -0
@@ -0,0 +1,138 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { homedir as osHomedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import which from "which";
6
+ const defaultDeps = {
7
+ platform: process.platform,
8
+ homedir: osHomedir(),
9
+ which: (binary) => which.sync(binary, { nothrow: true }) ?? undefined,
10
+ execVersion: (binary) => {
11
+ const result = spawnSync(binary, ["--version"], { encoding: "utf8" });
12
+ if (result.status !== 0) {
13
+ return null;
14
+ }
15
+ const output = `${result.stdout ?? ""}`.trim();
16
+ return output.length > 0 ? (output.split(/\s+/u).pop() ?? output) : null;
17
+ },
18
+ pathExists: (path) => existsSync(path),
19
+ listDirectory: (path) => {
20
+ try {
21
+ return readdirSync(path);
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ },
27
+ };
28
+ function appCandidates(tool, platform, home) {
29
+ if (tool === "cursor") {
30
+ if (platform === "darwin")
31
+ return ["/Applications/Cursor.app"];
32
+ if (platform === "linux")
33
+ return [join(home, ".local/share/cursor"), "/opt/cursor"];
34
+ return [join(home, "AppData/Local/Programs/Cursor")];
35
+ }
36
+ if (tool === "windsurf") {
37
+ if (platform === "darwin")
38
+ return ["/Applications/Windsurf.app"];
39
+ if (platform === "linux")
40
+ return [join(home, ".local/share/windsurf"), "/opt/windsurf"];
41
+ return [join(home, "AppData/Local/Programs/Windsurf")];
42
+ }
43
+ if (tool === "kiro") {
44
+ if (platform === "darwin")
45
+ return ["/Applications/Kiro.app"];
46
+ if (platform === "linux")
47
+ return [join(home, ".local/share/kiro"), "/opt/kiro"];
48
+ return [join(home, "AppData/Local/Programs/Kiro")];
49
+ }
50
+ if (tool === "jetbrains") {
51
+ if (platform === "darwin") {
52
+ return [
53
+ "/Applications/JetBrains Toolbox/JetBrains Toolbox.app",
54
+ join(home, "Library/Application Support/JetBrains/Toolbox/apps"),
55
+ "/Applications/IntelliJ IDEA.app",
56
+ "/Applications/WebStorm.app",
57
+ "/Applications/PyCharm.app",
58
+ ];
59
+ }
60
+ if (platform === "linux") {
61
+ return [join(home, ".local/share/JetBrains/Toolbox/apps"), "/opt/jetbrains-toolbox/apps"];
62
+ }
63
+ return [
64
+ join(home, "AppData/Local/JetBrains/Toolbox/apps"),
65
+ join(home, "AppData/Roaming/JetBrains/Toolbox/apps"),
66
+ ];
67
+ }
68
+ return [];
69
+ }
70
+ function detectCopilot(deps) {
71
+ const extensionsDir = join(deps.homedir, ".vscode/extensions");
72
+ const entries = deps.listDirectory(extensionsDir);
73
+ const copilot = entries.find((entry) => entry.startsWith("github.copilot-"));
74
+ if (!copilot) {
75
+ return {
76
+ tool: "github-copilot",
77
+ installed: false,
78
+ version: null,
79
+ path: null,
80
+ source: "none",
81
+ };
82
+ }
83
+ const version = copilot.split("github.copilot-")[1] ?? null;
84
+ return {
85
+ tool: "github-copilot",
86
+ installed: true,
87
+ version,
88
+ path: join(extensionsDir, copilot),
89
+ source: "extension",
90
+ };
91
+ }
92
+ function detectCliOrAppTool(tool, binary, deps, options) {
93
+ if (binary) {
94
+ const resolved = deps.which(binary);
95
+ if (resolved) {
96
+ return {
97
+ tool,
98
+ installed: true,
99
+ version: options.includeVersions === false ? null : deps.execVersion(binary),
100
+ path: resolved,
101
+ source: "path",
102
+ };
103
+ }
104
+ }
105
+ for (const candidate of appCandidates(tool, deps.platform, deps.homedir)) {
106
+ if (deps.pathExists(candidate)) {
107
+ return {
108
+ tool,
109
+ installed: true,
110
+ version: null,
111
+ path: candidate,
112
+ source: "app-bundle",
113
+ };
114
+ }
115
+ }
116
+ return {
117
+ tool,
118
+ installed: false,
119
+ version: null,
120
+ path: null,
121
+ source: "none",
122
+ };
123
+ }
124
+ export function detectTools(customDeps = defaultDeps, options = {}) {
125
+ const mappings = [
126
+ { tool: "claude-code", binary: "claude" },
127
+ { tool: "codex-cli", binary: "codex" },
128
+ { tool: "opencode", binary: "opencode" },
129
+ { tool: "cursor", binary: "cursor" },
130
+ { tool: "windsurf", binary: "windsurf" },
131
+ { tool: "kiro", binary: "kiro" },
132
+ { tool: "vscode", binary: "code" },
133
+ { tool: "jetbrains", binary: "idea" },
134
+ ];
135
+ const detections = mappings.map((entry) => detectCliOrAppTool(entry.tool, entry.binary, customDeps, options));
136
+ detections.push(detectCopilot(customDeps));
137
+ return detections;
138
+ }
@@ -0,0 +1,11 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface CommandExecInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ knownSafeMcpServers: string[];
7
+ knownSafeFormatters: string[];
8
+ knownSafeLspServers: string[];
9
+ blockedCommands: string[];
10
+ }
11
+ export declare function detectCommandExecution(input: CommandExecInput): Finding[];
@@ -0,0 +1,343 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ const LAUNCHERS = new Set(["npx", "uvx", "node", "python", "python3", "deno", "bun"]);
3
+ const SHELL_META_PATTERN = /[|;&`]|[$][(]/u;
4
+ const COMMAND_FIELD_KEYS = new Set([
5
+ "command",
6
+ "cmd",
7
+ "run",
8
+ "runcommand",
9
+ "script",
10
+ "exec",
11
+ "execute",
12
+ "shell",
13
+ "shellcommand",
14
+ "commandline",
15
+ ]);
16
+ const TEMPLATE_COMMAND_KEYS = new Set([
17
+ "command",
18
+ "cmd",
19
+ "run",
20
+ "runcommand",
21
+ "script",
22
+ "exec",
23
+ "execute",
24
+ "shell",
25
+ "shellcommand",
26
+ "commandline",
27
+ "program",
28
+ "binary",
29
+ "executable",
30
+ "file",
31
+ "launcher",
32
+ ]);
33
+ const TEMPLATE_ARGUMENT_KEYS = new Set([
34
+ "args",
35
+ "arguments",
36
+ "commandarguments",
37
+ "argv",
38
+ "params",
39
+ "parameters",
40
+ "commandargs",
41
+ ]);
42
+ const IMPLICIT_COMMAND_CONTEXT_KEYS = new Set([
43
+ "hook",
44
+ "hooks",
45
+ "workflow",
46
+ "workflows",
47
+ "mcp",
48
+ "mcpserver",
49
+ "mcpservers",
50
+ "server",
51
+ "servers",
52
+ "plugin",
53
+ "plugins",
54
+ "extension",
55
+ "extensions",
56
+ "formatter",
57
+ "formatters",
58
+ "lsp",
59
+ "lsps",
60
+ "command",
61
+ "commands",
62
+ "run",
63
+ "exec",
64
+ "execute",
65
+ "script",
66
+ ]);
67
+ function makeFinding(filePath, field, ruleId, severity, description, evidence) {
68
+ const location = { field };
69
+ if (typeof evidence?.line === "number") {
70
+ location.line = evidence.line;
71
+ }
72
+ if (typeof evidence?.column === "number") {
73
+ location.column = evidence.column;
74
+ }
75
+ return {
76
+ rule_id: ruleId,
77
+ finding_id: `COMMAND_EXEC-${filePath}-${field}`,
78
+ severity,
79
+ category: "COMMAND_EXEC",
80
+ layer: "L2",
81
+ file_path: filePath,
82
+ location,
83
+ description,
84
+ affected_tools: [
85
+ "claude-code",
86
+ "codex-cli",
87
+ "opencode",
88
+ "cursor",
89
+ "windsurf",
90
+ "github-copilot",
91
+ ],
92
+ cve: null,
93
+ owasp: ["ASI02", "ASI05"],
94
+ cwe: "CWE-78",
95
+ confidence: "HIGH",
96
+ fixable: true,
97
+ remediation_actions: ["remove_field", "replace_with_default"],
98
+ evidence: evidence?.evidence ?? null,
99
+ suppressed: false,
100
+ };
101
+ }
102
+ function isRecord(value) {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
104
+ }
105
+ function normalizeKey(value) {
106
+ return value.replace(/[^a-z0-9]/giu, "").toLowerCase();
107
+ }
108
+ function extractPackageFromPath(token) {
109
+ const normalized = token.replaceAll("\\", "/");
110
+ const scopedMatch = normalized.match(/node_modules\/(@[^/]+\/[^/]+)/u);
111
+ if (scopedMatch?.[1]) {
112
+ return scopedMatch[1];
113
+ }
114
+ const plainMatch = normalized.match(/node_modules\/([^/]+)/u);
115
+ return plainMatch?.[1] ?? null;
116
+ }
117
+ function extractIdentifier(tokens) {
118
+ for (const token of tokens) {
119
+ if (token.startsWith("-")) {
120
+ continue;
121
+ }
122
+ if (LAUNCHERS.has(token)) {
123
+ continue;
124
+ }
125
+ const fromPath = extractPackageFromPath(token);
126
+ if (fromPath) {
127
+ return fromPath;
128
+ }
129
+ return token;
130
+ }
131
+ return null;
132
+ }
133
+ function normalizeIdentifier(value) {
134
+ const trimmed = value.trim();
135
+ if (trimmed.length === 0) {
136
+ return "";
137
+ }
138
+ const fromPath = extractPackageFromPath(trimmed);
139
+ const candidate = (fromPath ?? trimmed)
140
+ .replaceAll("\\", "/")
141
+ .replace(/[#?].*$/u, "")
142
+ .replace(/\/+$/u, "");
143
+ const looksLikePackageOrTool = /^@?[a-z0-9._-]+(?:\/[a-z0-9._-]+)?$/iu.test(candidate);
144
+ return looksLikePackageOrTool ? candidate.toLowerCase() : candidate;
145
+ }
146
+ function isAllowlistedIdentifier(identifier, allowlist) {
147
+ const normalizedIdentifier = normalizeIdentifier(identifier);
148
+ if (normalizedIdentifier.length === 0) {
149
+ return false;
150
+ }
151
+ const normalizedAllowlist = new Set(allowlist.map((entry) => normalizeIdentifier(entry)).filter((entry) => entry.length > 0));
152
+ return normalizedAllowlist.has(normalizedIdentifier);
153
+ }
154
+ function commandTokensFromValue(value) {
155
+ if (typeof value === "string") {
156
+ const tokens = value.split(/\s+/u).filter((part) => part.length > 0);
157
+ return tokens.length > 0 ? tokens : null;
158
+ }
159
+ if (Array.isArray(value) && value.every((token) => typeof token === "string")) {
160
+ const tokens = value.map((token) => token.trim()).filter((token) => token.length > 0);
161
+ return tokens.length > 0 ? tokens : null;
162
+ }
163
+ return null;
164
+ }
165
+ function commandTokensFromTemplateObject(value) {
166
+ let commandTokens = null;
167
+ const argumentTokens = [];
168
+ const nestedCommandTemplates = [];
169
+ for (const [key, child] of Object.entries(value)) {
170
+ const normalizedKey = normalizeKey(key);
171
+ if (TEMPLATE_COMMAND_KEYS.has(normalizedKey)) {
172
+ const direct = commandTokensFromValue(child);
173
+ if (!commandTokens && direct) {
174
+ commandTokens = direct;
175
+ continue;
176
+ }
177
+ if (!commandTokens && isRecord(child)) {
178
+ nestedCommandTemplates.push(child);
179
+ }
180
+ continue;
181
+ }
182
+ if (TEMPLATE_ARGUMENT_KEYS.has(normalizedKey)) {
183
+ const parsedArgs = commandTokensFromValue(child);
184
+ if (parsedArgs) {
185
+ argumentTokens.push(...parsedArgs);
186
+ }
187
+ }
188
+ }
189
+ if (!commandTokens) {
190
+ for (const nested of nestedCommandTemplates) {
191
+ const nestedTokens = commandTokensFromTemplateObject(nested);
192
+ if (!nestedTokens) {
193
+ continue;
194
+ }
195
+ commandTokens = nestedTokens;
196
+ break;
197
+ }
198
+ }
199
+ if (!commandTokens) {
200
+ return null;
201
+ }
202
+ return [...commandTokens, ...argumentTokens];
203
+ }
204
+ function isLikelyExecutableContext(path) {
205
+ const segments = path.split(".").map((segment) => normalizeKey(segment));
206
+ return segments.some((segment) => IMPLICIT_COMMAND_CONTEXT_KEYS.has(segment));
207
+ }
208
+ function isAllowlisted(identifier, path, input) {
209
+ if (!identifier) {
210
+ return false;
211
+ }
212
+ if (path.includes("formatter")) {
213
+ return isAllowlistedIdentifier(identifier, input.knownSafeFormatters);
214
+ }
215
+ if (path.includes("lsp")) {
216
+ return isAllowlistedIdentifier(identifier, input.knownSafeLspServers);
217
+ }
218
+ return isAllowlistedIdentifier(identifier, input.knownSafeMcpServers);
219
+ }
220
+ function collectCommandEntries(value, prefix = "") {
221
+ if (!value || typeof value !== "object") {
222
+ return [];
223
+ }
224
+ const record = value;
225
+ const entries = [];
226
+ const hasExplicitCommandField = Object.keys(record).some((key) => COMMAND_FIELD_KEYS.has(normalizeKey(key)));
227
+ if (!hasExplicitCommandField && prefix.length > 0 && isLikelyExecutableContext(prefix)) {
228
+ const implicitCommandTokens = commandTokensFromTemplateObject(record);
229
+ if (implicitCommandTokens) {
230
+ entries.push({
231
+ path: prefix,
232
+ contextPath: prefix,
233
+ command: implicitCommandTokens,
234
+ context: record,
235
+ });
236
+ }
237
+ }
238
+ for (const [key, child] of Object.entries(record)) {
239
+ const path = prefix ? `${prefix}.${key}` : key;
240
+ if (COMMAND_FIELD_KEYS.has(normalizeKey(key))) {
241
+ const directCommandTokens = commandTokensFromValue(child);
242
+ if (directCommandTokens) {
243
+ entries.push({
244
+ path,
245
+ contextPath: prefix,
246
+ command: directCommandTokens,
247
+ context: record,
248
+ });
249
+ }
250
+ else if (isRecord(child)) {
251
+ const objectCommandTokens = commandTokensFromTemplateObject(child);
252
+ if (objectCommandTokens) {
253
+ entries.push({
254
+ path,
255
+ contextPath: path,
256
+ command: objectCommandTokens,
257
+ context: child,
258
+ });
259
+ }
260
+ }
261
+ continue;
262
+ }
263
+ entries.push(...collectCommandEntries(child, path));
264
+ }
265
+ return entries;
266
+ }
267
+ function normalizeMarkupCommandText(value) {
268
+ return value
269
+ .replace(/<[^>]+>/gu, " ")
270
+ .replace(/\s+/gu, " ")
271
+ .trim();
272
+ }
273
+ function collectMarkdownExecuteCommandEntries(value) {
274
+ const entries = [];
275
+ const executeCommandPattern = /<execute_command\b[^>]*>([\s\S]*?)<\/execute_command>/giu;
276
+ const commandPattern = /<command\b[^>]*>([\s\S]*?)<\/command>/giu;
277
+ let commandIndex = 0;
278
+ for (const executeMatch of value.matchAll(executeCommandPattern)) {
279
+ const block = executeMatch[1];
280
+ if (typeof block !== "string" || block.length === 0) {
281
+ continue;
282
+ }
283
+ for (const commandMatch of block.matchAll(commandPattern)) {
284
+ const commandText = commandMatch[1];
285
+ if (typeof commandText !== "string") {
286
+ continue;
287
+ }
288
+ const normalized = normalizeMarkupCommandText(commandText);
289
+ const commandTokens = commandTokensFromValue(normalized);
290
+ if (!commandTokens) {
291
+ continue;
292
+ }
293
+ const path = `markdown.execute_command.${commandIndex}`;
294
+ entries.push({
295
+ path,
296
+ contextPath: path,
297
+ command: commandTokens,
298
+ context: {},
299
+ });
300
+ commandIndex += 1;
301
+ }
302
+ }
303
+ return entries;
304
+ }
305
+ export function detectCommandExecution(input) {
306
+ const findings = [];
307
+ const entries = [
308
+ ...collectCommandEntries(input.parsed),
309
+ ...(typeof input.parsed === "string" ? collectMarkdownExecuteCommandEntries(input.parsed) : []),
310
+ ];
311
+ for (const entry of entries) {
312
+ const identifier = extractIdentifier(entry.command);
313
+ if (isAllowlisted(identifier, entry.path, input)) {
314
+ continue;
315
+ }
316
+ const commandLine = entry.command.join(" ");
317
+ const hasBlockedBinary = entry.command.some((token) => input.blockedCommands.includes(token));
318
+ const hasShellMeta = SHELL_META_PATTERN.test(commandLine);
319
+ const hasNetworkUtility = /\b(curl|wget|nc|ncat|socat)\b/u.test(commandLine);
320
+ if (hasBlockedBinary || hasShellMeta || hasNetworkUtility) {
321
+ const evidenceValue = `${entry.path} = ${JSON.stringify(entry.command)}`;
322
+ const evidence = buildFindingEvidence({
323
+ textContent: input.textContent,
324
+ jsonPaths: [entry.contextPath, entry.path],
325
+ searchTerms: [commandLine, ...entry.command],
326
+ fallbackValue: evidenceValue,
327
+ });
328
+ findings.push(makeFinding(input.filePath, entry.path, "command-exec-suspicious", "CRITICAL", `Suspicious command execution pattern detected: ${commandLine}`, evidence));
329
+ }
330
+ if ((entry.context.stdout === "ignore" || entry.context.stderr === "ignore") &&
331
+ (entry.path.includes("formatter") || entry.contextPath.includes("formatter"))) {
332
+ const stdoutField = entry.contextPath.length > 0 ? `${entry.contextPath}.stdout` : "stdout";
333
+ const evidence = buildFindingEvidence({
334
+ textContent: input.textContent,
335
+ jsonPaths: [stdoutField, entry.contextPath],
336
+ searchTerms: ["stdout", "ignore"],
337
+ fallbackValue: `${stdoutField} = ${JSON.stringify(entry.context.stdout)}`,
338
+ });
339
+ findings.push(makeFinding(input.filePath, stdoutField, "formatter-output-suppression", "HIGH", "Formatter output suppression hides command output", evidence));
340
+ }
341
+ }
342
+ return findings;
343
+ }
@@ -0,0 +1,8 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface ConsentBypassInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ trustedApiDomains?: string[];
7
+ }
8
+ export declare function detectConsentBypass(input: ConsentBypassInput): Finding[];