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
package/dist/scan.js ADDED
@@ -0,0 +1,593 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, relative, resolve, sep } from "node:path";
4
+ import { collectLocalTextAnalysisTargets, } from "./layer3-dynamic/local-text-analysis.js";
5
+ import { runStaticPipeline } from "./pipeline.js";
6
+ import { applyReportSummary } from "./report-summary.js";
7
+ import { parseConfigContent, parseConfigFile, } from "./layer1-discovery/config-parser.js";
8
+ import { loadKnowledgeBase, } from "./layer1-discovery/knowledge-base.js";
9
+ import { detectTools } from "./layer1-discovery/tool-detector.js";
10
+ import { walkProjectTree } from "./layer1-discovery/file-walker.js";
11
+ import { evaluateScanStateSnapshots, extractMcpServerSnapshots, loadScanState, saveScanState, } from "./layer2-static/state/scan-state.js";
12
+ const MCP_SERVER_CONTAINER_KEYS = ["mcpServers", "mcp_servers", "context_servers"];
13
+ const REMOTE_MCP_SERVER_ARRAY_KEYS = ["remoteMCPServers", "remote_mcp_servers"];
14
+ const USER_SCOPE_WILDCARD_MAX_DEPTH = 6;
15
+ const USER_SCOPE_WILDCARD_MAX_FILES = 500;
16
+ const INFERRED_ARTIFACT_RULES = [
17
+ { pattern: /(?:^|\/)agents\.md$/iu, format: "markdown", tool: "claude-code" },
18
+ { pattern: /(?:^|\/)claude\.md$/iu, format: "markdown", tool: "claude-code" },
19
+ { pattern: /(?:^|\/)codex\.md$/iu, format: "markdown", tool: "codex-cli" },
20
+ { pattern: /(?:^|\/)skill\.md$/iu, format: "markdown", tool: "codex-cli" },
21
+ { pattern: /(?:^|\/)[^/]+\.mdc$/iu, format: "markdown", tool: "cursor" },
22
+ { pattern: /(?:^|\/)plugins\.json$/iu, format: "json", tool: "opencode" },
23
+ { pattern: /(?:^|\/)extensions\.json$/iu, format: "json", tool: "vscode" },
24
+ { pattern: /(?:^|\/)marketplace\.json$/iu, format: "json", tool: "roo-code" },
25
+ { pattern: /(?:^|\/)product\.json$/iu, format: "json", tool: "kiro" },
26
+ ];
27
+ function escapeRegex(value) {
28
+ return value.replace(/[|\\{}()[\]^$+?.*]/g, "\\$&");
29
+ }
30
+ function wildcardToRegex(pattern) {
31
+ let escaped = escapeRegex(pattern);
32
+ escaped = escaped.replace(/\\\*\\\*\//g, "(?:[^/]+/)*");
33
+ escaped = escaped.replace(/\\\*\\\*/g, ".*");
34
+ escaped = escaped.replace(/\\\*/g, "[^/]*");
35
+ return new RegExp(`^${escaped}$`, "u");
36
+ }
37
+ function normalizePathForMatch(path) {
38
+ return path.split(sep).join("/");
39
+ }
40
+ function normalizeUserScopePattern(pattern) {
41
+ return normalizePathForMatch(pattern).replace(/^~\//u, "").replace(/^\/+/u, "");
42
+ }
43
+ function gatherCandidatePatterns(kb) {
44
+ const candidates = [];
45
+ for (const entry of kb.entries) {
46
+ for (const configPath of entry.config_paths) {
47
+ candidates.push({
48
+ pattern: normalizePathForMatch(configPath.path),
49
+ format: configPath.format,
50
+ tool: entry.tool,
51
+ scope: configPath.scope,
52
+ });
53
+ }
54
+ for (const skillPath of entry.skill_paths ?? []) {
55
+ const format = skillPath.path.endsWith(".md") ? "markdown" : "text";
56
+ candidates.push({
57
+ pattern: normalizePathForMatch(skillPath.path),
58
+ format,
59
+ tool: entry.tool,
60
+ scope: skillPath.scope,
61
+ });
62
+ }
63
+ }
64
+ return candidates;
65
+ }
66
+ function isRegularFile(path) {
67
+ try {
68
+ return statSync(path).isFile();
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ function toUserReportPath(pattern) {
75
+ const normalized = normalizeUserScopePattern(pattern);
76
+ return `~/${normalized}`;
77
+ }
78
+ function userScopeWildcardBaseDir(pattern) {
79
+ const normalized = normalizeUserScopePattern(pattern);
80
+ const wildcardIndex = normalized.indexOf("*");
81
+ if (wildcardIndex < 0) {
82
+ return normalized;
83
+ }
84
+ const prefix = normalized.slice(0, wildcardIndex);
85
+ const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
86
+ if (trimmedPrefix.length === 0) {
87
+ return "";
88
+ }
89
+ const slashIndex = trimmedPrefix.lastIndexOf("/");
90
+ return slashIndex >= 0 ? trimmedPrefix.slice(0, slashIndex) : "";
91
+ }
92
+ function collectUserScopeWildcardMatches(homeDir, pattern) {
93
+ const normalizedPattern = normalizeUserScopePattern(pattern);
94
+ const matchRegex = wildcardToRegex(normalizedPattern);
95
+ const baseDir = resolve(homeDir, userScopeWildcardBaseDir(normalizedPattern));
96
+ if (!existsSync(baseDir)) {
97
+ return [];
98
+ }
99
+ try {
100
+ if (!statSync(baseDir).isDirectory()) {
101
+ return [];
102
+ }
103
+ }
104
+ catch {
105
+ return [];
106
+ }
107
+ const matches = [];
108
+ const queue = [{ dir: baseDir, depth: 0 }];
109
+ while (queue.length > 0 && matches.length < USER_SCOPE_WILDCARD_MAX_FILES) {
110
+ const current = queue.pop();
111
+ if (!current) {
112
+ break;
113
+ }
114
+ let entries;
115
+ try {
116
+ entries = readdirSync(current.dir, { withFileTypes: true });
117
+ }
118
+ catch {
119
+ continue;
120
+ }
121
+ for (const entry of entries) {
122
+ if (matches.length >= USER_SCOPE_WILDCARD_MAX_FILES) {
123
+ break;
124
+ }
125
+ const absolutePath = join(current.dir, entry.name);
126
+ if (entry.isSymbolicLink()) {
127
+ continue;
128
+ }
129
+ if (entry.isDirectory()) {
130
+ if (current.depth < USER_SCOPE_WILDCARD_MAX_DEPTH) {
131
+ queue.push({ dir: absolutePath, depth: current.depth + 1 });
132
+ }
133
+ continue;
134
+ }
135
+ if (!entry.isFile()) {
136
+ continue;
137
+ }
138
+ const relativePath = normalizePathForMatch(relative(homeDir, absolutePath));
139
+ if (relativePath.startsWith("..")) {
140
+ continue;
141
+ }
142
+ if (!matchRegex.test(relativePath)) {
143
+ continue;
144
+ }
145
+ matches.push({ absolutePath, relativePath });
146
+ }
147
+ }
148
+ return matches;
149
+ }
150
+ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, options) {
151
+ const selected = new Map();
152
+ const filesByRelativePath = walkedFiles
153
+ .map((filePath) => ({
154
+ absolutePath: filePath,
155
+ relativePath: normalizePathForMatch(relative(absoluteTarget, filePath)),
156
+ }))
157
+ .filter((entry) => !entry.relativePath.startsWith(".."));
158
+ for (const file of filesByRelativePath) {
159
+ for (const candidate of patterns) {
160
+ if (candidate.scope !== "project") {
161
+ continue;
162
+ }
163
+ if (!wildcardToRegex(candidate.pattern).test(file.relativePath)) {
164
+ continue;
165
+ }
166
+ if (!selected.has(file.relativePath)) {
167
+ selected.set(file.relativePath, {
168
+ reportPath: file.relativePath,
169
+ absolutePath: file.absolutePath,
170
+ format: candidate.format,
171
+ tool: candidate.tool,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ if (!options.includeUserScope) {
177
+ for (const file of filesByRelativePath) {
178
+ if (selected.has(file.relativePath)) {
179
+ continue;
180
+ }
181
+ const inferred = inferArtifactCandidate(file.relativePath, file.absolutePath);
182
+ if (!inferred) {
183
+ continue;
184
+ }
185
+ selected.set(file.relativePath, inferred);
186
+ }
187
+ return Array.from(selected.values());
188
+ }
189
+ for (const candidate of patterns) {
190
+ if (candidate.scope !== "user") {
191
+ continue;
192
+ }
193
+ const userPattern = normalizeUserScopePattern(candidate.pattern);
194
+ if (userPattern.includes("*")) {
195
+ for (const match of collectUserScopeWildcardMatches(options.homeDir, userPattern)) {
196
+ const reportPath = toUserReportPath(match.relativePath);
197
+ if (!selected.has(reportPath)) {
198
+ selected.set(reportPath, {
199
+ reportPath,
200
+ absolutePath: match.absolutePath,
201
+ format: candidate.format,
202
+ tool: candidate.tool,
203
+ });
204
+ }
205
+ }
206
+ continue;
207
+ }
208
+ const absolutePath = resolve(options.homeDir, userPattern);
209
+ if (!existsSync(absolutePath) || !isRegularFile(absolutePath)) {
210
+ continue;
211
+ }
212
+ const reportPath = toUserReportPath(userPattern);
213
+ if (!selected.has(reportPath)) {
214
+ selected.set(reportPath, {
215
+ reportPath,
216
+ absolutePath,
217
+ format: candidate.format,
218
+ tool: candidate.tool,
219
+ });
220
+ }
221
+ }
222
+ for (const file of filesByRelativePath) {
223
+ if (selected.has(file.relativePath)) {
224
+ continue;
225
+ }
226
+ const inferred = inferArtifactCandidate(file.relativePath, file.absolutePath);
227
+ if (!inferred) {
228
+ continue;
229
+ }
230
+ selected.set(file.relativePath, inferred);
231
+ }
232
+ return Array.from(selected.values());
233
+ }
234
+ function mergeExplicitCandidates(selected, explicitCandidates) {
235
+ if (!explicitCandidates || explicitCandidates.length === 0) {
236
+ return selected;
237
+ }
238
+ const merged = new Map();
239
+ for (const candidate of selected) {
240
+ merged.set(candidate.reportPath, candidate);
241
+ }
242
+ for (const candidate of explicitCandidates) {
243
+ merged.set(candidate.reportPath, candidate);
244
+ }
245
+ return Array.from(merged.values());
246
+ }
247
+ function inferArtifactCandidate(relativePath, absolutePath) {
248
+ for (const rule of INFERRED_ARTIFACT_RULES) {
249
+ if (!rule.pattern.test(relativePath)) {
250
+ continue;
251
+ }
252
+ return {
253
+ reportPath: relativePath,
254
+ absolutePath,
255
+ format: rule.format,
256
+ tool: rule.tool,
257
+ };
258
+ }
259
+ return null;
260
+ }
261
+ function parseSelectedCandidates(selected) {
262
+ return selected.map((candidate) => ({
263
+ ...candidate,
264
+ parsed: candidate.textContent !== undefined
265
+ ? parseConfigContent(candidate.textContent, candidate.format)
266
+ : parseConfigFile(candidate.absolutePath, candidate.format),
267
+ }));
268
+ }
269
+ function ensureParsedCandidates(context) {
270
+ if (!context.parsedCandidates) {
271
+ context.parsedCandidates = parseSelectedCandidates(context.selected);
272
+ }
273
+ return context.parsedCandidates;
274
+ }
275
+ function makeParseErrorFinding(filePath, tool, message) {
276
+ return {
277
+ rule_id: "parse-error",
278
+ finding_id: `PARSE_ERROR-${filePath}`,
279
+ severity: "LOW",
280
+ category: "PARSE_ERROR",
281
+ layer: "L1",
282
+ file_path: filePath,
283
+ location: { field: "parse" },
284
+ description: message,
285
+ affected_tools: [tool],
286
+ cve: null,
287
+ owasp: ["ASI06"],
288
+ cwe: "CWE-20",
289
+ confidence: "HIGH",
290
+ fixable: false,
291
+ remediation_actions: [],
292
+ suppressed: false,
293
+ };
294
+ }
295
+ function readTextFileUtf8(path) {
296
+ try {
297
+ return readFileSync(path, "utf8");
298
+ }
299
+ catch {
300
+ return "";
301
+ }
302
+ }
303
+ function readCandidateText(candidate) {
304
+ return candidate.textContent ?? readTextFileUtf8(candidate.absolutePath);
305
+ }
306
+ function isRecord(value) {
307
+ return typeof value === "object" && value !== null && !Array.isArray(value);
308
+ }
309
+ function isHttpLikeUrl(value) {
310
+ return /^https?:\/\//iu.test(value);
311
+ }
312
+ function firstNonFlag(tokens, startIndex = 0) {
313
+ for (let index = startIndex; index < tokens.length; index += 1) {
314
+ const token = tokens[index];
315
+ if (typeof token !== "string" || token.length === 0 || token.startsWith("-")) {
316
+ continue;
317
+ }
318
+ return token;
319
+ }
320
+ return null;
321
+ }
322
+ function commandResourceFromTokens(command) {
323
+ if (command.length === 0) {
324
+ return null;
325
+ }
326
+ const launcher = command[0]?.toLowerCase();
327
+ if (launcher === "npx") {
328
+ const locator = firstNonFlag(command, 1);
329
+ if (!locator) {
330
+ return null;
331
+ }
332
+ return {
333
+ id: `npm:${locator}`,
334
+ kind: "npm",
335
+ locator,
336
+ preview: `npm view ${locator} --json`,
337
+ };
338
+ }
339
+ if (launcher === "uvx" || launcher === "pipx") {
340
+ const locator = firstNonFlag(command, 1);
341
+ if (!locator) {
342
+ return null;
343
+ }
344
+ return {
345
+ id: `pypi:${locator}`,
346
+ kind: "pypi",
347
+ locator,
348
+ preview: `https://pypi.org/pypi/${locator}/json`,
349
+ };
350
+ }
351
+ return null;
352
+ }
353
+ function inferHttpKind(url) {
354
+ const lower = url.toLowerCase();
355
+ if (lower.includes("sse") || lower.includes("eventstream") || lower.includes("event-stream")) {
356
+ return "sse";
357
+ }
358
+ return "http";
359
+ }
360
+ function collectMcpServerContainers(value) {
361
+ const containers = [];
362
+ for (const key of MCP_SERVER_CONTAINER_KEYS) {
363
+ const candidate = value[key];
364
+ if (!isRecord(candidate)) {
365
+ continue;
366
+ }
367
+ containers.push({ key, servers: candidate });
368
+ }
369
+ return containers;
370
+ }
371
+ function collectRemoteMcpServerArrays(value) {
372
+ const arrays = [];
373
+ for (const key of REMOTE_MCP_SERVER_ARRAY_KEYS) {
374
+ const candidate = value[key];
375
+ if (!Array.isArray(candidate)) {
376
+ continue;
377
+ }
378
+ const servers = candidate.filter((entry) => isRecord(entry));
379
+ if (servers.length === 0) {
380
+ continue;
381
+ }
382
+ arrays.push({ key, servers });
383
+ }
384
+ return arrays;
385
+ }
386
+ function collectDeepScanResourcesFromParsed(value, filePath, resources) {
387
+ if (!isRecord(value)) {
388
+ return;
389
+ }
390
+ for (const container of collectMcpServerContainers(value)) {
391
+ for (const [serverName, config] of Object.entries(container.servers)) {
392
+ if (!isRecord(config)) {
393
+ continue;
394
+ }
395
+ if (typeof config.url === "string" && isHttpLikeUrl(config.url)) {
396
+ const kind = inferHttpKind(config.url);
397
+ const id = `${kind}:${config.url}`;
398
+ if (!resources.has(id)) {
399
+ resources.set(id, {
400
+ id,
401
+ request: {
402
+ id,
403
+ kind,
404
+ locator: config.url,
405
+ },
406
+ commandPreview: `GET ${config.url} (from ${filePath} -> ${container.key}.${serverName}.url)`,
407
+ });
408
+ }
409
+ }
410
+ if (Array.isArray(config.command) &&
411
+ config.command.length > 0 &&
412
+ config.command.every((entry) => typeof entry === "string")) {
413
+ const commandResource = commandResourceFromTokens(config.command);
414
+ if (!commandResource || resources.has(commandResource.id)) {
415
+ continue;
416
+ }
417
+ resources.set(commandResource.id, {
418
+ id: commandResource.id,
419
+ request: {
420
+ id: commandResource.id,
421
+ kind: commandResource.kind,
422
+ locator: commandResource.locator,
423
+ },
424
+ commandPreview: `${commandResource.preview} (from ${filePath} -> ${container.key}.${serverName}.command)`,
425
+ });
426
+ }
427
+ }
428
+ }
429
+ for (const remoteArray of collectRemoteMcpServerArrays(value)) {
430
+ remoteArray.servers.forEach((config, index) => {
431
+ if (typeof config.url !== "string" || !isHttpLikeUrl(config.url)) {
432
+ return;
433
+ }
434
+ const kind = inferHttpKind(config.url);
435
+ const id = `${kind}:${config.url}`;
436
+ if (resources.has(id)) {
437
+ return;
438
+ }
439
+ resources.set(id, {
440
+ id,
441
+ request: {
442
+ id,
443
+ kind,
444
+ locator: config.url,
445
+ },
446
+ commandPreview: `GET ${config.url} (from ${filePath} -> ${remoteArray.key}.${index}.url)`,
447
+ });
448
+ });
449
+ }
450
+ for (const nested of Object.values(value)) {
451
+ collectDeepScanResourcesFromParsed(nested, filePath, resources);
452
+ }
453
+ }
454
+ export function discoverDeepScanResources(scanTarget, kbInput, options = {}) {
455
+ const context = createScanDiscoveryContext(scanTarget, kbInput, {
456
+ includeUserScope: options.includeUserScope,
457
+ homeDir: options.homeDir,
458
+ parseSelected: true,
459
+ });
460
+ return discoverDeepScanResourcesFromContext(context);
461
+ }
462
+ export function createScanDiscoveryContext(scanTarget, kbInput, options = {}) {
463
+ const absoluteTarget = resolve(scanTarget);
464
+ const targetStat = statSync(absoluteTarget);
465
+ if (!targetStat.isDirectory()) {
466
+ throw new Error(`Scan target is not a directory: ${scanTarget}`);
467
+ }
468
+ const kb = kbInput ?? loadKnowledgeBase();
469
+ const patterns = gatherCandidatePatterns(kb);
470
+ const walked = walkProjectTree(absoluteTarget);
471
+ const selected = mergeExplicitCandidates(collectSelectedCandidates(absoluteTarget, walked.files, patterns, {
472
+ includeUserScope: options.includeUserScope === true,
473
+ homeDir: resolve(options.homeDir ?? homedir()),
474
+ }), options.explicitCandidates);
475
+ return {
476
+ absoluteTarget,
477
+ kb,
478
+ walked,
479
+ selected,
480
+ parsedCandidates: options.parseSelected ? parseSelectedCandidates(selected) : undefined,
481
+ };
482
+ }
483
+ export function discoverDeepScanResourcesFromContext(context) {
484
+ const discovered = new Map();
485
+ for (const item of ensureParsedCandidates(context)) {
486
+ if (!item.parsed.ok) {
487
+ continue;
488
+ }
489
+ collectDeepScanResourcesFromParsed(item.parsed.data, item.reportPath, discovered);
490
+ }
491
+ return Array.from(discovered.values()).sort((a, b) => a.id.localeCompare(b.id));
492
+ }
493
+ export function collectScanSurface(scanTarget, kbInput, options = {}) {
494
+ const context = createScanDiscoveryContext(scanTarget, kbInput, {
495
+ includeUserScope: options.includeUserScope === true,
496
+ homeDir: options.homeDir,
497
+ });
498
+ const surface = new Set(context.walked.files);
499
+ for (const item of context.selected) {
500
+ surface.add(item.absolutePath);
501
+ }
502
+ return Array.from(surface).sort((left, right) => left.localeCompare(right));
503
+ }
504
+ export function discoverLocalTextAnalysisTargetsFromContext(context) {
505
+ return collectLocalTextAnalysisTargets(context.selected.map((item) => ({
506
+ reportPath: item.reportPath,
507
+ absolutePath: item.absolutePath,
508
+ format: item.format,
509
+ textContent: readCandidateText(item),
510
+ })));
511
+ }
512
+ export async function runScanEngine(input) {
513
+ const context = input.discoveryContext ??
514
+ createScanDiscoveryContext(input.scanTarget, input.kb, {
515
+ includeUserScope: input.config.scan_user_scope === true,
516
+ homeDir: input.homeDir,
517
+ parseSelected: true,
518
+ });
519
+ const absoluteTarget = context.absoluteTarget;
520
+ const kb = context.kb;
521
+ const parseErrors = [];
522
+ const staticFiles = [];
523
+ for (const item of ensureParsedCandidates(context)) {
524
+ if (!item.parsed.ok) {
525
+ parseErrors.push(makeParseErrorFinding(item.reportPath, item.tool, item.parsed.error));
526
+ continue;
527
+ }
528
+ staticFiles.push({
529
+ filePath: item.reportPath,
530
+ format: item.format,
531
+ parsed: item.parsed.data,
532
+ textContent: readCandidateText(item),
533
+ });
534
+ }
535
+ const filesByRelativePath = context.walked.files
536
+ .map((filePath) => ({
537
+ absolutePath: filePath,
538
+ relativePath: normalizePathForMatch(relative(absoluteTarget, filePath)),
539
+ }))
540
+ .filter((entry) => !entry.relativePath.startsWith(".."));
541
+ const hooks = filesByRelativePath
542
+ .filter((entry) => entry.relativePath.startsWith(".git/hooks/"))
543
+ .map((entry) => {
544
+ const mode = statSync(entry.absolutePath).mode;
545
+ return {
546
+ path: entry.relativePath,
547
+ content: readTextFileUtf8(entry.absolutePath),
548
+ executable: (mode & 0o111) !== 0,
549
+ };
550
+ });
551
+ const report = runStaticPipeline({
552
+ version: input.version,
553
+ kbVersion: kb.schemaVersion,
554
+ scanTarget: input.scanTarget,
555
+ toolsDetected: detectTools(undefined, { includeVersions: false })
556
+ .filter((tool) => tool.installed)
557
+ .map((tool) => tool.tool),
558
+ projectRoot: absoluteTarget,
559
+ files: staticFiles,
560
+ symlinkEscapes: context.walked.symlinkEscapes.map((entry) => ({
561
+ path: normalizePathForMatch(relative(absoluteTarget, entry.path)),
562
+ target: entry.target,
563
+ })),
564
+ hooks,
565
+ config: {
566
+ knownSafeMcpServers: input.config.known_safe_mcp_servers,
567
+ knownSafeFormatters: input.config.known_safe_formatters,
568
+ knownSafeLspServers: input.config.known_safe_lsp_servers,
569
+ knownSafeHooks: input.config.known_safe_hooks,
570
+ blockedCommands: input.config.blocked_commands,
571
+ trustedApiDomains: input.config.trusted_api_domains,
572
+ unicodeAnalysis: input.config.unicode_analysis,
573
+ checkIdeSettings: input.config.check_ide_settings,
574
+ },
575
+ });
576
+ const snapshots = new Map();
577
+ for (const file of staticFiles) {
578
+ for (const snapshot of extractMcpServerSnapshots(file.filePath, file.parsed)) {
579
+ snapshots.set(snapshot.serverId, snapshot);
580
+ }
581
+ }
582
+ const previousState = loadScanState(input.scanStatePath);
583
+ const stateResult = evaluateScanStateSnapshots({
584
+ snapshots: Array.from(snapshots.values()),
585
+ previousState,
586
+ });
587
+ saveScanState(stateResult.nextState, input.scanStatePath);
588
+ const findings = [...report.findings, ...parseErrors, ...stateResult.findings];
589
+ return applyReportSummary({
590
+ ...report,
591
+ findings,
592
+ });
593
+ }
@@ -0,0 +1,10 @@
1
+ import type { CodeGateReport } from "../types/report.js";
2
+ export type TuiView = "dashboard" | "progress" | "summary";
3
+ export interface CodeGateTuiAppProps {
4
+ view: TuiView;
5
+ report?: CodeGateReport;
6
+ progressMessage?: string;
7
+ notices?: string[];
8
+ }
9
+ export declare function CodeGateTuiApp(props: CodeGateTuiAppProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function renderTuiApp(props: CodeGateTuiAppProps): void;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "ink";
3
+ import { DashboardView } from "./views/dashboard.js";
4
+ import { ProgressView } from "./views/progress.js";
5
+ import { SummaryView } from "./views/summary.js";
6
+ export function CodeGateTuiApp(props) {
7
+ if (props.view === "progress") {
8
+ return _jsx(ProgressView, { progressMessage: props.progressMessage });
9
+ }
10
+ if (!props.report) {
11
+ return _jsx(ProgressView, { progressMessage: "Preparing report..." });
12
+ }
13
+ if (props.view === "summary") {
14
+ return _jsx(SummaryView, { report: props.report });
15
+ }
16
+ return _jsx(DashboardView, { report: props.report, notices: props.notices });
17
+ }
18
+ export function renderTuiApp(props) {
19
+ const app = render(_jsx(CodeGateTuiApp, { ...props }));
20
+ app.unmount();
21
+ }
@@ -0,0 +1,8 @@
1
+ export interface TuiTheme {
2
+ title: string;
3
+ ok: string;
4
+ warning: string;
5
+ danger: string;
6
+ muted: string;
7
+ }
8
+ export declare const defaultTheme: TuiTheme;
@@ -0,0 +1,7 @@
1
+ export const defaultTheme = {
2
+ title: "cyan",
3
+ ok: "green",
4
+ warning: "yellow",
5
+ danger: "red",
6
+ muted: "gray",
7
+ };
@@ -0,0 +1,6 @@
1
+ import type { CodeGateReport } from "../../types/report.js";
2
+ export interface DashboardViewProps {
3
+ report: CodeGateReport;
4
+ notices?: string[];
5
+ }
6
+ export declare function DashboardView(props: DashboardViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { toAbsoluteDisplayPath } from "../../path-display.js";
4
+ import { defaultTheme } from "../theme.js";
5
+ export function DashboardView(props) {
6
+ const visibleFindings = props.report.findings.slice(0, 5);
7
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Text, { color: defaultTheme.title, children: ["CodeGate v", props.report.version] }), _jsxs(Text, { color: defaultTheme.muted, children: ["Target: ", props.report.scan_target] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["Installed tools: ", props.report.tools_detected.join(", ") || "none"] }), _jsxs(Text, { children: ["Findings: ", props.report.summary.total, " (CRITICAL", " ", props.report.summary.by_severity.CRITICAL ?? 0, ", HIGH", " ", props.report.summary.by_severity.HIGH ?? 0, ", MEDIUM", " ", props.report.summary.by_severity.MEDIUM ?? 0, ", LOW", " ", props.report.summary.by_severity.LOW ?? 0, ", INFO", " ", props.report.summary.by_severity.INFO ?? 0, ")"] }), props.notices && props.notices.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Deep scan:" }), props.notices.map((notice, index) => (_jsx(Text, { color: defaultTheme.muted, children: notice }, `notice-${index}`)))] })) : null] }), visibleFindings.length > 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: defaultTheme.title, children: "Findings detail:" }), visibleFindings.map((finding) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: ["[", finding.severity, "]", " ", toAbsoluteDisplayPath(props.report.scan_target, finding.file_path)] }), _jsx(Text, { children: finding.description }), finding.evidence ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Evidence:" }), finding.evidence.split("\n").map((line, index) => (_jsx(Text, { children: line }, `${finding.finding_id}-evidence-${index}`)))] })) : null] }, finding.finding_id))), props.report.findings.length > visibleFindings.length ? (_jsxs(Text, { color: defaultTheme.muted, children: ["...and ", props.report.findings.length - visibleFindings.length, " more findings"] })) : null] })) : null] }));
8
+ }
@@ -0,0 +1,5 @@
1
+ export interface DeepScanConsentViewProps {
2
+ resourceId: string;
3
+ commandPreview: string;
4
+ }
5
+ export declare function DeepScanConsentView(props: DeepScanConsentViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { defaultTheme } from "../theme.js";
4
+ export function DeepScanConsentView(props) {
5
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsx(Text, { color: defaultTheme.title, children: "Deep Scan Consent" }), _jsxs(Text, { children: ["Resource: ", props.resourceId] }), _jsx(Text, { color: defaultTheme.muted, children: "Command preview:" }), _jsx(Text, { children: props.commandPreview }), _jsx(Text, { children: "Approve this action before execution. No network activity occurs unless consent is granted." })] }));
6
+ }
@@ -0,0 +1,4 @@
1
+ export interface ProgressViewProps {
2
+ progressMessage?: string;
3
+ }
4
+ export declare function ProgressView(props: ProgressViewProps): import("react/jsx-runtime").JSX.Element;