mcp-security-lab 0.2.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.
@@ -0,0 +1,145 @@
1
+ const SAFE_ENV_KEYS = [
2
+ "COMSPEC",
3
+ "OS",
4
+ "PATH",
5
+ "PATHEXT",
6
+ "SYSTEMDRIVE",
7
+ "SYSTEMROOT",
8
+ "TEMP",
9
+ "TMP",
10
+ "WINDIR",
11
+ "HOME",
12
+ "LANG",
13
+ "LC_ALL",
14
+ "SHELL",
15
+ ];
16
+ const NON_OVERRIDABLE_ENV_KEYS = [
17
+ "DYLD_INSERT_LIBRARIES",
18
+ "DYLD_LIBRARY_PATH",
19
+ "LD_LIBRARY_PATH",
20
+ "LD_PRELOAD",
21
+ "MCP_SECURITY_LAB",
22
+ "NODE_OPTIONS",
23
+ "NODE_PATH",
24
+ ];
25
+ export const RESERVED_ENV_KEYS = new Set([
26
+ ...SAFE_ENV_KEYS,
27
+ ...NON_OVERRIDABLE_ENV_KEYS,
28
+ ]);
29
+ const SENSITIVE_FLAG_PATTERN = /^--?(?:api[-_]?key|authorization|credential|password|secret|token)$/i;
30
+ const SENSITIVE_ASSIGNMENT_PATTERN = /^((?:--?)?(?:api[-_]?key|authorization|credential|password|secret|token)=).+$/i;
31
+ const SENSITIVE_ENV_ASSIGNMENT_PATTERN = /^([A-Z0-9_]*(?:API_KEY|AUTHORIZATION|CREDENTIAL|PASSWORD|SECRET|TOKEN)=).+$/i;
32
+ const URL_CREDENTIAL_PATTERN = /^([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/i;
33
+ export function createSanitizedEnvironment(source = process.env) {
34
+ const environment = {};
35
+ for (const key of SAFE_ENV_KEYS) {
36
+ const value = source[key];
37
+ if (value !== undefined) {
38
+ environment[key] = value;
39
+ }
40
+ }
41
+ environment.MCP_SECURITY_LAB = "1";
42
+ return environment;
43
+ }
44
+ function normalizeKey(key) {
45
+ return key.toLowerCase().replace(/[-_]/g, "");
46
+ }
47
+ // Compound names that are unambiguously credentials. Matched as substrings so
48
+ // client_secret, refresh_token, id_token, X-Amz-Security-Token, X-Amz-Signature,
49
+ // api_key, etc. are all caught.
50
+ const CREDENTIAL_NEEDLES = [
51
+ "secret",
52
+ "password",
53
+ "passwd",
54
+ "credential",
55
+ "signature",
56
+ "apikey",
57
+ "accesskey",
58
+ "privatekey",
59
+ "accesstoken",
60
+ "refreshtoken",
61
+ "idtoken",
62
+ "securitytoken",
63
+ "sessiontoken",
64
+ "bearer",
65
+ ];
66
+ // Additional bare keys redacted from reports (over-redaction is safe there) but
67
+ // NOT treated as findings, since a bare ?token=/?key= is often an opaque cursor.
68
+ const BROAD_REDACTION_KEYS = new Set(["token", "key", "auth", "authorization", "sig", "pwd"]);
69
+ /** Strong, low-false-positive test used to RAISE a credentials-in-URL finding. */
70
+ export function isCredentialQueryKey(key) {
71
+ const normalized = normalizeKey(key);
72
+ return CREDENTIAL_NEEDLES.some((needle) => normalized.includes(needle));
73
+ }
74
+ /** Broad test used only to REDACT report output, where over-redaction is safe. */
75
+ function isRedactableQueryKey(key) {
76
+ return isCredentialQueryKey(key) || BROAD_REDACTION_KEYS.has(normalizeKey(key));
77
+ }
78
+ function redactParams(params) {
79
+ let changed = false;
80
+ for (const key of [...params.keys()]) {
81
+ if (isRedactableQueryKey(key)) {
82
+ params.set(key, "[REDACTED]");
83
+ changed = true;
84
+ }
85
+ }
86
+ return changed;
87
+ }
88
+ /**
89
+ * Remove credentials from a URL before it enters a report: strip any userinfo
90
+ * (`user:pass@`) and redact the value of sensitive query AND fragment parameters
91
+ * (OAuth implicit-flow tokens live in the fragment). Falls back to a coarse regex
92
+ * when the string is not a parseable URL.
93
+ */
94
+ export function redactUrl(rawUrl) {
95
+ try {
96
+ const url = new URL(rawUrl);
97
+ if (url.username !== "" || url.password !== "") {
98
+ url.username = "[REDACTED]";
99
+ url.password = "";
100
+ }
101
+ redactParams(url.searchParams);
102
+ if (url.hash.length > 1) {
103
+ const fragment = new URLSearchParams(url.hash.slice(1));
104
+ if (redactParams(fragment)) {
105
+ url.hash = `#${fragment.toString()}`;
106
+ }
107
+ }
108
+ // URL percent-encodes the brackets of our sentinel; restore it for readability.
109
+ return url.toString().replace(/%5BREDACTED%5D/g, "[REDACTED]");
110
+ }
111
+ catch {
112
+ return rawUrl.replace(URL_CREDENTIAL_PATTERN, "$1[REDACTED]@");
113
+ }
114
+ }
115
+ export function redactArguments(args) {
116
+ const redacted = [];
117
+ let redactNext = false;
118
+ for (const argument of args) {
119
+ if (redactNext) {
120
+ redacted.push("[REDACTED]");
121
+ redactNext = false;
122
+ continue;
123
+ }
124
+ if (SENSITIVE_FLAG_PATTERN.test(argument)) {
125
+ redacted.push(argument);
126
+ redactNext = true;
127
+ continue;
128
+ }
129
+ if (SENSITIVE_ASSIGNMENT_PATTERN.test(argument)) {
130
+ redacted.push(argument.replace(SENSITIVE_ASSIGNMENT_PATTERN, "$1[REDACTED]"));
131
+ continue;
132
+ }
133
+ if (SENSITIVE_ENV_ASSIGNMENT_PATTERN.test(argument)) {
134
+ redacted.push(argument.replace(SENSITIVE_ENV_ASSIGNMENT_PATTERN, "$1[REDACTED]"));
135
+ continue;
136
+ }
137
+ if (URL_CREDENTIAL_PATTERN.test(argument)) {
138
+ redacted.push(argument.replace(URL_CREDENTIAL_PATTERN, "$1[REDACTED]@"));
139
+ continue;
140
+ }
141
+ redacted.push(argument);
142
+ }
143
+ return redacted;
144
+ }
145
+ //# sourceMappingURL=environment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/environment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { ScanReport } from "./types.js";
2
+ export declare function reportAsJson(report: ScanReport): string;
3
+ export declare function reportAsSarif(report: ScanReport, configPath: string, workspaceRoot?: string): string;
4
+ export declare function reportAsText(report: ScanReport): string;
@@ -0,0 +1,161 @@
1
+ import { createHash } from "node:crypto";
2
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
3
+ const ICONS = {
4
+ critical: "CRITICAL",
5
+ high: "HIGH",
6
+ medium: "MEDIUM",
7
+ low: "LOW",
8
+ info: "INFO",
9
+ };
10
+ export function reportAsJson(report) {
11
+ return `${JSON.stringify(report, null, 2)}\n`;
12
+ }
13
+ function sarifLevel(severity) {
14
+ if (severity === "critical" || severity === "high") {
15
+ return "error";
16
+ }
17
+ if (severity === "medium") {
18
+ return "warning";
19
+ }
20
+ if (severity === "low") {
21
+ return "note";
22
+ }
23
+ return "none";
24
+ }
25
+ function artifactUri(configPath, workspaceRoot) {
26
+ const absoluteConfigPath = resolve(configPath);
27
+ const absoluteWorkspaceRoot = resolve(workspaceRoot);
28
+ const relativePath = relative(absoluteWorkspaceRoot, absoluteConfigPath);
29
+ const outsideWorkspace = relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath);
30
+ return (outsideWorkspace ? basename(absoluteConfigPath) : relativePath || basename(absoluteConfigPath))
31
+ .split(sep)
32
+ .join("/");
33
+ }
34
+ function fingerprint(id, location, title, evidence) {
35
+ return createHash("sha256")
36
+ .update([id, location ?? "", title, evidence].join("\u0000"))
37
+ .digest("hex");
38
+ }
39
+ export function reportAsSarif(report, configPath, workspaceRoot = process.env.GITHUB_WORKSPACE ?? process.cwd()) {
40
+ const rules = new Map();
41
+ const uri = artifactUri(configPath, workspaceRoot);
42
+ for (const finding of report.findings) {
43
+ if (!rules.has(finding.id)) {
44
+ const tags = ["security", "mcp"];
45
+ if (finding.cwe !== undefined) {
46
+ tags.push(`external/cwe/${finding.cwe.toLowerCase()}`);
47
+ }
48
+ if (finding.owasp !== undefined) {
49
+ tags.push(`external/owasp-llm/${finding.owasp.toLowerCase()}`);
50
+ }
51
+ const taxonomy = [
52
+ finding.cwe === undefined ? undefined : `CWE: ${finding.cwe}`,
53
+ finding.owasp === undefined ? undefined : `OWASP LLM: ${finding.owasp}`,
54
+ ]
55
+ .filter((entry) => entry !== undefined)
56
+ .join(" | ");
57
+ rules.set(finding.id, {
58
+ id: finding.id,
59
+ shortDescription: { text: finding.title },
60
+ fullDescription: { text: finding.recommendation },
61
+ defaultConfiguration: { level: sarifLevel(finding.severity) },
62
+ help: {
63
+ text: finding.recommendation,
64
+ markdown: `**Recommendation:** ${finding.recommendation}${taxonomy === "" ? "" : `\n\n**Taxonomy:** ${taxonomy}`}`,
65
+ },
66
+ properties: {
67
+ tags,
68
+ ...(finding.cwe === undefined ? {} : { cwe: finding.cwe }),
69
+ ...(finding.owasp === undefined ? {} : { owasp: finding.owasp }),
70
+ },
71
+ });
72
+ }
73
+ }
74
+ const sarif = {
75
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
76
+ version: "2.1.0",
77
+ runs: [
78
+ {
79
+ tool: {
80
+ driver: {
81
+ name: report.scanner.name,
82
+ semanticVersion: report.scanner.version,
83
+ rules: [...rules.values()],
84
+ },
85
+ },
86
+ automationDetails: {
87
+ id: "mcp-security-lab/",
88
+ },
89
+ invocations: [
90
+ {
91
+ executionSuccessful: true,
92
+ properties: {
93
+ connected: report.execution.connected,
94
+ toolsInvoked: report.execution.toolsInvoked,
95
+ osSandboxed: report.execution.osSandboxed,
96
+ },
97
+ },
98
+ ],
99
+ results: report.findings.map((finding) => ({
100
+ ruleId: finding.id,
101
+ level: sarifLevel(finding.severity),
102
+ message: {
103
+ text: `${finding.title}\n\nEvidence: ${finding.evidence}\n\nRecommendation: ${finding.recommendation}`,
104
+ },
105
+ locations: [
106
+ {
107
+ physicalLocation: {
108
+ artifactLocation: { uri },
109
+ region: { startLine: 1 },
110
+ },
111
+ logicalLocations: finding.location === undefined
112
+ ? []
113
+ : [{ fullyQualifiedName: finding.location, kind: "configuration" }],
114
+ },
115
+ ],
116
+ partialFingerprints: {
117
+ "mcpSecurityLab/v1": fingerprint(finding.id, finding.location, finding.title, finding.evidence),
118
+ },
119
+ properties: {
120
+ severity: finding.severity,
121
+ recommendation: finding.recommendation,
122
+ scannerLocation: finding.location ?? "scan",
123
+ ...(finding.cwe === undefined ? {} : { cwe: finding.cwe }),
124
+ ...(finding.owasp === undefined ? {} : { owasp: finding.owasp }),
125
+ },
126
+ })),
127
+ },
128
+ ],
129
+ };
130
+ return `${JSON.stringify(sarif, null, 2)}\n`;
131
+ }
132
+ export function reportAsText(report) {
133
+ const targetLabel = report.target.url !== undefined
134
+ ? report.target.url
135
+ : `${report.target.command} ${(report.target.args ?? []).join(" ")}`.trim();
136
+ const lines = [
137
+ "MCP Security Lab",
138
+ `Target: ${targetLabel}`,
139
+ `Connected: ${report.execution.connected ? "yes" : "no"} | Transport: ${report.execution.transport} | Tools invoked: ${report.execution.toolsInvoked} | OS sandbox: ${report.execution.osSandboxed ? "yes" : "no"}`,
140
+ `Findings: ${report.summary.critical} critical, ${report.summary.high} high, ${report.summary.medium} medium, ${report.summary.low} low, ${report.summary.info} info`,
141
+ "",
142
+ ];
143
+ for (const finding of report.findings) {
144
+ lines.push(`[${ICONS[finding.severity]}] ${finding.id} ${finding.title}`);
145
+ lines.push(` Evidence: ${finding.evidence}`);
146
+ lines.push(` Recommendation: ${finding.recommendation}`);
147
+ const taxonomy = [finding.cwe, finding.owasp].filter(Boolean).join(", ");
148
+ if (taxonomy !== "") {
149
+ lines.push(` Taxonomy: ${taxonomy}`);
150
+ }
151
+ if (finding.location !== undefined) {
152
+ lines.push(` Location: ${finding.location}`);
153
+ }
154
+ lines.push("");
155
+ }
156
+ lines.push(report.execution.osSandboxed
157
+ ? "Note: target ran inside a network-isolated Docker container."
158
+ : "Limitation: target processes are not isolated from the host filesystem or network.");
159
+ return `${lines.join("\n")}\n`;
160
+ }
161
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.js","sourceRoot":"","sources":["../../src/reporter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { Finding } from "../types.js";
2
+ /**
3
+ * Normalize untrusted text before pattern matching. NFKC folds homoglyph and
4
+ * fullwidth variants onto their canonical form so that, for example, fullwidth
5
+ * "ignore" collapses to "ignore"; invisible characters are then stripped so
6
+ * they cannot break up a keyword mid-word.
7
+ */
8
+ export declare function normalizeText(text: string): string;
9
+ export declare function hasInvisibleCharacters(text: string): boolean;
10
+ /**
11
+ * Scan a single untrusted string for prompt-injection-like instructions.
12
+ * Matching runs against the normalized form so evasion via Unicode tricks is
13
+ * defeated. `sourceLabel` names where the text came from (e.g. "description",
14
+ * "parameter data.query") for actionable evidence.
15
+ */
16
+ export declare function scanTextForInjection(text: string, location: string, sourceLabel: string): Finding[];
@@ -0,0 +1,91 @@
1
+ const PROMPT_INJECTION_PATTERNS = [
2
+ {
3
+ pattern: /\bignore (?:all |any )?(?:previous|prior) instructions?\b/i,
4
+ label: "instruction override",
5
+ },
6
+ {
7
+ pattern: /\b(?:always|must) (?:call|invoke|use) (?:this|the) tool\b/i,
8
+ label: "forced invocation",
9
+ },
10
+ { pattern: /\bdo not (?:tell|inform|show) the user\b/i, label: "concealment" },
11
+ { pattern: /\b(?:system prompt|hidden instructions?)\b/i, label: "hidden prompt reference" },
12
+ { pattern: /\bdisable|bypass\b.{0,30}\b(?:safety|security|permission)/i, label: "safety bypass" },
13
+ ];
14
+ // Characters an author never needs in a human-readable description but that an
15
+ // attacker uses to smuggle instructions past a naive substring scan. Built from
16
+ // explicit code-point ranges so this source file itself stays free of invisible
17
+ // characters and remains reviewable.
18
+ // C0 controls (keep \t=0x09, \n=0x0A, \r=0x0D), DEL + C1 controls
19
+ // soft hyphen, zero-width space..RLM, bidi overrides, word joiner..BOM
20
+ const INVISIBLE_CODEPOINT_RANGES = [
21
+ [0x0, 0x8],
22
+ [0xb, 0xc],
23
+ [0xe, 0x1f],
24
+ [0x7f, 0x9f],
25
+ [0xad, 0xad], // soft hyphen
26
+ [0x200b, 0x200f], // zero-width space, ZWNJ, ZWJ, LRM, RLM
27
+ [0x202a, 0x202e], // bidirectional embedding/override controls
28
+ [0x2060, 0x2064], // word joiner, invisible operators
29
+ [0xfeff, 0xfeff], // zero-width no-break space / BOM
30
+ ];
31
+ function buildInvisibleCharPattern(flags) {
32
+ const body = INVISIBLE_CODEPOINT_RANGES.map(([start, end]) => start === end
33
+ ? `\\u{${start.toString(16)}}`
34
+ : `\\u{${start.toString(16)}}-\\u{${end.toString(16)}}`).join("");
35
+ return new RegExp(`[${body}]`, flags);
36
+ }
37
+ const INVISIBLE_CHAR_TEST = buildInvisibleCharPattern("u");
38
+ const INVISIBLE_CHAR_STRIP = buildInvisibleCharPattern("gu");
39
+ /**
40
+ * Normalize untrusted text before pattern matching. NFKC folds homoglyph and
41
+ * fullwidth variants onto their canonical form so that, for example, fullwidth
42
+ * "ignore" collapses to "ignore"; invisible characters are then stripped so
43
+ * they cannot break up a keyword mid-word.
44
+ */
45
+ export function normalizeText(text) {
46
+ return text.normalize("NFKC").replace(INVISIBLE_CHAR_STRIP, "");
47
+ }
48
+ export function hasInvisibleCharacters(text) {
49
+ return INVISIBLE_CHAR_TEST.test(text);
50
+ }
51
+ /**
52
+ * Scan a single untrusted string for prompt-injection-like instructions.
53
+ * Matching runs against the normalized form so evasion via Unicode tricks is
54
+ * defeated. `sourceLabel` names where the text came from (e.g. "description",
55
+ * "parameter data.query") for actionable evidence.
56
+ */
57
+ export function scanTextForInjection(text, location, sourceLabel) {
58
+ const findings = [];
59
+ if (text.trim() === "") {
60
+ return findings;
61
+ }
62
+ if (hasInvisibleCharacters(text)) {
63
+ findings.push({
64
+ id: "TOOL012",
65
+ severity: "high",
66
+ title: "Text contains invisible or control characters",
67
+ evidence: `The ${sourceLabel} contains zero-width, bidirectional, or control characters that are invisible to a human reviewer.`,
68
+ recommendation: "Remove invisible and control characters; server-injected text must be exactly what a human reviewer sees.",
69
+ location,
70
+ cwe: "CWE-1007",
71
+ owasp: "LLM01",
72
+ });
73
+ }
74
+ const normalized = normalizeText(text);
75
+ for (const candidate of PROMPT_INJECTION_PATTERNS) {
76
+ if (candidate.pattern.test(normalized)) {
77
+ findings.push({
78
+ id: "TOOL003",
79
+ severity: "high",
80
+ title: "Text contains a prompt-injection-like instruction",
81
+ evidence: `The ${sourceLabel} matched the ${candidate.label} pattern.`,
82
+ recommendation: "Describe functionality only; remove behavioral or hidden instructions.",
83
+ location,
84
+ cwe: "CWE-77",
85
+ owasp: "LLM01",
86
+ });
87
+ }
88
+ }
89
+ return findings;
90
+ }
91
+ //# sourceMappingURL=injection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injection.js","sourceRoot":"","sources":["../../../src/rules/injection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Finding, TargetConfig } from "../types.js";
2
+ export declare function inspectLaunch(target: TargetConfig): Finding[];
@@ -0,0 +1,102 @@
1
+ import { basename } from "node:path";
2
+ const SHELL_NAMES = new Set([
3
+ "bash",
4
+ "bash.exe",
5
+ "cmd",
6
+ "cmd.exe",
7
+ "pwsh",
8
+ "pwsh.exe",
9
+ "powershell",
10
+ "powershell.exe",
11
+ "sh",
12
+ "sh.exe",
13
+ ]);
14
+ const NETWORK_INSTALLERS = new Set(["npx", "npx.cmd", "pipx", "uvx"]);
15
+ const SUSPICIOUS_ARGUMENTS = [
16
+ { pattern: /\brm\s+-rf\b/i, label: "recursive deletion" },
17
+ { pattern: /\bremove-item\b.*\b-recurse\b/i, label: "recursive deletion" },
18
+ { pattern: /\bcurl\b|\bwget\b|invoke-webrequest/i, label: "network download" },
19
+ { pattern: /(?:^|[;&|])\s*(?:cmd|powershell|pwsh|bash|sh)\b/i, label: "nested shell" },
20
+ { pattern: /\b(?:sudo|runas)\b/i, label: "privilege escalation" },
21
+ { pattern: /\b(?:iex|invoke-expression)\b/i, label: "dynamic code execution" },
22
+ { pattern: /\bfrombase64string\b|base64\s+-d\b|base64\s+--decode\b/i, label: "encoded payload" },
23
+ ];
24
+ // Interpreters paired with the flags that make them execute inline source
25
+ // supplied on the command line, bypassing any reviewable script file.
26
+ const INLINE_EVAL_RULES = [
27
+ {
28
+ executables: new Set(["node", "node.exe", "deno", "deno.exe", "bun", "bun.exe"]),
29
+ flags: /^(?:-e|--eval|-p|--print|eval)$/i,
30
+ },
31
+ {
32
+ executables: new Set(["python", "python.exe", "python3", "python3.exe"]),
33
+ flags: /^-c$/,
34
+ },
35
+ {
36
+ executables: new Set(["ruby", "ruby.exe", "perl", "perl.exe"]),
37
+ flags: /^-e$/,
38
+ },
39
+ {
40
+ executables: new Set(["pwsh", "pwsh.exe", "powershell", "powershell.exe"]),
41
+ flags: /^-(?:e|ec|enc|encodedcommand|command|c)$/i,
42
+ },
43
+ ];
44
+ export function inspectLaunch(target) {
45
+ const findings = [];
46
+ if (target.command === undefined || target.args === undefined) {
47
+ return findings;
48
+ }
49
+ const executable = basename(target.command).toLowerCase();
50
+ const joinedArguments = target.args.join(" ");
51
+ if (SHELL_NAMES.has(executable)) {
52
+ findings.push({
53
+ id: "LAUNCH001",
54
+ severity: "high",
55
+ title: "Target is launched through a general-purpose shell",
56
+ evidence: `Configured executable is "${executable}".`,
57
+ recommendation: "Launch a fixed executable directly and pass arguments as an array.",
58
+ location: "target.command",
59
+ cwe: "CWE-78",
60
+ });
61
+ }
62
+ if (NETWORK_INSTALLERS.has(executable)) {
63
+ findings.push({
64
+ id: "LAUNCH002",
65
+ severity: "medium",
66
+ title: "Target may download executable code at startup",
67
+ evidence: `Configured executable is "${executable}".`,
68
+ recommendation: "Pin and install the package before scanning, then execute the local binary.",
69
+ location: "target.command",
70
+ cwe: "CWE-829",
71
+ });
72
+ }
73
+ for (const candidate of SUSPICIOUS_ARGUMENTS) {
74
+ if (candidate.pattern.test(joinedArguments)) {
75
+ findings.push({
76
+ id: "LAUNCH003",
77
+ severity: "high",
78
+ title: "Suspicious launcher argument",
79
+ evidence: `Arguments contain a pattern associated with ${candidate.label}.`,
80
+ recommendation: "Replace the launcher with a fixed, reviewable command.",
81
+ location: "target.args",
82
+ cwe: "CWE-78",
83
+ });
84
+ }
85
+ }
86
+ for (const rule of INLINE_EVAL_RULES) {
87
+ if (rule.executables.has(executable) && target.args.some((arg) => rule.flags.test(arg))) {
88
+ findings.push({
89
+ id: "LAUNCH004",
90
+ severity: "high",
91
+ title: "Target executes inline code from the command line",
92
+ evidence: `"${executable}" is invoked with an inline evaluation flag instead of a reviewable script file.`,
93
+ recommendation: "Move the code into a committed, reviewable script and launch that file directly.",
94
+ location: "target.args",
95
+ cwe: "CWE-95",
96
+ });
97
+ break;
98
+ }
99
+ }
100
+ return findings;
101
+ }
102
+ //# sourceMappingURL=launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../../../src/rules/launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAIrC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,MAAM;IACN,UAAU;IACV,KAAK;IACL,SAAS;IACT,MAAM;IACN,UAAU;IACV,YAAY;IACZ,gBAAgB;IAChB,IAAI;IACJ,QAAQ;CACT,CAAC,CAAC;AAEH,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAEtE,MAAM,oBAAoB,GAA8C;IACtE,EAAE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,oBAAoB,EAAE;IACzD,EAAE,OAAO,EAAE,gCAAgC,EAAE,KAAK,EAAE,oBAAoB,EAAE;IAC1E,EAAE,OAAO,EAAE,sCAAsC,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC9E,EAAE,OAAO,EAAE,kDAAkD,EAAE,KAAK,EAAE,cAAc,EAAE;IACtF,EAAE,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,sBAAsB,EAAE;IACjE,EAAE,OAAO,EAAE,gCAAgC,EAAE,KAAK,EAAE,wBAAwB,EAAE;IAC9E,EAAE,OAAO,EAAE,yDAAyD,EAAE,KAAK,EAAE,iBAAiB,EAAE;CACjG,CAAC;AAEF,0EAA0E;AAC1E,sEAAsE;AACtE,MAAM,iBAAiB,GAAuD;IAC5E;QACE,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;QAChF,KAAK,EAAE,kCAAkC;KAC1C;IACD;QACE,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC;QACxE,KAAK,EAAE,MAAM;KACd;IACD;QACE,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QAC9D,KAAK,EAAE,MAAM;KACd;IACD;QACE,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,CAAC,CAAC;QAC1E,KAAK,EAAE,2CAA2C;KACnD;CACF,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,MAAoB;IAChD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9D,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1D,MAAM,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE9C,IAAI,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,WAAW;YACf,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,oDAAoD;YAC3D,QAAQ,EAAE,6BAA6B,UAAU,IAAI;YACrD,cAAc,EAAE,oEAAoE;YACpF,QAAQ,EAAE,gBAAgB;YAC1B,GAAG,EAAE,QAAQ;SACd,CAAC,CAAC;IACL,CAAC;IAED,IAAI,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QACvC,QAAQ,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,WAAW;YACf,QAAQ,EAAE,QAAQ;YAClB,KAAK,EAAE,gDAAgD;YACvD,QAAQ,EAAE,6BAA6B,UAAU,IAAI;YACrD,cAAc,EAAE,6EAA6E;YAC7F,QAAQ,EAAE,gBAAgB;YAC1B,GAAG,EAAE,SAAS;SACf,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,oBAAoB,EAAE,CAAC;QAC7C,IAAI,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;YAC5C,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,WAAW;gBACf,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,8BAA8B;gBACrC,QAAQ,EAAE,+CAA+C,SAAS,CAAC,KAAK,GAAG;gBAC3E,cAAc,EAAE,wDAAwD;gBACxE,QAAQ,EAAE,aAAa;gBACvB,GAAG,EAAE,QAAQ;aACd,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,iBAAiB,EAAE,CAAC;QACrC,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACxF,QAAQ,CAAC,IAAI,CAAC;gBACZ,EAAE,EAAE,WAAW;gBACf,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,mDAAmD;gBAC1D,QAAQ,EAAE,IAAI,UAAU,kFAAkF;gBAC1G,cAAc,EACZ,kFAAkF;gBACpF,QAAQ,EAAE,aAAa;gBACvB,GAAG,EAAE,QAAQ;aACd,CAAC,CAAC;YACH,MAAM;QACR,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Finding } from "../types.js";
2
+ /**
3
+ * Decide whether it is safe for the scanner to fetch a URL taken from an
4
+ * untrusted server response.
5
+ * - "ok": public https (or loopback http when the operator's own target is local).
6
+ * - "ssrf": the host resolves to a private/loopback/link-local address the
7
+ * server should not be able to steer the scanner toward.
8
+ * - "unsafe": unparseable, non-http(s), unresolvable, or plaintext to a
9
+ * non-loopback host.
10
+ * `allowLoopback` is true only when the operator's target is itself local, so a
11
+ * local dev scan may follow local metadata.
12
+ */
13
+ export declare function assessServerUrl(rawUrl: string, allowLoopback: boolean): Promise<"ok" | "ssrf" | "unsafe">;
14
+ /**
15
+ * Audit the OAuth 2.1 authorization posture of a remote MCP endpoint, following
16
+ * the MCP authorization spec (RFC 9728 Protected Resource Metadata + RFC 8414
17
+ * Authorization Server Metadata). Only meaningful for a server that challenges
18
+ * unauthenticated requests; an open server is covered by REMOTE004. Every URL
19
+ * taken from a server response is SSRF-checked before it is fetched.
20
+ */
21
+ export declare function inspectRemoteAuth(rawUrl: string, timeoutMs: number): Promise<Finding[]>;