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 { readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, extname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const rulesDir = resolve(dirname(fileURLToPath(import.meta.url)), "rules");
5
+ function escapeRegex(value) {
6
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
7
+ }
8
+ function wildcardToRegex(pattern) {
9
+ const escaped = escapeRegex(pattern).replace(/\*/g, "[^/]*");
10
+ return new RegExp(`^${escaped}$`);
11
+ }
12
+ function matchesFilePattern(pattern, filePath) {
13
+ return pattern
14
+ .split("|")
15
+ .map((part) => part.trim())
16
+ .filter((part) => part.length > 0)
17
+ .some((part) => wildcardToRegex(part).test(filePath));
18
+ }
19
+ function getValuesByPath(root, segments) {
20
+ if (segments.length === 0) {
21
+ return [root];
22
+ }
23
+ const [head, ...tail] = segments;
24
+ if (head === "*") {
25
+ if (root === null || typeof root !== "object") {
26
+ return [];
27
+ }
28
+ const values = Array.isArray(root) ? root : Object.values(root);
29
+ return values.flatMap((value) => getValuesByPath(value, tail));
30
+ }
31
+ if (root === null || typeof root !== "object") {
32
+ return [];
33
+ }
34
+ const record = root;
35
+ if (!(head in record)) {
36
+ return [];
37
+ }
38
+ return getValuesByPath(record[head], tail);
39
+ }
40
+ function resolveJsonPath(parsed, query) {
41
+ const normalized = query.startsWith("$.") ? query.slice(2) : query.replace(/^\$/, "");
42
+ if (!normalized) {
43
+ return [parsed];
44
+ }
45
+ return getValuesByPath(parsed, normalized.split("."));
46
+ }
47
+ function resolveTomlPath(parsed, query) {
48
+ return getValuesByPath(parsed, query.split("."));
49
+ }
50
+ function resolveEnvKeys(parsed, query) {
51
+ if (!parsed || typeof parsed !== "object") {
52
+ return [];
53
+ }
54
+ const record = parsed;
55
+ const keys = query.split("|").map((token) => token.trim());
56
+ return keys.filter((key) => key in record).map((key) => record[key]);
57
+ }
58
+ function evaluateCondition(values, condition, query) {
59
+ const first = values[0];
60
+ switch (condition) {
61
+ case "equals_true":
62
+ return first === true;
63
+ case "equals_false":
64
+ return first === false;
65
+ case "exists":
66
+ return values.length > 0;
67
+ case "not_empty":
68
+ if (values.length === 0) {
69
+ return false;
70
+ }
71
+ if (typeof first === "string") {
72
+ return first.trim().length > 0;
73
+ }
74
+ if (Array.isArray(first)) {
75
+ return first.length > 0;
76
+ }
77
+ if (first && typeof first === "object") {
78
+ return Object.keys(first).length > 0;
79
+ }
80
+ return first !== null && first !== undefined;
81
+ case "matches_regex": {
82
+ const regex = new RegExp(query);
83
+ return values.some((value) => regex.test(String(value)));
84
+ }
85
+ case "not_in_allowlist": {
86
+ const allowlist = query.split("|").map((token) => token.trim());
87
+ return values.some((value) => !allowlist.includes(String(value)));
88
+ }
89
+ default:
90
+ return false;
91
+ }
92
+ }
93
+ function evaluateTextCondition(content, condition, query) {
94
+ switch (condition) {
95
+ case "regex_match": {
96
+ const regex = new RegExp(query, "u");
97
+ return regex.test(content);
98
+ }
99
+ case "contains":
100
+ return content.includes(query);
101
+ case "line_length_exceeds": {
102
+ const threshold = Number.parseInt(query, 10);
103
+ if (Number.isNaN(threshold)) {
104
+ return false;
105
+ }
106
+ return content.split(/\r?\n/u).some((line) => line.length > threshold);
107
+ }
108
+ default:
109
+ return false;
110
+ }
111
+ }
112
+ export function evaluateRule(rule, input) {
113
+ if (!matchesFilePattern(rule.file_pattern, input.filePath)) {
114
+ return false;
115
+ }
116
+ if (rule.query_type === "text_pattern") {
117
+ return evaluateTextCondition(input.textContent, rule.condition, rule.query);
118
+ }
119
+ if (rule.query_type === "json_path") {
120
+ return evaluateCondition(resolveJsonPath(input.parsed, rule.query), rule.condition, rule.query);
121
+ }
122
+ if (rule.query_type === "toml_path") {
123
+ return evaluateCondition(resolveTomlPath(input.parsed, rule.query), rule.condition, rule.query);
124
+ }
125
+ if (rule.query_type === "env_key") {
126
+ return evaluateCondition(resolveEnvKeys(input.parsed, rule.query), rule.condition, rule.query);
127
+ }
128
+ return false;
129
+ }
130
+ export function loadRulePacks(baseDir = rulesDir) {
131
+ const files = readdirSync(baseDir)
132
+ .filter((file) => extname(file) === ".json")
133
+ .sort();
134
+ return files.flatMap((file) => {
135
+ const raw = readFileSync(join(baseDir, file), "utf8");
136
+ return JSON.parse(raw);
137
+ });
138
+ }
@@ -0,0 +1,32 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface ScanStateServerEntry {
3
+ config_hash: string;
4
+ config_path: string;
5
+ first_seen: string;
6
+ last_seen: string;
7
+ }
8
+ export interface ScanState {
9
+ servers: Record<string, ScanStateServerEntry>;
10
+ }
11
+ export interface McpServerSnapshot {
12
+ serverId: string;
13
+ serverName: string;
14
+ configHash: string;
15
+ configPath: string;
16
+ serverPath?: string;
17
+ }
18
+ export interface EvaluateScanStateSnapshotsInput {
19
+ snapshots: McpServerSnapshot[];
20
+ previousState: ScanState;
21
+ nowIso?: string;
22
+ }
23
+ export interface EvaluateScanStateSnapshotsResult {
24
+ findings: Finding[];
25
+ nextState: ScanState;
26
+ }
27
+ export declare function getScanStatePath(customPath?: string): string;
28
+ export declare function loadScanState(customPath?: string): ScanState;
29
+ export declare function saveScanState(state: ScanState, customPath?: string): void;
30
+ export declare function resetScanState(customPath?: string): void;
31
+ export declare function evaluateScanStateSnapshots(input: EvaluateScanStateSnapshotsInput): EvaluateScanStateSnapshotsResult;
32
+ export declare function extractMcpServerSnapshots(filePath: string, parsed: unknown): McpServerSnapshot[];
@@ -0,0 +1,296 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, resolve } from "node:path";
5
+ const LAUNCHERS = new Set(["npx", "uvx", "node", "python", "python3", "deno", "bun"]);
6
+ const MCP_SERVER_CONTAINER_KEYS = ["mcpServers", "mcp_servers", "context_servers"];
7
+ const REMOTE_MCP_SERVER_ARRAY_KEYS = ["remoteMCPServers", "remote_mcp_servers"];
8
+ function defaultPath() {
9
+ return resolve(homedir(), ".codegate", "scan-state.json");
10
+ }
11
+ function expandHomePath(path) {
12
+ if (path === "~") {
13
+ return homedir();
14
+ }
15
+ if (path.startsWith("~/")) {
16
+ return resolve(homedir(), path.slice(2));
17
+ }
18
+ return path;
19
+ }
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ function stableStringify(value) {
24
+ if (value === null || typeof value !== "object") {
25
+ return JSON.stringify(value);
26
+ }
27
+ if (Array.isArray(value)) {
28
+ return `[${value.map(stableStringify).join(",")}]`;
29
+ }
30
+ const record = value;
31
+ const keys = Object.keys(record).sort();
32
+ const entries = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
33
+ return `{${entries.join(",")}}`;
34
+ }
35
+ function sha256(input) {
36
+ return `sha256:${createHash("sha256").update(input).digest("hex")}`;
37
+ }
38
+ function extractPackageFromPath(token) {
39
+ const normalized = token.replaceAll("\\", "/");
40
+ const scopedMatch = normalized.match(/node_modules\/(@[^/]+\/[^/]+)/u);
41
+ if (scopedMatch?.[1]) {
42
+ return scopedMatch[1];
43
+ }
44
+ const plainMatch = normalized.match(/node_modules\/([^/]+)/u);
45
+ return plainMatch?.[1] ?? null;
46
+ }
47
+ function extractIdentifierFromCommand(command) {
48
+ for (const token of command) {
49
+ if (token.startsWith("-")) {
50
+ continue;
51
+ }
52
+ if (LAUNCHERS.has(token)) {
53
+ continue;
54
+ }
55
+ const fromPath = extractPackageFromPath(token);
56
+ if (fromPath) {
57
+ return fromPath;
58
+ }
59
+ return token;
60
+ }
61
+ return null;
62
+ }
63
+ function normalizeIdentifier(value) {
64
+ const trimmed = value.trim();
65
+ if (trimmed.length === 0) {
66
+ return "";
67
+ }
68
+ const fromPath = extractPackageFromPath(trimmed);
69
+ const candidate = (fromPath ?? trimmed)
70
+ .replaceAll("\\", "/")
71
+ .replace(/[#?].*$/u, "")
72
+ .replace(/\/+$/u, "");
73
+ const looksLikePackageOrTool = /^@?[a-z0-9._-]+(?:\/[a-z0-9._-]+)?$/iu.test(candidate);
74
+ return looksLikePackageOrTool ? candidate.toLowerCase() : candidate;
75
+ }
76
+ function normalizedUrlServerId(rawUrl) {
77
+ const trimmed = rawUrl.trim();
78
+ if (trimmed.length === 0) {
79
+ return "url:";
80
+ }
81
+ try {
82
+ const parsed = new URL(trimmed);
83
+ parsed.protocol = parsed.protocol.toLowerCase();
84
+ parsed.hostname = parsed.hostname.toLowerCase();
85
+ if ((parsed.protocol === "https:" && parsed.port === "443") ||
86
+ (parsed.protocol === "http:" && parsed.port === "80")) {
87
+ parsed.port = "";
88
+ }
89
+ const normalizedPath = parsed.pathname.replace(/\/{2,}/gu, "/");
90
+ parsed.pathname =
91
+ normalizedPath.length > 1 ? normalizedPath.replace(/\/+$/u, "") : normalizedPath;
92
+ const normalizedParams = Array.from(parsed.searchParams.entries()).sort(([leftKey, leftValue], [rightKey, rightValue]) => {
93
+ if (leftKey === rightKey) {
94
+ return leftValue.localeCompare(rightValue);
95
+ }
96
+ return leftKey.localeCompare(rightKey);
97
+ });
98
+ parsed.search = "";
99
+ for (const [key, value] of normalizedParams) {
100
+ parsed.searchParams.append(key, value);
101
+ }
102
+ parsed.hash = "";
103
+ return `url:${parsed.toString()}`;
104
+ }
105
+ catch {
106
+ return `url:${trimmed}`;
107
+ }
108
+ }
109
+ function makeStateFinding(kind, snapshot, previousLastSeen) {
110
+ const locationField = snapshot.serverPath ?? `mcpServers.${snapshot.serverName}`;
111
+ if (kind === "NEW_SERVER") {
112
+ return {
113
+ rule_id: "mcp-server-first-seen",
114
+ finding_id: `NEW_SERVER-${snapshot.serverId}`,
115
+ severity: "INFO",
116
+ category: "NEW_SERVER",
117
+ layer: "L2",
118
+ file_path: snapshot.configPath,
119
+ location: { field: locationField },
120
+ description: `MCP server "${snapshot.serverId}" first seen in this project. Not previously scanned.`,
121
+ affected_tools: ["claude-code", "cursor", "windsurf", "codex-cli", "opencode"],
122
+ cve: null,
123
+ owasp: ["ASI08"],
124
+ cwe: "CWE-829",
125
+ confidence: "HIGH",
126
+ fixable: false,
127
+ remediation_actions: [],
128
+ suppressed: false,
129
+ };
130
+ }
131
+ return {
132
+ rule_id: "mcp-server-config-change",
133
+ finding_id: `CONFIG_CHANGE-${snapshot.serverId}`,
134
+ severity: "HIGH",
135
+ category: "CONFIG_CHANGE",
136
+ layer: "L2",
137
+ file_path: snapshot.configPath,
138
+ location: { field: locationField },
139
+ description: `MCP server "${snapshot.serverId}" configuration has changed since last scan (${previousLastSeen ?? "unknown date"}). Review the changes before proceeding.`,
140
+ affected_tools: ["claude-code", "cursor", "windsurf", "codex-cli", "opencode"],
141
+ cve: null,
142
+ owasp: ["ASI08"],
143
+ cwe: "CWE-829",
144
+ confidence: "HIGH",
145
+ fixable: false,
146
+ remediation_actions: [],
147
+ suppressed: false,
148
+ };
149
+ }
150
+ export function getScanStatePath(customPath) {
151
+ return resolve(expandHomePath(customPath ?? defaultPath()));
152
+ }
153
+ export function loadScanState(customPath) {
154
+ const path = getScanStatePath(customPath);
155
+ if (!existsSync(path)) {
156
+ return { servers: {} };
157
+ }
158
+ let parsed;
159
+ try {
160
+ const raw = readFileSync(path, "utf8");
161
+ parsed = JSON.parse(raw);
162
+ }
163
+ catch {
164
+ return { servers: {} };
165
+ }
166
+ if (!isRecord(parsed) || !isRecord(parsed.servers)) {
167
+ return { servers: {} };
168
+ }
169
+ const entries = {};
170
+ for (const [key, value] of Object.entries(parsed.servers)) {
171
+ if (!isRecord(value)) {
172
+ continue;
173
+ }
174
+ const config_hash = typeof value.config_hash === "string" ? value.config_hash : "";
175
+ const config_path = typeof value.config_path === "string" ? value.config_path : "";
176
+ const first_seen = typeof value.first_seen === "string" ? value.first_seen : "";
177
+ const last_seen = typeof value.last_seen === "string" ? value.last_seen : "";
178
+ if (!config_hash || !config_path || !first_seen || !last_seen) {
179
+ continue;
180
+ }
181
+ entries[key] = {
182
+ config_hash,
183
+ config_path,
184
+ first_seen,
185
+ last_seen,
186
+ };
187
+ }
188
+ return { servers: entries };
189
+ }
190
+ export function saveScanState(state, customPath) {
191
+ const path = getScanStatePath(customPath);
192
+ mkdirSync(dirname(path), { recursive: true });
193
+ writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`, "utf8");
194
+ }
195
+ export function resetScanState(customPath) {
196
+ const path = getScanStatePath(customPath);
197
+ rmSync(path, { force: true });
198
+ }
199
+ export function evaluateScanStateSnapshots(input) {
200
+ const nowIso = input.nowIso ?? new Date().toISOString();
201
+ const nextState = {
202
+ servers: { ...input.previousState.servers },
203
+ };
204
+ const findings = [];
205
+ for (const snapshot of input.snapshots) {
206
+ const previous = nextState.servers[snapshot.serverId];
207
+ if (!previous) {
208
+ findings.push(makeStateFinding("NEW_SERVER", snapshot, null));
209
+ nextState.servers[snapshot.serverId] = {
210
+ config_hash: snapshot.configHash,
211
+ config_path: snapshot.configPath,
212
+ first_seen: nowIso,
213
+ last_seen: nowIso,
214
+ };
215
+ continue;
216
+ }
217
+ if (previous.config_hash !== snapshot.configHash) {
218
+ findings.push(makeStateFinding("CONFIG_CHANGE", snapshot, previous.last_seen));
219
+ nextState.servers[snapshot.serverId] = {
220
+ config_hash: snapshot.configHash,
221
+ config_path: snapshot.configPath,
222
+ first_seen: previous.first_seen,
223
+ last_seen: nowIso,
224
+ };
225
+ continue;
226
+ }
227
+ nextState.servers[snapshot.serverId] = {
228
+ ...previous,
229
+ config_path: snapshot.configPath,
230
+ last_seen: nowIso,
231
+ };
232
+ }
233
+ return { findings, nextState };
234
+ }
235
+ function collectMcpServerRecords(value, filePath, snapshots) {
236
+ if (!isRecord(value)) {
237
+ return;
238
+ }
239
+ for (const key of MCP_SERVER_CONTAINER_KEYS) {
240
+ const container = value[key];
241
+ if (!isRecord(container)) {
242
+ continue;
243
+ }
244
+ for (const [serverName, config] of Object.entries(container)) {
245
+ if (!isRecord(config)) {
246
+ continue;
247
+ }
248
+ const commandArray = Array.isArray(config.command) && config.command.every((token) => typeof token === "string")
249
+ ? config.command
250
+ : null;
251
+ const idFromCommandRaw = commandArray ? extractIdentifierFromCommand(commandArray) : null;
252
+ const idFromCommand = idFromCommandRaw ? normalizeIdentifier(idFromCommandRaw) : null;
253
+ const serverId = idFromCommand ??
254
+ (typeof config.url === "string" ? normalizedUrlServerId(config.url) : serverName);
255
+ snapshots.push({
256
+ serverId,
257
+ serverName,
258
+ configHash: sha256(stableStringify(config)),
259
+ configPath: filePath,
260
+ serverPath: `${key}.${serverName}`,
261
+ });
262
+ }
263
+ }
264
+ for (const key of REMOTE_MCP_SERVER_ARRAY_KEYS) {
265
+ const container = value[key];
266
+ if (!Array.isArray(container)) {
267
+ continue;
268
+ }
269
+ container.forEach((config, index) => {
270
+ if (!isRecord(config)) {
271
+ return;
272
+ }
273
+ const serverName = typeof config.name === "string" && config.name.trim().length > 0
274
+ ? config.name.trim()
275
+ : `${key}.${index}`;
276
+ const serverId = typeof config.url === "string" && config.url.trim().length > 0
277
+ ? normalizedUrlServerId(config.url)
278
+ : `${key}:${serverName}`;
279
+ snapshots.push({
280
+ serverId,
281
+ serverName,
282
+ configHash: sha256(stableStringify(config)),
283
+ configPath: filePath,
284
+ serverPath: `${key}.${index}`,
285
+ });
286
+ });
287
+ }
288
+ for (const child of Object.values(value)) {
289
+ collectMcpServerRecords(child, filePath, snapshots);
290
+ }
291
+ }
292
+ export function extractMcpServerSnapshots(filePath, parsed) {
293
+ const snapshots = [];
294
+ collectMcpServerRecords(parsed, filePath, snapshots);
295
+ return snapshots;
296
+ }
@@ -0,0 +1,15 @@
1
+ export type MetaAgentTool = "claude" | "codex" | "generic";
2
+ export interface MetaAgentCommandInput {
3
+ tool: MetaAgentTool;
4
+ prompt: string;
5
+ workingDirectory: string;
6
+ binaryPath?: string;
7
+ }
8
+ export interface MetaAgentCommand {
9
+ command: string;
10
+ args: string[];
11
+ cwd: string;
12
+ preview: string;
13
+ timeoutMs?: number;
14
+ }
15
+ export declare function buildMetaAgentCommand(input: MetaAgentCommandInput): MetaAgentCommand;
@@ -0,0 +1,39 @@
1
+ const INVISIBLE_UNICODE = /[\u200B-\u200D\u2060\uFEFF]/gu;
2
+ function shellEscape(value) {
3
+ return `'${value.replaceAll("'", "'\"'\"'")}'`;
4
+ }
5
+ function normalizePrompt(prompt) {
6
+ return prompt.replace(INVISIBLE_UNICODE, "").replaceAll("\r", "").trim();
7
+ }
8
+ export function buildMetaAgentCommand(input) {
9
+ const prompt = normalizePrompt(input.prompt);
10
+ if (input.tool === "claude") {
11
+ const command = input.binaryPath ?? "claude";
12
+ const args = ["--print", "--max-turns", "1", "--output-format", "json", "--tools=", prompt];
13
+ return {
14
+ command,
15
+ args,
16
+ cwd: input.workingDirectory,
17
+ preview: `${command} ${args.map(shellEscape).join(" ")}`,
18
+ };
19
+ }
20
+ if (input.tool === "codex") {
21
+ const command = input.binaryPath ?? "codex";
22
+ const args = ["--quiet", "--approval-mode", "never", prompt];
23
+ return {
24
+ command,
25
+ args,
26
+ cwd: input.workingDirectory,
27
+ preview: `${command} ${args.map(shellEscape).join(" ")}`,
28
+ };
29
+ }
30
+ const command = "sh";
31
+ const genericToolBinary = input.binaryPath ?? "tool";
32
+ const pipeCommand = `printf %s ${shellEscape(prompt)} | ${shellEscape(genericToolBinary)} --stdin --no-interactive`;
33
+ return {
34
+ command,
35
+ args: ["-lc", pipeCommand],
36
+ cwd: input.workingDirectory,
37
+ preview: `${command} ${shellEscape("-lc")} ${shellEscape(pipeCommand)}`,
38
+ };
39
+ }
@@ -0,0 +1,19 @@
1
+ import type { DiscoveryFormat } from "../types/discovery.js";
2
+ import type { MetaAgentTool } from "./command-builder.js";
3
+ export interface LocalTextAnalysisCandidate {
4
+ reportPath: string;
5
+ absolutePath: string;
6
+ format: DiscoveryFormat;
7
+ textContent: string;
8
+ }
9
+ export interface LocalTextAnalysisTarget {
10
+ id: string;
11
+ reportPath: string;
12
+ absolutePath: string;
13
+ textContent: string;
14
+ referencedUrls: string[];
15
+ }
16
+ export declare function extractReferencedUrls(textContent: string): string[];
17
+ export declare function collectLocalTextAnalysisTargets(candidates: LocalTextAnalysisCandidate[]): LocalTextAnalysisTarget[];
18
+ export declare function supportsToollessLocalTextAnalysis(tool: MetaAgentTool): boolean;
19
+ export declare function buildPromptEvidenceText(textContent: string): string;
@@ -0,0 +1,73 @@
1
+ const LOCAL_TEXT_PATH_PATTERNS = [
2
+ /^agents\.md$/iu,
3
+ /^claude\.md$/iu,
4
+ /^codex\.md$/iu,
5
+ /(?:^|\/)agents\.md$/iu,
6
+ /(?:^|\/)claude\.md$/iu,
7
+ /(?:^|\/)codex\.md$/iu,
8
+ /(?:^|\/)skill\.md$/iu,
9
+ /(?:^|\/)[^/]+\.mdc$/iu,
10
+ /^\.codex\/.*\.(?:md|mdc)$/iu,
11
+ /^\.cursor\/rules\/.*\.mdc$/iu,
12
+ /^\.opencode\/(?:rules|skills|commands)\/.*\.md$/iu,
13
+ /^\.roo\/(?:rules|skills|commands)\/.*\.md$/iu,
14
+ /^\.kiro\/(?:steering|commands)\/.*\.(?:md|txt)$/iu,
15
+ /^\.windsurf.*\.md$/iu,
16
+ /^\.github\/copilot-instructions\.md$/iu,
17
+ ];
18
+ const EXCERPT_SIGNAL_PATTERN = /\b(?:allowed-tools|ignore previous instructions|secret instructions|curl\b|wget\b|bash\b|sh\b|powershell\b|cookies?\s+(?:export|import|get)|session\s+share|profile\s+sync|real chrome|login sessions|session tokens?|tunnel\b|trycloudflare|webhook|upload externally|install\s+-g|@latest|bootstrap\b|restart\b|mcp configuration)\b|\.claude\/(?:hooks|settings\.json|agents\/)|\bclaude\.md\b/iu;
19
+ function normalizeReportPath(reportPath) {
20
+ return reportPath.replaceAll("\\", "/");
21
+ }
22
+ function isLocalTextCandidate(candidate) {
23
+ if (candidate.format !== "markdown" && candidate.format !== "text") {
24
+ return false;
25
+ }
26
+ const normalized = normalizeReportPath(candidate.reportPath);
27
+ return LOCAL_TEXT_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
28
+ }
29
+ export function extractReferencedUrls(textContent) {
30
+ const matches = textContent.match(/https?:\/\/[^\s<>"'`)\]]+/giu) ?? [];
31
+ const unique = new Set();
32
+ for (const match of matches) {
33
+ unique.add(match);
34
+ }
35
+ return Array.from(unique);
36
+ }
37
+ export function collectLocalTextAnalysisTargets(candidates) {
38
+ return candidates.filter(isLocalTextCandidate).map((candidate) => ({
39
+ id: `local:${candidate.reportPath}`,
40
+ reportPath: candidate.reportPath,
41
+ absolutePath: candidate.absolutePath,
42
+ textContent: candidate.textContent,
43
+ referencedUrls: extractReferencedUrls(candidate.textContent),
44
+ }));
45
+ }
46
+ export function supportsToollessLocalTextAnalysis(tool) {
47
+ return tool === "claude";
48
+ }
49
+ export function buildPromptEvidenceText(textContent) {
50
+ const lines = textContent.split(/\r?\n/u);
51
+ const excerptLineNumbers = new Set();
52
+ for (let index = 0; index < Math.min(lines.length, 8); index += 1) {
53
+ excerptLineNumbers.add(index + 1);
54
+ }
55
+ for (let index = 0; index < lines.length; index += 1) {
56
+ const line = lines[index] ?? "";
57
+ if (!EXCERPT_SIGNAL_PATTERN.test(line)) {
58
+ continue;
59
+ }
60
+ excerptLineNumbers.add(index + 1);
61
+ }
62
+ const selected = Array.from(excerptLineNumbers)
63
+ .sort((left, right) => left - right)
64
+ .slice(0, 80);
65
+ const excerptBlocks = selected.map((lineNumber) => `${lineNumber} | ${lines[lineNumber - 1] ?? ""}`);
66
+ return [
67
+ "File stats:",
68
+ `- total lines: ${lines.length}`,
69
+ `- total chars: ${textContent.length}`,
70
+ "Key excerpts:",
71
+ ...excerptBlocks,
72
+ ].join("\n");
73
+ }
@@ -0,0 +1,17 @@
1
+ export interface SecurityAnalysisPromptInput {
2
+ resourceId: string;
3
+ resourceSummary: string;
4
+ }
5
+ export interface LocalTextAnalysisPromptInput {
6
+ filePath: string;
7
+ textContent: string;
8
+ referencedUrls?: string[];
9
+ }
10
+ export interface ToolPoisoningPromptInput {
11
+ resourceId: string;
12
+ toolName: string;
13
+ evidence: string;
14
+ }
15
+ export declare function buildSecurityAnalysisPrompt(input: SecurityAnalysisPromptInput): string;
16
+ export declare function buildLocalTextAnalysisPrompt(input: LocalTextAnalysisPromptInput): string;
17
+ export declare function buildToolPoisoningPrompt(input: ToolPoisoningPromptInput): string;
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const templatesRoot = join(dirname(fileURLToPath(import.meta.url)), "prompt-templates");
5
+ function readTemplate(name) {
6
+ return readFileSync(join(templatesRoot, name), "utf8");
7
+ }
8
+ function normalize(value) {
9
+ return value.replace(/[\u200B-\u200D\u2060\uFEFF]/gu, "").trim();
10
+ }
11
+ export function buildSecurityAnalysisPrompt(input) {
12
+ return readTemplate("security-analysis.md")
13
+ .replaceAll("{{RESOURCE_ID}}", normalize(input.resourceId))
14
+ .replaceAll("{{RESOURCE_SUMMARY}}", normalize(input.resourceSummary));
15
+ }
16
+ export function buildLocalTextAnalysisPrompt(input) {
17
+ const referencedUrls = input.referencedUrls && input.referencedUrls.length > 0
18
+ ? input.referencedUrls.map((url) => `- ${normalize(url)}`).join("\n")
19
+ : "- none";
20
+ const truncatedContent = input.textContent.length > 18_000
21
+ ? `${input.textContent.slice(0, 18_000)}\n...[truncated ${input.textContent.length - 18_000} chars]`
22
+ : input.textContent;
23
+ return readTemplate("local-text-analysis.md")
24
+ .replaceAll("{{FILE_PATH}}", normalize(input.filePath))
25
+ .replaceAll("{{REFERENCED_URLS}}", referencedUrls)
26
+ .replaceAll("{{TEXT_CONTENT}}", normalize(truncatedContent));
27
+ }
28
+ export function buildToolPoisoningPrompt(input) {
29
+ return readTemplate("tool-poisoning.md")
30
+ .replaceAll("{{RESOURCE_ID}}", normalize(input.resourceId))
31
+ .replaceAll("{{TOOL_NAME}}", normalize(input.toolName))
32
+ .replaceAll("{{EVIDENCE}}", normalize(input.evidence));
33
+ }