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.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 MCP Security Lab contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # MCP Security Lab
2
+
3
+ [![CI](https://github.com/warlyjr-cloud/mcp-security-lab/actions/workflows/ci.yml/badge.svg)](https://github.com/warlyjr-cloud/mcp-security-lab/actions/workflows/ci.yml)
4
+ [![npm version](https://badge.fury.io/js/mcp-security-lab.svg)](https://badge.fury.io/js/mcp-security-lab)
5
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
+
7
+ Evidence-first security checks for local and remote Model Context Protocol (MCP) servers.
8
+
9
+ > **MCP Security Lab is a DevSecOps DevTool that automatically audits Model Context Protocol (MCP) servers for prompt injections, context window exhaustion, and unsafe tool schemas without requiring an LLM or API keys. It runs natively in CI/CD pipelines (SARIF 2.1.0) and supports Docker Sandboxing for safe active probing.**
10
+
11
+ The MVP audits how a local or remote MCP server is launched and what it advertises during the MCP
12
+ handshake. It detects risky launcher patterns (including inline code execution and encoded payloads),
13
+ missing tool annotations, mixed read/write interfaces, least-privilege violations, overly broad
14
+ schemas, context exhaustion risks, and prompt-injection-like text. Prompt-injection scanning covers
15
+ the full surface a server injects into the model's context — tool descriptions, annotation titles,
16
+ parameter names, enum values, server instructions, and every advertised **prompt** and **resource** —
17
+ and is resilient to Unicode evasion (homoglyphs, zero-width and bidirectional characters).
18
+
19
+ Every finding is mapped to a **CWE** and, where relevant, to the **OWASP Top 10 for LLM Applications**,
20
+ and this taxonomy is emitted in the SARIF output. By default the scanner does not call discovered
21
+ tools and requires no LLM or API key. Reports can be emitted as text, JSON, or SARIF 2.1.0. The full
22
+ rule catalog is documented in [docs/RULES.md](docs/RULES.md); the trust boundaries are described in
23
+ [THREAT_MODEL.md](THREAT_MODEL.md).
24
+
25
+ ## Why this exists
26
+
27
+ Installing a local MCP server executes code with the user's privileges. Protocol compliance
28
+ alone does not show whether a server asks for excessive access or advertises unsafe tools.
29
+ MCP Security Lab produces a small, reviewable report before deeper testing.
30
+
31
+ ## Quick start
32
+
33
+ ```powershell
34
+ npm.cmd install
35
+ npm.cmd run build
36
+ node dist/src/cli.js scan --config examples/insecure-server.json
37
+ node dist/src/cli.js scan --config examples/insecure-server.json --execute
38
+ ```
39
+
40
+ The first scan performs launch-configuration checks only. `--execute` acknowledges that the
41
+ target command will run and enables MCP discovery.
42
+
43
+ ## Configuration
44
+
45
+ ```json
46
+ {
47
+ "target": {
48
+ "command": "node",
49
+ "args": ["dist/fixtures/insecure-server.js"],
50
+ "cwd": "."
51
+ },
52
+ "policy": {
53
+ "timeoutMs": 5000,
54
+ "maxTools": 100
55
+ }
56
+ }
57
+ ```
58
+
59
+ Paths are resolved relative to the configuration file. Environment values are intentionally
60
+ not supported in the MVP; this prevents secrets from entering reports or committed examples.
61
+
62
+ ## Output
63
+
64
+ ```powershell
65
+ node dist/src/cli.js scan --config examples/insecure-server.json --execute --format json
66
+ node dist/src/cli.js scan --config examples/insecure-server.json --execute --format sarif
67
+ node dist/src/cli.js scan --config examples/insecure-server.json --execute --output reports/scan.json
68
+ ```
69
+
70
+ Sensitive values supplied through common arguments such as `--token`, `--api-key`,
71
+ `--password`, and `--secret` are redacted from every report. Avoid credentials in command
72
+ arguments entirely when the target supports a safer authentication mechanism.
73
+
74
+ Exit codes:
75
+
76
+ - `0`: scan completed without high or critical findings
77
+ - `1`: invalid input or runtime failure
78
+ - `2`: scan completed with at least one high or critical finding
79
+
80
+ ## Remote servers
81
+
82
+ To scan a remote MCP server, give the target a `url` instead of a `command`:
83
+
84
+ ```json
85
+ {
86
+ "target": {
87
+ "url": "https://mcp.example.com/mcp",
88
+ "transport": "http"
89
+ },
90
+ "policy": { "timeoutMs": 5000, "maxTools": 100 }
91
+ }
92
+ ```
93
+
94
+ `transport` defaults to `"http"` (the modern **Streamable HTTP** transport); use `"sse"` only for
95
+ legacy servers. Remote targets are checked for plaintext HTTP on non-local hosts (`REMOTE002`),
96
+ credentials embedded in the URL (`REMOTE003`, redacted from the report), and — with `--execute` —
97
+ whether the server accepts unauthenticated connections (`REMOTE004`).
98
+
99
+ With `--execute`, a protected remote server is also audited against the MCP authorization spec
100
+ (OAuth 2.1 + RFC 9728 / RFC 8414): a missing resource-metadata pointer (`AUTH001`), unreachable or
101
+ malformed OAuth metadata (`AUTH002`), an authorization server that does not require PKCE/S256
102
+ (`AUTH003`), and OAuth endpoints served over plaintext HTTP (`AUTH004`). See [docs/RULES.md](docs/RULES.md).
103
+
104
+ ```powershell
105
+ node dist/src/cli.js scan --config examples/remote-server.json --execute
106
+ ```
107
+
108
+ ## Active probing (fuzzing)
109
+
110
+ By default the scanner never invokes a discovered tool. Active probing is opt-in through `--fuzz`,
111
+ which sends malformed and injection-shaped arguments to each tool to see whether the server validates
112
+ input or crashes/hangs. Because this executes tool code, `--fuzz` **requires both `--execute` and
113
+ `--sandbox docker`** so the target is network-isolated; the scanner refuses to fuzz on the host.
114
+
115
+ ```powershell
116
+ node dist/src/cli.js scan --config examples/vulnerable-server.json --execute --sandbox docker --fuzz
117
+ ```
118
+
119
+ `examples/vulnerable-server.json` targets a bundled honeypot (`fixtures/honeypot.ts`) that intentionally
120
+ crashes on malicious input, so this command demonstrates a `FUZZ001` critical finding. A well-behaved
121
+ server that returns a proper MCP error for bad input produces no fuzzing finding.
122
+
123
+ ## GitHub Action
124
+
125
+ The repository includes a composite action that generates SARIF and can upload it to GitHub
126
+ code scanning. The calling workflow must grant `security-events: write`.
127
+
128
+ ```yaml
129
+ name: MCP security
130
+
131
+ on:
132
+ workflow_dispatch:
133
+
134
+ permissions:
135
+ contents: read
136
+ security-events: write
137
+
138
+ jobs:
139
+ scan:
140
+ runs-on: ubuntu-latest
141
+ steps:
142
+ - uses: actions/checkout@v6
143
+ - uses: warlyjr-cloud/mcp-security-lab@v0.2.0
144
+ with:
145
+ config: mcp-security-lab.json
146
+ execute: "true"
147
+ upload-sarif: "true"
148
+ fail-on-findings: "true"
149
+ ```
150
+
151
+ Use `execute: "false"` for launch-configuration checks that must not start the target.
152
+ SARIF upload is available for public repositories and for eligible private repositories with
153
+ GitHub Code Security enabled.
154
+
155
+ `execute: "true"` runs the configured server without OS-level filesystem or network isolation.
156
+ Use it only for reviewed code or inside a disposable runner that contains no sensitive source
157
+ or credentials.
158
+
159
+ ## Current limitations
160
+
161
+ Without `--sandbox docker`, `--execute` provides no OS-level filesystem or network isolation: the
162
+ server runs with the current user's permissions. Use `--sandbox docker` (which runs the target in a
163
+ `--network none` container) or a disposable VM for untrusted software. The Docker adapter is
164
+ POSIX-oriented; Windows paths are translated for bind-mounts but Docker Desktop must be running.
165
+ Remote scanning uses the Streamable HTTP transport by default (SSE is available for legacy servers).
166
+ See [SECURITY.md](SECURITY.md) and [THREAT_MODEL.md](THREAT_MODEL.md).
167
+
168
+ ## Roadmap
169
+
170
+ 1. Windows Sandbox execution adapter (Docker container adapter shipped).
171
+ 2. Explicit, synthetic tool-call scenarios with canary files and blocked egress.
172
+ 3. Public conformance corpus maintained with the MCP community.
173
+
174
+ Shipped: Streamable HTTP transport, remote transport checks, and OAuth authorization-posture checks
175
+ (RFC 9728 / RFC 8414).
package/SECURITY.md ADDED
@@ -0,0 +1,25 @@
1
+ # Security model
2
+
3
+ MCP Security Lab treats a configured MCP server as untrusted code.
4
+
5
+ ## What the MVP protects
6
+
7
+ - Dynamic scanning requires the explicit `--execute` flag.
8
+ - The target receives a small allowlist of inherited environment variables.
9
+ - Target startup and protocol discovery have a hard timeout.
10
+ - The scanner lists tools but never calls them.
11
+ - Findings are deterministic and contain no environment values.
12
+
13
+ ## What the MVP does not protect
14
+
15
+ The current process runner is not an OS-level sandbox. A target process still runs with the
16
+ current user's operating-system permissions and may access the filesystem or network. Run
17
+ unknown servers inside a disposable VM or container.
18
+
19
+ Future releases may add platform adapters for Windows Sandbox, containers, and restricted
20
+ egress. Those adapters must be opt-in and independently verifiable.
21
+
22
+ ## Reporting vulnerabilities
23
+
24
+ Do not include credentials, private server configuration, or personal data in a public issue.
25
+ Provide a minimal synthetic reproduction and identify the affected version.
@@ -0,0 +1,66 @@
1
+ # Threat model
2
+
3
+ This document states what MCP Security Lab treats as trusted, what it treats as hostile, and what
4
+ it does and does not guarantee. It complements [SECURITY.md](SECURITY.md), which covers vulnerability
5
+ reporting.
6
+
7
+ ## Assets we protect
8
+
9
+ - The operator's workstation or CI runner (filesystem, network, credentials, environment).
10
+ - The integrity of the scan report itself (no secret leakage, no attacker-controlled content that
11
+ can crash or mislead the scanner).
12
+
13
+ ## Trust boundaries
14
+
15
+ | Component | Trust level | Rationale |
16
+ | --------------------------------- | ----------- | ------------------------------------------------------ |
17
+ | The scanner code and its config | Trusted | Authored and reviewed by the operator. |
18
+ | The target MCP server binary/args | Untrusted | May be third-party or malicious; running it is a risk. |
19
+ | Everything the server sends | Untrusted | Tool/prompt/resource metadata is attacker-controlled. |
20
+ | The parent process environment | Sensitive | Must not leak into the target or the report. |
21
+
22
+ Everything a target server returns — names, descriptions, schemas, annotations, instructions, prompt
23
+ and resource metadata — is untrusted input and is handled defensively. A target must never be able to
24
+ crash the scanner, exfiltrate the operator's environment, or inject content into the report.
25
+
26
+ ## Adversaries and the mitigations against them
27
+
28
+ - **Malicious launcher (supply chain).** A config that runs a shell, a network installer
29
+ (`npx`/`uvx`), inline interpreter code (`node -e`, `python -c`), or encoded payloads. Detected
30
+ statically without execution (`LAUNCH001`–`LAUNCH004`).
31
+ - **Prompt injection via advertised metadata.** Hidden instructions in tool descriptions, parameter
32
+ names, enum values, server instructions, prompts, or resources — including Unicode-obfuscated
33
+ variants. Detected after NFKC normalization and invisible-character stripping (`TOOL003`,
34
+ `TOOL012`).
35
+ - **Excessive agency / least-privilege violations.** Undeclared destructive tools, missing safety
36
+ hints, read+write capability mixing (`TOOL005`–`TOOL008`, `TOOL011`).
37
+ - **Context-exhaustion / denial of service.** Unbounded read tools, tool flooding, or oversized
38
+ discovery payloads (`TOOL010`, `DISC001`, `DISC002`). The scanner bounds process lifetime with a
39
+ timeout and caps the size of discovery responses so a hostile server cannot exhaust the scanner.
40
+ - **Insecure remote transport.** Plaintext HTTP on non-local endpoints, credentials in the URL, or a
41
+ remote server that accepts anonymous connections (`REMOTE002`–`REMOTE004`).
42
+ - **Weak OAuth authorization posture.** A protected remote server that omits its resource-metadata
43
+ pointer, serves unreachable/malformed OAuth metadata, does not require PKCE, or exposes OAuth
44
+ endpoints over plaintext HTTP (`AUTH001`–`AUTH004`, per RFC 9728 / RFC 8414).
45
+ - **Secret leakage into reports.** Credentials passed as arguments or in URLs are redacted; only an
46
+ allowlist of environment variables is inherited by the target.
47
+ - **Uncontrolled active probing.** `--fuzz` executes tool code, so it requires an OS-level sandbox
48
+ (`--sandbox docker`, `--network none`) and refuses to run on the host.
49
+
50
+ ## Explicit non-goals
51
+
52
+ - **No isolation without `--sandbox docker`.** A target started with `--execute` alone runs with the
53
+ operator's OS permissions. The scanner never claims isolation it does not enforce.
54
+ - **No dynamic tool invocation by default.** Tools are only called when `--fuzz` is explicitly
55
+ requested and a sandbox is active.
56
+ - **Not a substitute for code review.** Static launcher checks and advertised-metadata analysis
57
+ reduce risk; they do not prove a server is safe.
58
+ - **No guarantee against a determined sandbox escape.** Container isolation reduces, but does not
59
+ eliminate, the risk of running untrusted code. Use disposable runners for unknown software.
60
+ - **OAuth discovery follows server-controlled URLs (SSRF).** Auditing a remote server's authorization
61
+ posture (`AUTH00x`) requires fetching the metadata URLs the server advertises. Before contacting any
62
+ such URL the scanner resolves the host and refuses loopback, link-local (incl. `169.254.169.254`),
63
+ and private addresses — reporting the attempt as `AUTH005` — requires https for non-local hosts, uses
64
+ `redirect: "manual"` so a 3xx cannot bypass the check, caps the number of authorization servers
65
+ contacted, and bounds every request with a timeout. Loopback metadata is followed only when the
66
+ operator's own target is local (a dev scan). The scanner never forwards responses anywhere.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { loadConfig } from "./config.js";
5
+ import { reportAsJson, reportAsSarif, reportAsText } from "./reporter.js";
6
+ import { scan } from "./scanner.js";
7
+ function usage() {
8
+ return [
9
+ "Usage:",
10
+ " mcpsl scan --config <path> [--execute] [--sandbox docker|none] [--fuzz]",
11
+ " [--format text|json|sarif] [--output <path>]",
12
+ "",
13
+ "Safety:",
14
+ " Without --execute, only the launch configuration is inspected.",
15
+ " With --execute, the target starts with a filtered environment; its tools are never called.",
16
+ " Use --sandbox docker to run the target inside a disposable Docker container.",
17
+ " Use --fuzz to actively call discovered tools with malicious inputs (DANGEROUS).",
18
+ " --fuzz requires --execute and --sandbox docker so probing stays isolated.",
19
+ ].join("\n");
20
+ }
21
+ function parseArgs(args) {
22
+ if (args[0] !== "scan") {
23
+ throw new Error(usage());
24
+ }
25
+ let configPath;
26
+ let execute = false;
27
+ let fuzz = false;
28
+ let format = "text";
29
+ let outputPath;
30
+ let sandbox = "none";
31
+ for (let index = 1; index < args.length; index += 1) {
32
+ const argument = args[index];
33
+ if (argument === "--execute") {
34
+ execute = true;
35
+ continue;
36
+ }
37
+ if (argument === "--fuzz") {
38
+ fuzz = true;
39
+ continue;
40
+ }
41
+ if (argument === "--config" ||
42
+ argument === "--format" ||
43
+ argument === "--output" ||
44
+ argument === "--sandbox") {
45
+ const value = args[index + 1];
46
+ if (value === undefined) {
47
+ throw new Error(`${argument} requires a value.`);
48
+ }
49
+ index += 1;
50
+ if (argument === "--config") {
51
+ configPath = value;
52
+ }
53
+ else if (argument === "--output") {
54
+ outputPath = value;
55
+ }
56
+ else if (argument === "--sandbox") {
57
+ if (value === "docker" || value === "none") {
58
+ sandbox = value;
59
+ }
60
+ else {
61
+ throw new Error("--sandbox must be docker or none.");
62
+ }
63
+ }
64
+ else if (value === "text" || value === "json" || value === "sarif") {
65
+ format = value;
66
+ }
67
+ else {
68
+ throw new Error("--format must be text, json, or sarif.");
69
+ }
70
+ continue;
71
+ }
72
+ throw new Error(`Unknown argument: ${argument}\n\n${usage()}`);
73
+ }
74
+ if (configPath === undefined) {
75
+ throw new Error(`--config is required.\n\n${usage()}`);
76
+ }
77
+ return {
78
+ configPath,
79
+ execute,
80
+ fuzz,
81
+ format,
82
+ sandbox,
83
+ ...(outputPath === undefined ? {} : { outputPath }),
84
+ };
85
+ }
86
+ async function main() {
87
+ const options = parseArgs(process.argv.slice(2));
88
+ const config = await loadConfig(options.configPath);
89
+ const report = await scan(config, options.execute, options.sandbox, options.fuzz);
90
+ const output = options.format === "json"
91
+ ? reportAsJson(report)
92
+ : options.format === "sarif"
93
+ ? reportAsSarif(report, options.configPath)
94
+ : reportAsText(report);
95
+ if (options.outputPath === undefined) {
96
+ process.stdout.write(output);
97
+ }
98
+ else {
99
+ const absoluteOutputPath = resolve(options.outputPath);
100
+ await mkdir(dirname(absoluteOutputPath), { recursive: true });
101
+ await writeFile(absoluteOutputPath, output, "utf8");
102
+ process.stdout.write(`Report written to ${absoluteOutputPath}\n`);
103
+ }
104
+ if (report.summary.critical > 0 || report.summary.high > 0) {
105
+ process.exitCode = 2;
106
+ }
107
+ }
108
+ main().catch((error) => {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ process.stderr.write(`Error: ${message}\n`);
111
+ process.exitCode = 1;
112
+ });
113
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { ScanConfig } from "./types.js";
2
+ export declare function loadConfig(configPath: string): Promise<ScanConfig>;
@@ -0,0 +1,90 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { RESERVED_ENV_KEYS } from "./environment.js";
4
+ const DEFAULT_TIMEOUT_MS = 5_000;
5
+ const DEFAULT_MAX_TOOLS = 100;
6
+ const MAX_TIMEOUT_MS = 60_000;
7
+ const MAX_TOOLS = 1_000;
8
+ const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
+ function assertRecord(value, label) {
10
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
11
+ throw new Error(`${label} must be a JSON object.`);
12
+ }
13
+ }
14
+ function readPositiveInteger(value, fallback, maximum, label) {
15
+ if (value === undefined) {
16
+ return fallback;
17
+ }
18
+ if (!Number.isInteger(value) || value <= 0 || value > maximum) {
19
+ throw new Error(`${label} must be an integer between 1 and ${maximum}.`);
20
+ }
21
+ return value;
22
+ }
23
+ function readTargetEnvironment(value) {
24
+ assertRecord(value, "target.env");
25
+ const environment = {};
26
+ for (const [key, entry] of Object.entries(value)) {
27
+ if (!ENV_KEY_PATTERN.test(key)) {
28
+ throw new Error(`target.env['${key}'] is not a valid environment variable name.`);
29
+ }
30
+ if (RESERVED_ENV_KEYS.has(key.toUpperCase())) {
31
+ throw new Error(`target.env['${key}'] is reserved by the sanitized environment and cannot be overridden.`);
32
+ }
33
+ if (typeof entry !== "string") {
34
+ throw new Error(`target.env['${key}'] must be a string.`);
35
+ }
36
+ environment[key] = entry;
37
+ }
38
+ return environment;
39
+ }
40
+ export async function loadConfig(configPath) {
41
+ const absoluteConfigPath = resolve(configPath);
42
+ const raw = await readFile(absoluteConfigPath, "utf8");
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(raw);
46
+ }
47
+ catch (error) {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ throw new Error(`Config is not valid JSON: ${message}`, { cause: error });
50
+ }
51
+ assertRecord(parsed, "Config");
52
+ assertRecord(parsed.target, "target");
53
+ const { command, args = [], cwd = ".", env = {}, url, transport = "http" } = parsed.target;
54
+ if (url !== undefined && typeof url !== "string") {
55
+ throw new Error("target.url must be a string if provided.");
56
+ }
57
+ if (transport !== "http" && transport !== "sse") {
58
+ throw new Error('target.transport must be "http" or "sse".');
59
+ }
60
+ if (url === undefined) {
61
+ if (typeof command !== "string" || command.trim() === "") {
62
+ throw new Error("target.command must be a non-empty string when url is not provided.");
63
+ }
64
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
65
+ throw new Error("target.args must be an array of strings.");
66
+ }
67
+ if (typeof cwd !== "string" || cwd.trim() === "") {
68
+ throw new Error("target.cwd must be a non-empty string.");
69
+ }
70
+ }
71
+ const policyValue = parsed.policy ?? {};
72
+ assertRecord(policyValue, "policy");
73
+ return {
74
+ target: {
75
+ ...(url !== undefined
76
+ ? { url: url, transport: transport }
77
+ : {
78
+ command: command,
79
+ args: [...args],
80
+ cwd: resolve(dirname(absoluteConfigPath), cwd),
81
+ }),
82
+ env: readTargetEnvironment(env),
83
+ },
84
+ policy: {
85
+ timeoutMs: readPositiveInteger(policyValue.timeoutMs, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, "policy.timeoutMs"),
86
+ maxTools: readPositiveInteger(policyValue.maxTools, DEFAULT_MAX_TOOLS, MAX_TOOLS, "policy.maxTools"),
87
+ },
88
+ };
89
+ }
90
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare const RESERVED_ENV_KEYS: ReadonlySet<string>;
2
+ export declare function createSanitizedEnvironment(source?: NodeJS.ProcessEnv): Record<string, string>;
3
+ /** Strong, low-false-positive test used to RAISE a credentials-in-URL finding. */
4
+ export declare function isCredentialQueryKey(key: string): boolean;
5
+ /**
6
+ * Remove credentials from a URL before it enters a report: strip any userinfo
7
+ * (`user:pass@`) and redact the value of sensitive query AND fragment parameters
8
+ * (OAuth implicit-flow tokens live in the fragment). Falls back to a coarse regex
9
+ * when the string is not a parseable URL.
10
+ */
11
+ export declare function redactUrl(rawUrl: string): string;
12
+ export declare function redactArguments(args: string[]): string[];