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.
- package/LICENSE +22 -0
- package/README.md +390 -0
- package/dist/cli-prompts.d.ts +6 -0
- package/dist/cli-prompts.js +94 -0
- package/dist/cli.d.ts +64 -0
- package/dist/cli.js +443 -0
- package/dist/commands/run-policy.d.ts +27 -0
- package/dist/commands/run-policy.js +39 -0
- package/dist/commands/scan-command/helpers.d.ts +28 -0
- package/dist/commands/scan-command/helpers.js +233 -0
- package/dist/commands/scan-command.d.ts +90 -0
- package/dist/commands/scan-command.js +403 -0
- package/dist/commands/undo.d.ts +5 -0
- package/dist/commands/undo.js +14 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +187 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/knowledge-base/claude-code.json +152 -0
- package/dist/knowledge-base/cline.json +224 -0
- package/dist/knowledge-base/codex.json +162 -0
- package/dist/knowledge-base/copilot.json +132 -0
- package/dist/knowledge-base/cursor.json +134 -0
- package/dist/knowledge-base/gemini-cli.json +112 -0
- package/dist/knowledge-base/jetbrains-junie.json +208 -0
- package/dist/knowledge-base/kiro.json +102 -0
- package/dist/knowledge-base/opencode.json +128 -0
- package/dist/knowledge-base/roo-code.json +116 -0
- package/dist/knowledge-base/schema.json +77 -0
- package/dist/knowledge-base/windsurf.json +80 -0
- package/dist/knowledge-base/zed.json +88 -0
- package/dist/layer1-discovery/config-parser.d.ts +12 -0
- package/dist/layer1-discovery/config-parser.js +52 -0
- package/dist/layer1-discovery/file-walker.d.ts +13 -0
- package/dist/layer1-discovery/file-walker.js +77 -0
- package/dist/layer1-discovery/knowledge-base.d.ts +36 -0
- package/dist/layer1-discovery/knowledge-base.js +58 -0
- package/dist/layer1-discovery/tool-detector.d.ts +20 -0
- package/dist/layer1-discovery/tool-detector.js +138 -0
- package/dist/layer2-static/detectors/command-exec.d.ts +11 -0
- package/dist/layer2-static/detectors/command-exec.js +343 -0
- package/dist/layer2-static/detectors/consent-bypass.d.ts +8 -0
- package/dist/layer2-static/detectors/consent-bypass.js +330 -0
- package/dist/layer2-static/detectors/env-override.d.ts +8 -0
- package/dist/layer2-static/detectors/env-override.js +132 -0
- package/dist/layer2-static/detectors/git-hooks.d.ts +11 -0
- package/dist/layer2-static/detectors/git-hooks.js +61 -0
- package/dist/layer2-static/detectors/ide-settings.d.ts +8 -0
- package/dist/layer2-static/detectors/ide-settings.js +66 -0
- package/dist/layer2-static/detectors/plugin-manifest.d.ts +9 -0
- package/dist/layer2-static/detectors/plugin-manifest.js +1943 -0
- package/dist/layer2-static/detectors/rule-file.d.ts +7 -0
- package/dist/layer2-static/detectors/rule-file.js +299 -0
- package/dist/layer2-static/detectors/symlink.d.ts +9 -0
- package/dist/layer2-static/detectors/symlink.js +45 -0
- package/dist/layer2-static/engine.d.ts +28 -0
- package/dist/layer2-static/engine.js +83 -0
- package/dist/layer2-static/evidence.d.ts +12 -0
- package/dist/layer2-static/evidence.js +128 -0
- package/dist/layer2-static/rule-engine.d.ts +24 -0
- package/dist/layer2-static/rule-engine.js +138 -0
- package/dist/layer2-static/state/scan-state.d.ts +32 -0
- package/dist/layer2-static/state/scan-state.js +296 -0
- package/dist/layer3-dynamic/command-builder.d.ts +15 -0
- package/dist/layer3-dynamic/command-builder.js +39 -0
- package/dist/layer3-dynamic/local-text-analysis.d.ts +19 -0
- package/dist/layer3-dynamic/local-text-analysis.js +73 -0
- package/dist/layer3-dynamic/meta-agent.d.ts +17 -0
- package/dist/layer3-dynamic/meta-agent.js +33 -0
- package/dist/layer3-dynamic/prompt-templates/local-text-analysis.md +32 -0
- package/dist/layer3-dynamic/prompt-templates/security-analysis.md +13 -0
- package/dist/layer3-dynamic/prompt-templates/tool-poisoning.md +15 -0
- package/dist/layer3-dynamic/resource-fetcher.d.ts +25 -0
- package/dist/layer3-dynamic/resource-fetcher.js +119 -0
- package/dist/layer3-dynamic/sandbox.d.ts +13 -0
- package/dist/layer3-dynamic/sandbox.js +40 -0
- package/dist/layer3-dynamic/tool-description-acquisition.d.ts +22 -0
- package/dist/layer3-dynamic/tool-description-acquisition.js +76 -0
- package/dist/layer3-dynamic/tool-description-scanner.d.ts +11 -0
- package/dist/layer3-dynamic/tool-description-scanner.js +53 -0
- package/dist/layer3-dynamic/toxic-flow.d.ts +12 -0
- package/dist/layer3-dynamic/toxic-flow.js +57 -0
- package/dist/layer4-remediation/actions/quarantine.d.ts +1 -0
- package/dist/layer4-remediation/actions/quarantine.js +8 -0
- package/dist/layer4-remediation/actions/remove-field.d.ts +5 -0
- package/dist/layer4-remediation/actions/remove-field.js +53 -0
- package/dist/layer4-remediation/actions/replace-value.d.ts +5 -0
- package/dist/layer4-remediation/actions/replace-value.js +26 -0
- package/dist/layer4-remediation/actions/strip-unicode.d.ts +5 -0
- package/dist/layer4-remediation/actions/strip-unicode.js +8 -0
- package/dist/layer4-remediation/backup-manager.d.ts +32 -0
- package/dist/layer4-remediation/backup-manager.js +138 -0
- package/dist/layer4-remediation/diff-generator.d.ts +6 -0
- package/dist/layer4-remediation/diff-generator.js +29 -0
- package/dist/layer4-remediation/remediation-runner.d.ts +36 -0
- package/dist/layer4-remediation/remediation-runner.js +230 -0
- package/dist/layer4-remediation/remediator.d.ts +36 -0
- package/dist/layer4-remediation/remediator.js +117 -0
- package/dist/path-display.d.ts +1 -0
- package/dist/path-display.js +20 -0
- package/dist/pipeline.d.ts +34 -0
- package/dist/pipeline.js +259 -0
- package/dist/report-summary.d.ts +6 -0
- package/dist/report-summary.js +48 -0
- package/dist/reporter/html.d.ts +2 -0
- package/dist/reporter/html.js +103 -0
- package/dist/reporter/json.d.ts +2 -0
- package/dist/reporter/json.js +3 -0
- package/dist/reporter/markdown.d.ts +2 -0
- package/dist/reporter/markdown.js +52 -0
- package/dist/reporter/sarif.d.ts +2 -0
- package/dist/reporter/sarif.js +84 -0
- package/dist/reporter/terminal.d.ts +5 -0
- package/dist/reporter/terminal.js +94 -0
- package/dist/runtime/signal-handlers.d.ts +10 -0
- package/dist/runtime/signal-handlers.js +17 -0
- package/dist/scan-target/helpers.d.ts +20 -0
- package/dist/scan-target/helpers.js +268 -0
- package/dist/scan-target/staging.d.ts +5 -0
- package/dist/scan-target/staging.js +114 -0
- package/dist/scan-target/types.d.ts +18 -0
- package/dist/scan-target/types.js +1 -0
- package/dist/scan-target.d.ts +3 -0
- package/dist/scan-target.js +31 -0
- package/dist/scan.d.ts +54 -0
- package/dist/scan.js +593 -0
- package/dist/tui/app.d.ts +10 -0
- package/dist/tui/app.js +21 -0
- package/dist/tui/theme.d.ts +8 -0
- package/dist/tui/theme.js +7 -0
- package/dist/tui/views/dashboard.d.ts +6 -0
- package/dist/tui/views/dashboard.js +8 -0
- package/dist/tui/views/deep-scan-consent.d.ts +5 -0
- package/dist/tui/views/deep-scan-consent.js +6 -0
- package/dist/tui/views/progress.d.ts +4 -0
- package/dist/tui/views/progress.js +6 -0
- package/dist/tui/views/summary.d.ts +5 -0
- package/dist/tui/views/summary.js +16 -0
- package/dist/types/discovery.d.ts +12 -0
- package/dist/types/discovery.js +1 -0
- package/dist/types/finding.d.ts +46 -0
- package/dist/types/finding.js +15 -0
- package/dist/types/report.d.ts +25 -0
- package/dist/types/report.js +23 -0
- package/dist/wrapper.d.ts +35 -0
- package/dist/wrapper.js +220 -0
- package/package.json +97 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
const AUTO_APPROVAL_KEYS = new Set(["alwaysallow", "always_allow", "autoapprove", "auto_approve"]);
|
|
3
|
+
const REMOTE_MCP_ARRAY_KEYS = ["remoteMCPServers", "remote_mcp_servers"];
|
|
4
|
+
const SENSITIVE_REMOTE_MCP_HEADER_KEYS = new Set([
|
|
5
|
+
"authorization",
|
|
6
|
+
"proxyauthorization",
|
|
7
|
+
"cookie",
|
|
8
|
+
"xapikey",
|
|
9
|
+
"apikey",
|
|
10
|
+
"xauthtoken",
|
|
11
|
+
"xaccesstoken",
|
|
12
|
+
]);
|
|
13
|
+
const ROUTING_REMOTE_MCP_HEADER_KEYS = new Set([
|
|
14
|
+
"host",
|
|
15
|
+
"origin",
|
|
16
|
+
"referer",
|
|
17
|
+
"forwarded",
|
|
18
|
+
"xforwardedhost",
|
|
19
|
+
"xforwardedfor",
|
|
20
|
+
"xrealip",
|
|
21
|
+
]);
|
|
22
|
+
function normalizeToken(value) {
|
|
23
|
+
return value.replace(/[^a-z0-9]/giu, "").toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
function hasMeaningfulHeaderValue(value) {
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
return value.trim().length > 0;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
return Number.isFinite(value);
|
|
31
|
+
}
|
|
32
|
+
return value === true;
|
|
33
|
+
}
|
|
34
|
+
function normalizeTrustedDomain(value) {
|
|
35
|
+
const trimmed = value.trim().toLowerCase();
|
|
36
|
+
if (trimmed.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return new URL(trimmed).hostname.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
const withoutScheme = trimmed.replace(/^[a-z][a-z0-9+.-]*:\/\//u, "");
|
|
44
|
+
const domainOnly = withoutScheme.split(/[/?#]/u)[0]?.toLowerCase() ?? "";
|
|
45
|
+
return domainOnly.length > 0 ? domainOnly : null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function isTrustedDomain(hostname, trustedDomains) {
|
|
49
|
+
const lowerHost = hostname.toLowerCase();
|
|
50
|
+
for (const domain of trustedDomains) {
|
|
51
|
+
const normalized = normalizeTrustedDomain(domain);
|
|
52
|
+
if (!normalized) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (normalized.startsWith("*.")) {
|
|
56
|
+
const suffix = normalized.slice(1);
|
|
57
|
+
if (lowerHost.endsWith(suffix)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (lowerHost === normalized || lowerHost.endsWith(`.${normalized}`)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
function extractHostFromHeaderValue(value) {
|
|
69
|
+
if (typeof value !== "string") {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const trimmed = value.trim();
|
|
73
|
+
if (trimmed.length === 0) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return new URL(trimmed).hostname.toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Continue with host-like parsing.
|
|
81
|
+
}
|
|
82
|
+
const hostLike = trimmed.split(/[/?#]/u)[0] ?? "";
|
|
83
|
+
const normalizedHost = hostLike.replace(/:\d+$/u, "").toLowerCase();
|
|
84
|
+
if (/^[a-z0-9.-]+$/u.test(normalizedHost) && normalizedHost.includes(".")) {
|
|
85
|
+
return normalizedHost;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function makeFinding(filePath, field, ruleId, description, evidence) {
|
|
90
|
+
const location = { field };
|
|
91
|
+
if (typeof evidence?.line === "number") {
|
|
92
|
+
location.line = evidence.line;
|
|
93
|
+
}
|
|
94
|
+
if (typeof evidence?.column === "number") {
|
|
95
|
+
location.column = evidence.column;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
rule_id: ruleId,
|
|
99
|
+
finding_id: `CONSENT_BYPASS-${filePath}-${field}`,
|
|
100
|
+
severity: "CRITICAL",
|
|
101
|
+
category: "CONSENT_BYPASS",
|
|
102
|
+
layer: "L2",
|
|
103
|
+
file_path: filePath,
|
|
104
|
+
location,
|
|
105
|
+
description,
|
|
106
|
+
affected_tools: [
|
|
107
|
+
"claude-code",
|
|
108
|
+
"codex-cli",
|
|
109
|
+
"opencode",
|
|
110
|
+
"cursor",
|
|
111
|
+
"windsurf",
|
|
112
|
+
"github-copilot",
|
|
113
|
+
],
|
|
114
|
+
cve: null,
|
|
115
|
+
owasp: ["ASI05", "ASI09"],
|
|
116
|
+
cwe: "CWE-78",
|
|
117
|
+
confidence: "HIGH",
|
|
118
|
+
fixable: true,
|
|
119
|
+
remediation_actions: ["remove_field", "replace_with_default"],
|
|
120
|
+
evidence: evidence?.evidence ?? null,
|
|
121
|
+
suppressed: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function detectConsentBypass(input) {
|
|
125
|
+
const findings = [];
|
|
126
|
+
const trustedApiDomains = input.trustedApiDomains ?? [];
|
|
127
|
+
const parsed = (input.parsed && typeof input.parsed === "object"
|
|
128
|
+
? input.parsed
|
|
129
|
+
: {});
|
|
130
|
+
const stack = [{ value: parsed, path: "" }];
|
|
131
|
+
for (const { value, path } of stack) {
|
|
132
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const record = value;
|
|
136
|
+
for (const [key, child] of Object.entries(record)) {
|
|
137
|
+
const fieldPath = path.length > 0 ? `${path}.${key}` : key;
|
|
138
|
+
const normalized = key.toLowerCase();
|
|
139
|
+
if (AUTO_APPROVAL_KEYS.has(normalized) && child === true) {
|
|
140
|
+
const evidence = buildFindingEvidence({
|
|
141
|
+
textContent: input.textContent,
|
|
142
|
+
jsonPaths: [fieldPath],
|
|
143
|
+
searchTerms: [key, normalized],
|
|
144
|
+
fallbackValue: `${fieldPath} = true`,
|
|
145
|
+
});
|
|
146
|
+
findings.push(makeFinding(input.filePath, fieldPath, "cross-tool-auto-approval", `Cross-tool auto-approval flag is enabled: ${fieldPath}`, evidence));
|
|
147
|
+
}
|
|
148
|
+
stack.push({ value: child, path: fieldPath });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (parsed.enableAllProjectMcpServers === true) {
|
|
152
|
+
const evidence = buildFindingEvidence({
|
|
153
|
+
textContent: input.textContent,
|
|
154
|
+
jsonPaths: ["enableAllProjectMcpServers"],
|
|
155
|
+
searchTerms: ['"enableAllProjectMcpServers"', "enableAllProjectMcpServers"],
|
|
156
|
+
fallbackValue: "enableAllProjectMcpServers = true",
|
|
157
|
+
});
|
|
158
|
+
findings.push(makeFinding(input.filePath, "enableAllProjectMcpServers", "claude-mcp-consent-bypass", "Project-level MCP auto-approval bypass is enabled", evidence));
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(parsed.enabledMcpjsonServers) && parsed.enabledMcpjsonServers.length > 0) {
|
|
161
|
+
const evidence = buildFindingEvidence({
|
|
162
|
+
textContent: input.textContent,
|
|
163
|
+
jsonPaths: ["enabledMcpjsonServers"],
|
|
164
|
+
searchTerms: ['"enabledMcpjsonServers"', "enabledMcpjsonServers"],
|
|
165
|
+
fallbackValue: `enabledMcpjsonServers = ${JSON.stringify(parsed.enabledMcpjsonServers)}`,
|
|
166
|
+
});
|
|
167
|
+
findings.push(makeFinding(input.filePath, "enabledMcpjsonServers", "claude-mcp-server-auto-approval", "Specific MCP servers are auto-approved in project config", evidence));
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(parsed.trustedCommands)
|
|
170
|
+
? parsed.trustedCommands.length > 0
|
|
171
|
+
: typeof parsed.trustedCommands === "object" && parsed.trustedCommands !== null) {
|
|
172
|
+
const evidence = buildFindingEvidence({
|
|
173
|
+
textContent: input.textContent,
|
|
174
|
+
jsonPaths: ["trustedCommands"],
|
|
175
|
+
searchTerms: ['"trustedCommands"', "trustedCommands"],
|
|
176
|
+
fallbackValue: `trustedCommands = ${JSON.stringify(parsed.trustedCommands)}`,
|
|
177
|
+
});
|
|
178
|
+
findings.push(makeFinding(input.filePath, "trustedCommands", "trusted-commands-consent-bypass", "Trusted command allowlist may bypass consent prompts", evidence));
|
|
179
|
+
}
|
|
180
|
+
if (parsed.mcpMarketplaceEnabled === false) {
|
|
181
|
+
const evidence = buildFindingEvidence({
|
|
182
|
+
textContent: input.textContent,
|
|
183
|
+
jsonPaths: ["mcpMarketplaceEnabled"],
|
|
184
|
+
searchTerms: ['"mcpMarketplaceEnabled"', "mcpMarketplaceEnabled"],
|
|
185
|
+
fallbackValue: "mcpMarketplaceEnabled = false",
|
|
186
|
+
});
|
|
187
|
+
findings.push(makeFinding(input.filePath, "mcpMarketplaceEnabled", "cline-mcp-marketplace-disabled", "Cline remote policy disables MCP marketplace and local MCP server usage", evidence));
|
|
188
|
+
}
|
|
189
|
+
if (parsed.blockPersonalRemoteMCPServers === true) {
|
|
190
|
+
const evidence = buildFindingEvidence({
|
|
191
|
+
textContent: input.textContent,
|
|
192
|
+
jsonPaths: ["blockPersonalRemoteMCPServers"],
|
|
193
|
+
searchTerms: ['"blockPersonalRemoteMCPServers"', "blockPersonalRemoteMCPServers"],
|
|
194
|
+
fallbackValue: "blockPersonalRemoteMCPServers = true",
|
|
195
|
+
});
|
|
196
|
+
findings.push(makeFinding(input.filePath, "blockPersonalRemoteMCPServers", "cline-block-personal-remote-mcp", "Cline remote policy blocks personal remote MCP servers and enforces organization endpoints", evidence));
|
|
197
|
+
}
|
|
198
|
+
for (const arrayKey of REMOTE_MCP_ARRAY_KEYS) {
|
|
199
|
+
const remoteServers = parsed[arrayKey];
|
|
200
|
+
if (!Array.isArray(remoteServers)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
remoteServers.forEach((entry, index) => {
|
|
204
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const server = entry;
|
|
208
|
+
const alwaysEnabledField = `${arrayKey}.${index}.alwaysEnabled`;
|
|
209
|
+
if (server.alwaysEnabled === true) {
|
|
210
|
+
const evidence = buildFindingEvidence({
|
|
211
|
+
textContent: input.textContent,
|
|
212
|
+
jsonPaths: [alwaysEnabledField],
|
|
213
|
+
searchTerms: ['"alwaysEnabled"', "alwaysEnabled", arrayKey],
|
|
214
|
+
fallbackValue: `${alwaysEnabledField} = true`,
|
|
215
|
+
});
|
|
216
|
+
findings.push(makeFinding(input.filePath, alwaysEnabledField, "cline-remote-mcp-always-enabled", "Cline remote MCP server is configured as always-enabled and cannot be disabled by users", evidence));
|
|
217
|
+
}
|
|
218
|
+
const urlField = `${arrayKey}.${index}.url`;
|
|
219
|
+
let remoteUrlHost = null;
|
|
220
|
+
if (typeof server.url === "string") {
|
|
221
|
+
let protocol;
|
|
222
|
+
try {
|
|
223
|
+
const parsedUrl = new URL(server.url);
|
|
224
|
+
protocol = parsedUrl.protocol;
|
|
225
|
+
remoteUrlHost = parsedUrl.hostname.toLowerCase();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
protocol = "";
|
|
229
|
+
}
|
|
230
|
+
if (protocol === "http:") {
|
|
231
|
+
const evidence = buildFindingEvidence({
|
|
232
|
+
textContent: input.textContent,
|
|
233
|
+
jsonPaths: [urlField],
|
|
234
|
+
searchTerms: [server.url],
|
|
235
|
+
fallbackValue: `${urlField} = ${JSON.stringify(server.url)}`,
|
|
236
|
+
});
|
|
237
|
+
findings.push(makeFinding(input.filePath, urlField, "cline-remote-mcp-insecure-url", `Cline remote MCP server uses insecure HTTP endpoint: ${server.url}`, evidence));
|
|
238
|
+
}
|
|
239
|
+
if (protocol === "https:" &&
|
|
240
|
+
remoteUrlHost &&
|
|
241
|
+
trustedApiDomains.length > 0 &&
|
|
242
|
+
!isTrustedDomain(remoteUrlHost, trustedApiDomains)) {
|
|
243
|
+
const evidence = buildFindingEvidence({
|
|
244
|
+
textContent: input.textContent,
|
|
245
|
+
jsonPaths: [urlField],
|
|
246
|
+
searchTerms: [server.url, remoteUrlHost],
|
|
247
|
+
fallbackValue: `${urlField} = ${JSON.stringify(server.url)}`,
|
|
248
|
+
});
|
|
249
|
+
findings.push(makeFinding(input.filePath, urlField, "cline-remote-mcp-unallowlisted-url-domain", `Cline remote MCP server URL domain is not allowlisted: ${remoteUrlHost}`, evidence));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!server.headers || typeof server.headers !== "object" || Array.isArray(server.headers)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const [headerName, headerValue] of Object.entries(server.headers)) {
|
|
256
|
+
if (!hasMeaningfulHeaderValue(headerValue)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const normalizedHeader = normalizeToken(headerName);
|
|
260
|
+
const headerField = `${arrayKey}.${index}.headers.${headerName}`;
|
|
261
|
+
if (SENSITIVE_REMOTE_MCP_HEADER_KEYS.has(normalizedHeader)) {
|
|
262
|
+
const evidence = buildFindingEvidence({
|
|
263
|
+
textContent: input.textContent,
|
|
264
|
+
jsonPaths: [headerField, `${arrayKey}.${index}.headers`],
|
|
265
|
+
searchTerms: [headerName],
|
|
266
|
+
fallbackValue: `${headerField} = ${JSON.stringify(headerValue)}`,
|
|
267
|
+
});
|
|
268
|
+
findings.push(makeFinding(input.filePath, headerField, "cline-remote-mcp-sensitive-header", `Cline remote MCP server config injects sensitive credential-bearing header: ${headerName}`, evidence));
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (ROUTING_REMOTE_MCP_HEADER_KEYS.has(normalizedHeader)) {
|
|
272
|
+
const evidence = buildFindingEvidence({
|
|
273
|
+
textContent: input.textContent,
|
|
274
|
+
jsonPaths: [headerField, `${arrayKey}.${index}.headers`],
|
|
275
|
+
searchTerms: [headerName],
|
|
276
|
+
fallbackValue: `${headerField} = ${JSON.stringify(headerValue)}`,
|
|
277
|
+
});
|
|
278
|
+
findings.push(makeFinding(input.filePath, headerField, "cline-remote-mcp-routing-header", `Cline remote MCP server config injects routing/identity override header: ${headerName}`, evidence));
|
|
279
|
+
const headerHost = extractHostFromHeaderValue(headerValue);
|
|
280
|
+
if (headerHost &&
|
|
281
|
+
trustedApiDomains.length > 0 &&
|
|
282
|
+
!isTrustedDomain(headerHost, trustedApiDomains)) {
|
|
283
|
+
const domainEvidence = buildFindingEvidence({
|
|
284
|
+
textContent: input.textContent,
|
|
285
|
+
jsonPaths: [headerField, `${arrayKey}.${index}.headers`],
|
|
286
|
+
searchTerms: [headerName, headerHost],
|
|
287
|
+
fallbackValue: `${headerField} = ${JSON.stringify(headerValue)}`,
|
|
288
|
+
});
|
|
289
|
+
findings.push(makeFinding(input.filePath, headerField, "cline-remote-mcp-unallowlisted-header-domain", `Cline remote MCP routing header references non-allowlisted domain: ${headerHost}`, domainEvidence));
|
|
290
|
+
}
|
|
291
|
+
else if (headerHost &&
|
|
292
|
+
remoteUrlHost &&
|
|
293
|
+
headerHost !== remoteUrlHost &&
|
|
294
|
+
trustedApiDomains.length === 0) {
|
|
295
|
+
const mismatchEvidence = buildFindingEvidence({
|
|
296
|
+
textContent: input.textContent,
|
|
297
|
+
jsonPaths: [headerField, urlField],
|
|
298
|
+
searchTerms: [headerName, headerHost, remoteUrlHost],
|
|
299
|
+
fallbackValue: `${headerField} overrides ${remoteUrlHost} -> ${headerHost}`,
|
|
300
|
+
});
|
|
301
|
+
findings.push(makeFinding(input.filePath, headerField, "cline-remote-mcp-header-host-mismatch", `Cline remote MCP routing header host differs from server URL host (${remoteUrlHost} -> ${headerHost})`, mismatchEvidence));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const patterns = [
|
|
308
|
+
{ match: "--dangerously-skip-permissions", evidenceTerms: ["--dangerously-skip-permissions"] },
|
|
309
|
+
{ match: "--trust-all-tools", evidenceTerms: ["--trust-all-tools"] },
|
|
310
|
+
{ match: "--no-interactive", evidenceTerms: ["--no-interactive"] },
|
|
311
|
+
{ match: "alwaysallow", evidenceTerms: ["alwaysAllow", "alwaysallow"] },
|
|
312
|
+
{ match: "always_allow", evidenceTerms: ["always_allow"] },
|
|
313
|
+
{ match: "autoapprove", evidenceTerms: ["autoApprove", "autoapprove"] },
|
|
314
|
+
{ match: "auto_approve", evidenceTerms: ["auto_approve"] },
|
|
315
|
+
{ match: "yolo", evidenceTerms: ["YOLO", "yolo"] },
|
|
316
|
+
];
|
|
317
|
+
const lower = input.textContent.toLowerCase();
|
|
318
|
+
for (const pattern of patterns) {
|
|
319
|
+
if (lower.includes(pattern.match)) {
|
|
320
|
+
const evidence = buildFindingEvidence({
|
|
321
|
+
textContent: input.textContent,
|
|
322
|
+
searchTerms: pattern.evidenceTerms,
|
|
323
|
+
fallbackValue: `text contains "${pattern.match}"`,
|
|
324
|
+
});
|
|
325
|
+
findings.push(makeFinding(input.filePath, "script_flags", "consent-bypass-cli-flag", `Consent bypass CLI flag detected: ${pattern.match}`, evidence));
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return findings;
|
|
330
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface EnvOverrideInput {
|
|
3
|
+
filePath: string;
|
|
4
|
+
parsed: unknown;
|
|
5
|
+
textContent: string;
|
|
6
|
+
trustedApiDomains: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function detectEnvOverrides(input: EnvOverrideInput): Finding[];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
const CRITICAL_KEYS = new Set([
|
|
3
|
+
"ANTHROPIC_BASE_URL",
|
|
4
|
+
"ANTHROPIC_BEDROCK_BASE_URL",
|
|
5
|
+
"ANTHROPIC_VERTEX_BASE_URL",
|
|
6
|
+
"OPENAI_BASE_URL",
|
|
7
|
+
"OPENAI_API_BASE",
|
|
8
|
+
"CODEX_HOME",
|
|
9
|
+
]);
|
|
10
|
+
const HEADER_KEYS = new Set(["ANTHROPIC_CUSTOM_HEADERS"]);
|
|
11
|
+
const API_KEY_KEYS = new Set([
|
|
12
|
+
"ANTHROPIC_API_KEY",
|
|
13
|
+
"OPENAI_API_KEY",
|
|
14
|
+
"AZURE_OPENAI_API_KEY",
|
|
15
|
+
"GOOGLE_AI_API_KEY",
|
|
16
|
+
"DEEPSEEK_API_KEY",
|
|
17
|
+
]);
|
|
18
|
+
function getEnvRecord(parsed) {
|
|
19
|
+
if (!parsed || typeof parsed !== "object") {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
const root = parsed;
|
|
23
|
+
if (root.env && typeof root.env === "object") {
|
|
24
|
+
return root.env;
|
|
25
|
+
}
|
|
26
|
+
return root;
|
|
27
|
+
}
|
|
28
|
+
function isTrustedHost(hostname, trustedApiDomains) {
|
|
29
|
+
if (hostname === "api.anthropic.com" || hostname.endsWith(".anthropic.com")) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (hostname === "api.openai.com" || hostname.endsWith(".openai.azure.com")) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (hostname.endsWith(".amazonaws.com")) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (hostname.endsWith(".googleapis.com")) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return trustedApiDomains.some((domain) => {
|
|
42
|
+
if (domain.startsWith("*.")) {
|
|
43
|
+
return hostname.endsWith(domain.slice(1));
|
|
44
|
+
}
|
|
45
|
+
return hostname === domain;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function makeFinding(filePath, field, ruleId, severity, description, evidence) {
|
|
49
|
+
const location = { field };
|
|
50
|
+
if (typeof evidence?.line === "number") {
|
|
51
|
+
location.line = evidence.line;
|
|
52
|
+
}
|
|
53
|
+
if (typeof evidence?.column === "number") {
|
|
54
|
+
location.column = evidence.column;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
rule_id: ruleId,
|
|
58
|
+
finding_id: `ENV_OVERRIDE-${filePath}-${field}`,
|
|
59
|
+
severity,
|
|
60
|
+
category: "ENV_OVERRIDE",
|
|
61
|
+
layer: "L2",
|
|
62
|
+
file_path: filePath,
|
|
63
|
+
location,
|
|
64
|
+
description,
|
|
65
|
+
affected_tools: [
|
|
66
|
+
"claude-code",
|
|
67
|
+
"codex-cli",
|
|
68
|
+
"opencode",
|
|
69
|
+
"cursor",
|
|
70
|
+
"windsurf",
|
|
71
|
+
"github-copilot",
|
|
72
|
+
],
|
|
73
|
+
cve: null,
|
|
74
|
+
owasp: ["ASI03", "ASI06"],
|
|
75
|
+
cwe: "CWE-522",
|
|
76
|
+
confidence: "HIGH",
|
|
77
|
+
fixable: true,
|
|
78
|
+
remediation_actions: ["remove_field", "replace_with_default"],
|
|
79
|
+
evidence: evidence?.evidence ?? null,
|
|
80
|
+
suppressed: false,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function detectEnvOverrides(input) {
|
|
84
|
+
const root = input.parsed && typeof input.parsed === "object"
|
|
85
|
+
? input.parsed
|
|
86
|
+
: null;
|
|
87
|
+
const hasEnvObject = !!(root && root.env && typeof root.env === "object");
|
|
88
|
+
const env = getEnvRecord(input.parsed);
|
|
89
|
+
const findings = [];
|
|
90
|
+
for (const [key, rawValue] of Object.entries(env)) {
|
|
91
|
+
if (typeof rawValue !== "string") {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const field = `env.${key}`;
|
|
95
|
+
const evidence = buildFindingEvidence({
|
|
96
|
+
textContent: input.textContent,
|
|
97
|
+
jsonPaths: hasEnvObject ? [field] : [key, field],
|
|
98
|
+
searchTerms: [`"${key}"`, rawValue],
|
|
99
|
+
fallbackValue: `${field} = ${rawValue}`,
|
|
100
|
+
});
|
|
101
|
+
if (HEADER_KEYS.has(key) || key.endsWith("_CUSTOM_HEADERS") || key.endsWith("_EXTRA_HEADERS")) {
|
|
102
|
+
findings.push(makeFinding(input.filePath, field, "env-custom-headers-override", "HIGH", `${key} injects custom API headers`, evidence));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (API_KEY_KEYS.has(key)) {
|
|
106
|
+
findings.push(makeFinding(input.filePath, field, "env-api-key-override", "MEDIUM", `${key} overrides AI tool credentials at project scope`, evidence));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const looksLikeEndpoint = CRITICAL_KEYS.has(key) ||
|
|
110
|
+
key.endsWith("_BASE_URL") ||
|
|
111
|
+
key.endsWith("_API_URL") ||
|
|
112
|
+
key.endsWith("_ENDPOINT");
|
|
113
|
+
if (!looksLikeEndpoint) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL(rawValue);
|
|
118
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
119
|
+
findings.push(makeFinding(input.filePath, field, "env-local-endpoint-override", "MEDIUM", `${key} points to localhost/loopback and may intercept API traffic`, evidence));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (isTrustedHost(url.hostname, input.trustedApiDomains)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
findings.push(makeFinding(input.filePath, field, "env-base-url-override", "CRITICAL", `${key} redirects API traffic to an untrusted domain`, evidence));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
findings.push(makeFinding(input.filePath, field, "env-invalid-endpoint-override", "CRITICAL", `${key} contains an invalid endpoint override`, evidence));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface GitHookEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
executable: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface GitHooksInput {
|
|
8
|
+
hooks: GitHookEntry[];
|
|
9
|
+
knownSafeHooks?: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function detectGitHookIssues(input: GitHooksInput): Finding[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
function makeFinding(path, description, evidence) {
|
|
3
|
+
const location = { field: "hook_content" };
|
|
4
|
+
if (typeof evidence?.line === "number") {
|
|
5
|
+
location.line = evidence.line;
|
|
6
|
+
}
|
|
7
|
+
if (typeof evidence?.column === "number") {
|
|
8
|
+
location.column = evidence.column;
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
rule_id: "git-hook-suspicious-pattern",
|
|
12
|
+
finding_id: `GIT_HOOK-${path}`,
|
|
13
|
+
severity: "MEDIUM",
|
|
14
|
+
category: "GIT_HOOK",
|
|
15
|
+
layer: "L2",
|
|
16
|
+
file_path: path,
|
|
17
|
+
location,
|
|
18
|
+
description,
|
|
19
|
+
affected_tools: [
|
|
20
|
+
"claude-code",
|
|
21
|
+
"codex-cli",
|
|
22
|
+
"opencode",
|
|
23
|
+
"cursor",
|
|
24
|
+
"windsurf",
|
|
25
|
+
"github-copilot",
|
|
26
|
+
],
|
|
27
|
+
cve: null,
|
|
28
|
+
owasp: ["ASI05", "ASI06"],
|
|
29
|
+
cwe: "CWE-78",
|
|
30
|
+
confidence: "HIGH",
|
|
31
|
+
fixable: true,
|
|
32
|
+
remediation_actions: ["remove_execute_permission", "quarantine_file", "remove_file"],
|
|
33
|
+
evidence: evidence?.evidence ?? null,
|
|
34
|
+
suppressed: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function detectGitHookIssues(input) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
const knownSafeHooks = new Set((input.knownSafeHooks ?? []).map((hook) => hook.trim()).filter((hook) => hook.length > 0));
|
|
40
|
+
const suspiciousPattern = /\b(curl|wget|nc|ncat|socat)\b|[|;&`]|[$][(]/u;
|
|
41
|
+
const exfilPattern = /(~\/\.ssh|~\/\.aws|\.env|id_rsa|git-credentials)/u;
|
|
42
|
+
for (const hook of input.hooks) {
|
|
43
|
+
if (knownSafeHooks.has(hook.path)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!hook.executable) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (suspiciousPattern.test(hook.content) || exfilPattern.test(hook.content)) {
|
|
50
|
+
const suspiciousMatch = hook.content.match(suspiciousPattern)?.[0];
|
|
51
|
+
const exfilMatch = hook.content.match(exfilPattern)?.[0];
|
|
52
|
+
const evidence = buildFindingEvidence({
|
|
53
|
+
textContent: hook.content,
|
|
54
|
+
searchTerms: [suspiciousMatch ?? "", exfilMatch ?? ""],
|
|
55
|
+
fallbackValue: "suspicious hook content detected",
|
|
56
|
+
});
|
|
57
|
+
findings.push(makeFinding(hook.path, "Executable hook contains suspicious command or exfiltration pattern", evidence));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return findings;
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface IdeSettingsInput {
|
|
3
|
+
filePath: string;
|
|
4
|
+
parsed: unknown;
|
|
5
|
+
textContent: string;
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function detectIdeSettingsIssues(input: IdeSettingsInput): Finding[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isAbsolute, join, normalize, relative } from "node:path";
|
|
2
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
3
|
+
const KNOWN_DANGEROUS_KEYS = new Set(["php.validate.executablePath", "PATH_TO_GIT"]);
|
|
4
|
+
function isInsideProject(pathValue, projectRoot) {
|
|
5
|
+
const resolved = isAbsolute(pathValue)
|
|
6
|
+
? normalize(pathValue)
|
|
7
|
+
: normalize(join(projectRoot, pathValue));
|
|
8
|
+
const rel = relative(normalize(projectRoot), resolved);
|
|
9
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
10
|
+
}
|
|
11
|
+
function makeFinding(filePath, field, severity, ruleId, description, evidence) {
|
|
12
|
+
const location = { field };
|
|
13
|
+
if (typeof evidence?.line === "number") {
|
|
14
|
+
location.line = evidence.line;
|
|
15
|
+
}
|
|
16
|
+
if (typeof evidence?.column === "number") {
|
|
17
|
+
location.column = evidence.column;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
rule_id: ruleId,
|
|
21
|
+
finding_id: `IDE_SETTINGS-${filePath}-${field}`,
|
|
22
|
+
severity,
|
|
23
|
+
category: "IDE_SETTINGS",
|
|
24
|
+
layer: "L2",
|
|
25
|
+
file_path: filePath,
|
|
26
|
+
location,
|
|
27
|
+
description,
|
|
28
|
+
affected_tools: ["cursor", "github-copilot", "windsurf", "claude-code"],
|
|
29
|
+
cve: null,
|
|
30
|
+
owasp: ["ASI05", "ASI06"],
|
|
31
|
+
cwe: "CWE-78",
|
|
32
|
+
confidence: "HIGH",
|
|
33
|
+
fixable: true,
|
|
34
|
+
remediation_actions: ["remove_field", "replace_with_default"],
|
|
35
|
+
evidence: evidence?.evidence ?? null,
|
|
36
|
+
suppressed: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function detectIdeSettingsIssues(input) {
|
|
40
|
+
if (!input.parsed || typeof input.parsed !== "object") {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const settings = input.parsed;
|
|
44
|
+
const findings = [];
|
|
45
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
46
|
+
if (typeof value !== "string") {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!isInsideProject(value, input.projectRoot)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const evidence = buildFindingEvidence({
|
|
53
|
+
textContent: input.textContent,
|
|
54
|
+
searchTerms: [`"${key}"`, key, value],
|
|
55
|
+
fallbackValue: `${key} = ${value}`,
|
|
56
|
+
});
|
|
57
|
+
if (KNOWN_DANGEROUS_KEYS.has(key)) {
|
|
58
|
+
findings.push(makeFinding(input.filePath, key, "CRITICAL", "ide-known-dangerous-executable-path", `Known-dangerous executable path key points inside project: ${key}`, evidence));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (/(path|executable|binary|command|interpreter)/iu.test(key)) {
|
|
62
|
+
findings.push(makeFinding(input.filePath, key, "HIGH", "ide-pattern-executable-path", `Executable-like settings key points inside project: ${key}`, evidence));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface PluginManifestInput {
|
|
3
|
+
filePath: string;
|
|
4
|
+
parsed: unknown;
|
|
5
|
+
textContent: string;
|
|
6
|
+
trustedApiDomains: string[];
|
|
7
|
+
blockedCommands: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function detectPluginManifestIssues(input: PluginManifestInput): Finding[];
|