@streamblur/mcp 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/README.md +43 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +116 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/patterns.d.ts +5 -0
- package/dist/src/patterns.js +73 -0
- package/dist/src/patterns.js.map +1 -0
- package/dist/src/redact.d.ts +9 -0
- package/dist/src/redact.js +64 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/test/redact.test.d.ts +1 -0
- package/dist/test/redact.test.js +29 -0
- package/dist/test/redact.test.js.map +1 -0
- package/package.json +24 -0
- package/src/index.ts +135 -0
- package/src/patterns.ts +91 -0
- package/src/redact.ts +79 -0
- package/test/redact.test.ts +33 -0
- package/test/sample-code.ts +4 -0
- package/test/sample-secrets.txt +10 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# streamblur-mcp
|
|
2
|
+
|
|
3
|
+
StreamBlur MCP server that redacts API keys, tokens, credentials, and sensitive secrets from text and files.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 3 MCP tools:
|
|
8
|
+
- `redact_text`: Redact secrets in a string.
|
|
9
|
+
- `redact_file`: Read a file and return redacted content without writing changes.
|
|
10
|
+
- `scan_text`: Return detected secrets with `type`, `start`, and `end` positions.
|
|
11
|
+
- 50+ credential patterns including OpenAI, Anthropic, GitHub, AWS, Stripe, Twilio, Slack, SendGrid, database URLs, bearer tokens, private keys, and generic `.env` assignments.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Run
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
streamblur-mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node dist/index.js
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Development
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run build
|
|
36
|
+
node dist/test/redact.test.js
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick manual redaction check
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node -e "const r = require('./dist/redact'); const text = require('fs').readFileSync('./test/sample-secrets.txt','utf8'); console.log('BEFORE:\n' + text); console.log('\nAFTER:\n' + r.redactText(text));"
|
|
43
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
6
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
8
|
+
const redact_1 = require("./redact");
|
|
9
|
+
const server = new index_js_1.Server({
|
|
10
|
+
name: "streamblur-mcp",
|
|
11
|
+
version: "0.1.0"
|
|
12
|
+
}, {
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
18
|
+
return {
|
|
19
|
+
tools: [
|
|
20
|
+
{
|
|
21
|
+
name: "redact_text",
|
|
22
|
+
description: "Redacts secrets from input text and replaces them with [REDACTED:type]",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
text: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Text to redact"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
required: ["text"],
|
|
32
|
+
additionalProperties: false
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "redact_file",
|
|
37
|
+
description: "Reads a file and returns redacted content without modifying the original file",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
path: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Path to file"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
required: ["path"],
|
|
47
|
+
additionalProperties: false
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "scan_text",
|
|
52
|
+
description: "Scans text and returns a list of detected secrets with type and position",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
text: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Text to scan"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
required: ["text"],
|
|
62
|
+
additionalProperties: false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
69
|
+
const args = request.params.arguments ?? {};
|
|
70
|
+
switch (request.params.name) {
|
|
71
|
+
case "redact_text": {
|
|
72
|
+
const text = args.text;
|
|
73
|
+
if (typeof text !== "string") {
|
|
74
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "'text' must be a string");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: (0, redact_1.redactText)(text) }]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case "redact_file": {
|
|
81
|
+
const path = args.path;
|
|
82
|
+
if (typeof path !== "string") {
|
|
83
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "'path' must be a string");
|
|
84
|
+
}
|
|
85
|
+
const content = (0, node_fs_1.readFileSync)(path, "utf8");
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text", text: (0, redact_1.redactText)(content) }]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
case "scan_text": {
|
|
91
|
+
const text = args.text;
|
|
92
|
+
if (typeof text !== "string") {
|
|
93
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, "'text' must be a string");
|
|
94
|
+
}
|
|
95
|
+
const detections = (0, redact_1.scanText)(text).map((detection) => ({
|
|
96
|
+
type: detection.type,
|
|
97
|
+
start: detection.start,
|
|
98
|
+
end: detection.end
|
|
99
|
+
}));
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: JSON.stringify(detections, null, 2) }]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
async function main() {
|
|
109
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
110
|
+
await server.connect(transport);
|
|
111
|
+
}
|
|
112
|
+
main().catch((error) => {
|
|
113
|
+
console.error("streamblur-mcp failed to start", error);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.credentialPatterns = void 0;
|
|
4
|
+
exports.credentialPatterns = [
|
|
5
|
+
{ type: "anthropic_api_key", regex: /sk-ant-api03-[A-Za-z0-9_-]{20,120}/g },
|
|
6
|
+
{ type: "openai_project_key", regex: /sk-proj-[A-Za-z0-9_-]{20,120}/g },
|
|
7
|
+
{ type: "openai_api_key", regex: /\bsk-[A-Za-z0-9]{20,120}\b/g },
|
|
8
|
+
{ type: "github_pat", regex: /github_pat_[A-Za-z0-9_]{20,255}/g },
|
|
9
|
+
{ type: "github_token_ghp", regex: /\bghp_[A-Za-z0-9]{20,255}\b/g },
|
|
10
|
+
{ type: "github_token_gho", regex: /\bgho_[A-Za-z0-9]{20,255}\b/g },
|
|
11
|
+
{ type: "github_token_ghs", regex: /\bghs_[A-Za-z0-9]{20,255}\b/g },
|
|
12
|
+
{ type: "aws_access_key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
13
|
+
{ type: "aws_secret_key", regex: /\b(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key)\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{40}['\"]?/g },
|
|
14
|
+
{ type: "aws_secret_key_raw", regex: /\b[A-Za-z0-9\/+=]{40}\b/g },
|
|
15
|
+
{ type: "google_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
16
|
+
{ type: "stripe_secret_live", regex: /\bsk_live_[A-Za-z0-9]{16,120}\b/g },
|
|
17
|
+
{ type: "stripe_publishable_live", regex: /\bpk_live_[A-Za-z0-9]{16,120}\b/g },
|
|
18
|
+
{ type: "stripe_secret_test", regex: /\bsk_test_[A-Za-z0-9]{16,120}\b/g },
|
|
19
|
+
{ type: "stripe_restricted_live", regex: /\brk_live_[A-Za-z0-9]{16,120}\b/g },
|
|
20
|
+
{ type: "twilio_sid", regex: /\bAC[a-f0-9]{32}\b/gi },
|
|
21
|
+
{ type: "twilio_api_key", regex: /\bSK[a-f0-9]{32}\b/gi },
|
|
22
|
+
{ type: "twilio_auth_token", regex: /\b(?:TWILIO_AUTH_TOKEN|twilio_auth_token)\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
|
|
23
|
+
{ type: "sendgrid_api_key", regex: /\bSG\.[A-Za-z0-9_-]{20,120}\b/g },
|
|
24
|
+
{ type: "slack_bot_token", regex: /\bxoxb-[0-9A-Za-z-]{20,120}\b/g },
|
|
25
|
+
{ type: "slack_user_token", regex: /\bxoxp-[0-9A-Za-z-]{20,120}\b/g },
|
|
26
|
+
{ type: "slack_session_token", regex: /\bxoxs-[0-9A-Za-z-]{20,120}\b/g },
|
|
27
|
+
{ type: "discord_bot_token", regex: /\b[A-Za-z\d_-]{24}\.[A-Za-z\d_-]{6}\.[A-Za-z\d_-]{27}\b/g },
|
|
28
|
+
{ type: "jwt_token", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
29
|
+
{ type: "postgres_url", regex: /\bpostgres(?:ql)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
30
|
+
{ type: "mysql_url", regex: /\bmysql:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
31
|
+
{ type: "mongodb_url", regex: /\bmongodb(?:\+srv)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
32
|
+
{ type: "redis_url", regex: /\bredis(?:s)?:\/\/[^\s'"`]*:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
33
|
+
{ type: "http_basic_auth_url", regex: /\bhttps?:\/\/[^\s:@\/]+:[^\s@\/]+@[^\s'"`]+\b/g },
|
|
34
|
+
{ type: "rsa_private_key", regex: /-----BEGIN RSA PRIVATE KEY-----[\s\S]+?-----END RSA PRIVATE KEY-----/g },
|
|
35
|
+
{ type: "ec_private_key", regex: /-----BEGIN EC PRIVATE KEY-----[\s\S]+?-----END EC PRIVATE KEY-----/g },
|
|
36
|
+
{ type: "generic_private_key", regex: /-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g },
|
|
37
|
+
{ type: "openssh_private_key", regex: /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]+?-----END OPENSSH PRIVATE KEY-----/g },
|
|
38
|
+
{ type: "dsa_private_key", regex: /-----BEGIN DSA PRIVATE KEY-----[\s\S]+?-----END DSA PRIVATE KEY-----/g },
|
|
39
|
+
{ type: "authorization_bearer", regex: /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._-]{12,}/gi },
|
|
40
|
+
{ type: "firebase_web_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
41
|
+
{ type: "firebase_db_url", regex: /\bhttps:\/\/[a-z0-9-]+\.firebaseio\.com\b/gi },
|
|
42
|
+
{ type: "firebase_service_account", regex: /"type"\s*:\s*"service_account"[\s\S]*?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----\\n"/g },
|
|
43
|
+
{ type: "netlify_token", regex: /\bnfp_[A-Za-z0-9]{20,120}\b/g },
|
|
44
|
+
{ type: "vercel_token", regex: /\b(?:vercel|VERCEL)_[A-Za-z0-9_]*TOKEN\s*[:=]\s*['\"]?[A-Za-z0-9]{20,120}['\"]?/g },
|
|
45
|
+
{ type: "vercel_pat", regex: /\b[A-Za-z0-9]{24}_[A-Za-z0-9]{24}\b/g },
|
|
46
|
+
{ type: "env_api_key", regex: /\bAPI_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
|
|
47
|
+
{ type: "env_openai_api_key", regex: /\bOPENAI_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
48
|
+
{ type: "env_anthropic_api_key", regex: /\bANTHROPIC_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
49
|
+
{ type: "env_github_token", regex: /\bGITHUB_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
50
|
+
{ type: "env_secret_key", regex: /\bSECRET_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
|
|
51
|
+
{ type: "env_access_token", regex: /\bACCESS_TOKEN\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
|
|
52
|
+
{ type: "env_password", regex: /\bPASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
|
|
53
|
+
{ type: "env_db_password", regex: /\bDB_PASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
|
|
54
|
+
{ type: "env_database_url", regex: /\bDATABASE_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
55
|
+
{ type: "env_redis_url", regex: /\bREDIS_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
56
|
+
{ type: "env_sendgrid_api_key", regex: /\bSENDGRID_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
57
|
+
{ type: "env_slack_token", regex: /\bSLACK(?:_BOT)?_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
58
|
+
{ type: "env_stripe_key", regex: /\bSTRIPE(?:_SECRET)?_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
59
|
+
{ type: "env_aws_access_key_id", regex: /\bAWS_ACCESS_KEY_ID\s*[:=]\s*['\"]?[A-Z0-9]{16,32}['\"]?/g },
|
|
60
|
+
{ type: "env_aws_secret_access_key", regex: /\bAWS_SECRET_ACCESS_KEY\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{20,80}['\"]?/g },
|
|
61
|
+
{ type: "env_twilio_auth_token", regex: /\bTWILIO_AUTH_TOKEN\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
|
|
62
|
+
{ type: "env_private_key", regex: /\bPRIVATE_KEY\s*[:=]\s*['\"]?-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----['\"]?/g },
|
|
63
|
+
{ type: "generic_token_assignment", regex: /\b(?:token|secret|passwd|password|api[_-]?key|client[_-]?secret)\b\s*[:=]\s*['\"]?[A-Za-z0-9._\-\/+=]{8,}['\"]?/gi },
|
|
64
|
+
{ type: "npm_token", regex: /\bnpm_[A-Za-z0-9]{20,120}\b/g },
|
|
65
|
+
{ type: "github_fine_grained_token", regex: /\bghu_[A-Za-z0-9]{20,255}\b/g },
|
|
66
|
+
{ type: "azure_storage_key", regex: /\bDefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{40,};EndpointSuffix=core\.windows\.net\b/g },
|
|
67
|
+
{ type: "gcp_service_account_json_key", regex: /"private_key_id"\s*:\s*"[a-f0-9]{30,}"/g },
|
|
68
|
+
{ type: "oauth_refresh_token", regex: /\b1\/\/[A-Za-z0-9_-]{20,}\b/g },
|
|
69
|
+
{ type: "mailgun_api_key", regex: /\bkey-[A-Za-z0-9]{32}\b/g },
|
|
70
|
+
{ type: "shopify_access_token", regex: /\bshpat_[A-Za-z0-9]{20,120}\b/g },
|
|
71
|
+
{ type: "gitlab_token", regex: /\bglpat-[A-Za-z0-9_-]{20,120}\b/g }
|
|
72
|
+
];
|
|
73
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/patterns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type SecretDetection = {
|
|
2
|
+
type: string;
|
|
3
|
+
value: string;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function scanText(text: string): SecretDetection[];
|
|
8
|
+
export declare function redactText(text: string): string;
|
|
9
|
+
export declare function redactFile(filePath: string): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scanText = scanText;
|
|
4
|
+
exports.redactText = redactText;
|
|
5
|
+
exports.redactFile = redactFile;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const patterns_1 = require("./patterns");
|
|
8
|
+
function runPatternScan(text) {
|
|
9
|
+
const hits = [];
|
|
10
|
+
for (const pattern of patterns_1.credentialPatterns) {
|
|
11
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
12
|
+
regex.lastIndex = 0;
|
|
13
|
+
let match;
|
|
14
|
+
while ((match = regex.exec(text)) !== null) {
|
|
15
|
+
const value = match[0];
|
|
16
|
+
const start = match.index;
|
|
17
|
+
const end = start + value.length;
|
|
18
|
+
hits.push({ type: pattern.type, value, start, end });
|
|
19
|
+
if (value.length === 0) {
|
|
20
|
+
regex.lastIndex += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return hits;
|
|
25
|
+
}
|
|
26
|
+
function collapseOverlaps(detections) {
|
|
27
|
+
const sorted = [...detections].sort((a, b) => {
|
|
28
|
+
if (a.start !== b.start) {
|
|
29
|
+
return a.start - b.start;
|
|
30
|
+
}
|
|
31
|
+
return b.end - a.end;
|
|
32
|
+
});
|
|
33
|
+
const accepted = [];
|
|
34
|
+
for (const detection of sorted) {
|
|
35
|
+
const previous = accepted[accepted.length - 1];
|
|
36
|
+
if (!previous || detection.start >= previous.end) {
|
|
37
|
+
accepted.push(detection);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return accepted;
|
|
41
|
+
}
|
|
42
|
+
function scanText(text) {
|
|
43
|
+
return collapseOverlaps(runPatternScan(text));
|
|
44
|
+
}
|
|
45
|
+
function redactText(text) {
|
|
46
|
+
const detections = scanText(text);
|
|
47
|
+
if (detections.length === 0) {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
let result = "";
|
|
51
|
+
let cursor = 0;
|
|
52
|
+
for (const detection of detections) {
|
|
53
|
+
result += text.slice(cursor, detection.start);
|
|
54
|
+
result += `[REDACTED:${detection.type}]`;
|
|
55
|
+
cursor = detection.end;
|
|
56
|
+
}
|
|
57
|
+
result += text.slice(cursor);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function redactFile(filePath) {
|
|
61
|
+
const content = (0, node_fs_1.readFileSync)(filePath, "utf8");
|
|
62
|
+
return redactText(content);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/redact.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_assert_1 = require("node:assert");
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const redact_1 = require("../src/redact");
|
|
7
|
+
function run() {
|
|
8
|
+
const fixturePath = (0, node_path_1.join)(__dirname, "..", "test", "sample-secrets.txt");
|
|
9
|
+
const fixture = (0, node_fs_1.readFileSync)(fixturePath, "utf8");
|
|
10
|
+
const detections = (0, redact_1.scanText)(fixture);
|
|
11
|
+
node_assert_1.strict.ok(detections.length >= 10, "should detect multiple secrets");
|
|
12
|
+
const redacted = (0, redact_1.redactText)(fixture);
|
|
13
|
+
node_assert_1.strict.ok(redacted.includes("[REDACTED:openai_project_key]"));
|
|
14
|
+
node_assert_1.strict.ok(redacted.includes("[REDACTED:anthropic_api_key]"));
|
|
15
|
+
node_assert_1.strict.ok(redacted.includes("[REDACTED:github_token_ghp]"));
|
|
16
|
+
node_assert_1.strict.ok(redacted.includes("[REDACTED:aws_access_key]"));
|
|
17
|
+
node_assert_1.strict.ok(redacted.includes("[REDACTED:sendgrid_api_key]"));
|
|
18
|
+
const fromFile = (0, redact_1.redactFile)(fixturePath);
|
|
19
|
+
node_assert_1.strict.equal(fromFile, redacted, "redactFile should match redactText output");
|
|
20
|
+
const codeFixturePath = (0, node_path_1.join)(__dirname, "..", "test", "sample-code.ts");
|
|
21
|
+
const codeFixture = (0, node_fs_1.readFileSync)(codeFixturePath, "utf8");
|
|
22
|
+
const codeRedacted = (0, redact_1.redactText)(codeFixture);
|
|
23
|
+
node_assert_1.strict.ok(codeRedacted.includes("[REDACTED:openai_project_key]"));
|
|
24
|
+
node_assert_1.strict.ok(codeRedacted.includes("[REDACTED:github_token_ghp]"));
|
|
25
|
+
node_assert_1.strict.ok(codeRedacted.includes("[REDACTED:postgres_url]"));
|
|
26
|
+
console.log("All redact tests passed.");
|
|
27
|
+
}
|
|
28
|
+
run();
|
|
29
|
+
//# sourceMappingURL=redact.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.test.js","sourceRoot":"","sources":["../../test/redact.test.ts"],"names":[],"mappings":";;AAAA,6CAA+C;AAC/C,qCAAuC;AACvC,yCAAiC;AACjC,0CAAiE;AAEjE,SAAS,GAAG;IACV,MAAM,WAAW,GAAG,IAAA,gBAAI,EAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,IAAA,sBAAY,EAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,IAAA,iBAAQ,EAAC,OAAO,CAAC,CAAC;IACrC,oBAAM,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,IAAI,EAAE,EAAE,gCAAgC,CAAC,CAAC;IAErE,MAAM,QAAQ,GAAG,IAAA,mBAAU,EAAC,OAAO,CAAC,CAAC;IACrC,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,+BAA+B,CAAC,CAAC,CAAC;IAC9D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,8BAA8B,CAAC,CAAC,CAAC;IAC7D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAC5D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,2BAA2B,CAAC,CAAC,CAAC;IAC1D,oBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,IAAA,mBAAU,EAAC,WAAW,CAAC,CAAC;IACzC,oBAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,2CAA2C,CAAC,CAAC;IAE9E,MAAM,eAAe,GAAG,IAAA,gBAAI,EAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACxE,MAAM,WAAW,GAAG,IAAA,sBAAY,EAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,YAAY,GAAG,IAAA,mBAAU,EAAC,WAAW,CAAC,CAAC;IAC7C,oBAAM,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,+BAA+B,CAAC,CAAC,CAAC;IAClE,oBAAM,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAChE,oBAAM,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC,CAAC;IAE5D,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;AAC1C,CAAC;AAED,GAAG,EAAE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streamblur/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "StreamBlur MCP server - redact API keys, tokens, and secrets from text and files",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"streamblur-mcp": "./dist/src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"start": "node dist/src/index.js",
|
|
12
|
+
"test": "node dist/test/redact.test.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["mcp", "security", "redact", "api-keys", "secrets", "privacy", "streamblur"],
|
|
15
|
+
"author": "StreamBlur",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"typescript": "^5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
McpError
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { redactText, scanText } from "./redact";
|
|
13
|
+
|
|
14
|
+
const server = new Server(
|
|
15
|
+
{
|
|
16
|
+
name: "streamblur-mcp",
|
|
17
|
+
version: "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
capabilities: {
|
|
21
|
+
tools: {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
27
|
+
return {
|
|
28
|
+
tools: [
|
|
29
|
+
{
|
|
30
|
+
name: "redact_text",
|
|
31
|
+
description: "Redacts secrets from input text and replaces them with [REDACTED:type]",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
text: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Text to redact"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
required: ["text"],
|
|
41
|
+
additionalProperties: false
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "redact_file",
|
|
46
|
+
description: "Reads a file and returns redacted content without modifying the original file",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
path: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Path to file"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
required: ["path"],
|
|
56
|
+
additionalProperties: false
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "scan_text",
|
|
61
|
+
description: "Scans text and returns a list of detected secrets with type and position",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
text: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Text to scan"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
required: ["text"],
|
|
71
|
+
additionalProperties: false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
79
|
+
const args = request.params.arguments ?? {};
|
|
80
|
+
|
|
81
|
+
switch (request.params.name) {
|
|
82
|
+
case "redact_text": {
|
|
83
|
+
const text = args.text;
|
|
84
|
+
if (typeof text !== "string") {
|
|
85
|
+
throw new McpError(ErrorCode.InvalidParams, "'text' must be a string");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: "text", text: redactText(text) }]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "redact_file": {
|
|
94
|
+
const path = args.path;
|
|
95
|
+
if (typeof path !== "string") {
|
|
96
|
+
throw new McpError(ErrorCode.InvalidParams, "'path' must be a string");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const content = readFileSync(path, "utf8");
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: redactText(content) }]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "scan_text": {
|
|
106
|
+
const text = args.text;
|
|
107
|
+
if (typeof text !== "string") {
|
|
108
|
+
throw new McpError(ErrorCode.InvalidParams, "'text' must be a string");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const detections = scanText(text).map((detection) => ({
|
|
112
|
+
type: detection.type,
|
|
113
|
+
start: detection.start,
|
|
114
|
+
end: detection.end
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: JSON.stringify(detections, null, 2) }]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
async function main(): Promise<void> {
|
|
128
|
+
const transport = new StdioServerTransport();
|
|
129
|
+
await server.connect(transport);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main().catch((error) => {
|
|
133
|
+
console.error("streamblur-mcp failed to start", error);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
package/src/patterns.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export type CredentialPattern = {
|
|
2
|
+
type: string;
|
|
3
|
+
regex: RegExp;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const credentialPatterns: CredentialPattern[] = [
|
|
7
|
+
{ type: "anthropic_api_key", regex: /sk-ant-api03-[A-Za-z0-9_-]{20,120}/g },
|
|
8
|
+
{ type: "openai_project_key", regex: /sk-proj-[A-Za-z0-9_-]{20,120}/g },
|
|
9
|
+
{ type: "openai_api_key", regex: /\bsk-[A-Za-z0-9]{20,120}\b/g },
|
|
10
|
+
|
|
11
|
+
{ type: "github_pat", regex: /github_pat_[A-Za-z0-9_]{20,255}/g },
|
|
12
|
+
{ type: "github_token_ghp", regex: /\bghp_[A-Za-z0-9]{20,255}\b/g },
|
|
13
|
+
{ type: "github_token_gho", regex: /\bgho_[A-Za-z0-9]{20,255}\b/g },
|
|
14
|
+
{ type: "github_token_ghs", regex: /\bghs_[A-Za-z0-9]{20,255}\b/g },
|
|
15
|
+
|
|
16
|
+
{ type: "aws_access_key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
17
|
+
{ type: "aws_secret_key", regex: /\b(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key)\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{40}['\"]?/g },
|
|
18
|
+
{ type: "aws_secret_key_raw", regex: /\b[A-Za-z0-9\/+=]{40}\b/g },
|
|
19
|
+
|
|
20
|
+
{ type: "google_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
21
|
+
|
|
22
|
+
{ type: "stripe_secret_live", regex: /\bsk_live_[A-Za-z0-9]{16,120}\b/g },
|
|
23
|
+
{ type: "stripe_publishable_live", regex: /\bpk_live_[A-Za-z0-9]{16,120}\b/g },
|
|
24
|
+
{ type: "stripe_secret_test", regex: /\bsk_test_[A-Za-z0-9]{16,120}\b/g },
|
|
25
|
+
{ type: "stripe_restricted_live", regex: /\brk_live_[A-Za-z0-9]{16,120}\b/g },
|
|
26
|
+
|
|
27
|
+
{ type: "twilio_sid", regex: /\bAC[a-f0-9]{32}\b/gi },
|
|
28
|
+
{ type: "twilio_api_key", regex: /\bSK[a-f0-9]{32}\b/gi },
|
|
29
|
+
{ type: "twilio_auth_token", regex: /\b(?:TWILIO_AUTH_TOKEN|twilio_auth_token)\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
|
|
30
|
+
|
|
31
|
+
{ type: "sendgrid_api_key", regex: /\bSG\.[A-Za-z0-9_-]{20,120}\b/g },
|
|
32
|
+
|
|
33
|
+
{ type: "slack_bot_token", regex: /\bxoxb-[0-9A-Za-z-]{20,120}\b/g },
|
|
34
|
+
{ type: "slack_user_token", regex: /\bxoxp-[0-9A-Za-z-]{20,120}\b/g },
|
|
35
|
+
{ type: "slack_session_token", regex: /\bxoxs-[0-9A-Za-z-]{20,120}\b/g },
|
|
36
|
+
|
|
37
|
+
{ type: "discord_bot_token", regex: /\b[A-Za-z\d_-]{24}\.[A-Za-z\d_-]{6}\.[A-Za-z\d_-]{27}\b/g },
|
|
38
|
+
|
|
39
|
+
{ type: "jwt_token", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
|
|
40
|
+
|
|
41
|
+
{ type: "postgres_url", regex: /\bpostgres(?:ql)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
42
|
+
{ type: "mysql_url", regex: /\bmysql:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
43
|
+
{ type: "mongodb_url", regex: /\bmongodb(?:\+srv)?:\/\/[^\s'"`]+:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
44
|
+
{ type: "redis_url", regex: /\bredis(?:s)?:\/\/[^\s'"`]*:[^\s'"`]+@[^\s'"`]+\b/g },
|
|
45
|
+
|
|
46
|
+
{ type: "http_basic_auth_url", regex: /\bhttps?:\/\/[^\s:@\/]+:[^\s@\/]+@[^\s'"`]+\b/g },
|
|
47
|
+
|
|
48
|
+
{ type: "rsa_private_key", regex: /-----BEGIN RSA PRIVATE KEY-----[\s\S]+?-----END RSA PRIVATE KEY-----/g },
|
|
49
|
+
{ type: "ec_private_key", regex: /-----BEGIN EC PRIVATE KEY-----[\s\S]+?-----END EC PRIVATE KEY-----/g },
|
|
50
|
+
{ type: "generic_private_key", regex: /-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g },
|
|
51
|
+
{ type: "openssh_private_key", regex: /-----BEGIN OPENSSH PRIVATE KEY-----[\s\S]+?-----END OPENSSH PRIVATE KEY-----/g },
|
|
52
|
+
{ type: "dsa_private_key", regex: /-----BEGIN DSA PRIVATE KEY-----[\s\S]+?-----END DSA PRIVATE KEY-----/g },
|
|
53
|
+
|
|
54
|
+
{ type: "authorization_bearer", regex: /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._-]{12,}/gi },
|
|
55
|
+
|
|
56
|
+
{ type: "firebase_web_api_key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
57
|
+
{ type: "firebase_db_url", regex: /\bhttps:\/\/[a-z0-9-]+\.firebaseio\.com\b/gi },
|
|
58
|
+
{ type: "firebase_service_account", regex: /"type"\s*:\s*"service_account"[\s\S]*?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----\\n"/g },
|
|
59
|
+
|
|
60
|
+
{ type: "netlify_token", regex: /\bnfp_[A-Za-z0-9]{20,120}\b/g },
|
|
61
|
+
{ type: "vercel_token", regex: /\b(?:vercel|VERCEL)_[A-Za-z0-9_]*TOKEN\s*[:=]\s*['\"]?[A-Za-z0-9]{20,120}['\"]?/g },
|
|
62
|
+
{ type: "vercel_pat", regex: /\b[A-Za-z0-9]{24}_[A-Za-z0-9]{24}\b/g },
|
|
63
|
+
|
|
64
|
+
{ type: "env_api_key", regex: /\bAPI_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
|
|
65
|
+
{ type: "env_openai_api_key", regex: /\bOPENAI_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
66
|
+
{ type: "env_anthropic_api_key", regex: /\bANTHROPIC_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
67
|
+
{ type: "env_github_token", regex: /\bGITHUB_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
68
|
+
{ type: "env_secret_key", regex: /\bSECRET_KEY\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
|
|
69
|
+
{ type: "env_access_token", regex: /\bACCESS_TOKEN\s*[:=]\s*['\"]?[^\s'\"]{8,}['\"]?/g },
|
|
70
|
+
{ type: "env_password", regex: /\bPASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
|
|
71
|
+
{ type: "env_db_password", regex: /\bDB_PASSWORD\s*[:=]\s*['\"]?[^\s'\"]{6,}['\"]?/g },
|
|
72
|
+
{ type: "env_database_url", regex: /\bDATABASE_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
73
|
+
{ type: "env_redis_url", regex: /\bREDIS_URL\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
74
|
+
{ type: "env_sendgrid_api_key", regex: /\bSENDGRID_API_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
75
|
+
{ type: "env_slack_token", regex: /\bSLACK(?:_BOT)?_TOKEN\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
76
|
+
{ type: "env_stripe_key", regex: /\bSTRIPE(?:_SECRET)?_KEY\s*[:=]\s*['\"]?[^\s'\"]+['\"]?/g },
|
|
77
|
+
{ type: "env_aws_access_key_id", regex: /\bAWS_ACCESS_KEY_ID\s*[:=]\s*['\"]?[A-Z0-9]{16,32}['\"]?/g },
|
|
78
|
+
{ type: "env_aws_secret_access_key", regex: /\bAWS_SECRET_ACCESS_KEY\s*[:=]\s*['\"]?[A-Za-z0-9\/+=]{20,80}['\"]?/g },
|
|
79
|
+
{ type: "env_twilio_auth_token", regex: /\bTWILIO_AUTH_TOKEN\s*[:=]\s*['\"]?[A-Fa-f0-9]{32}['\"]?/g },
|
|
80
|
+
{ type: "env_private_key", regex: /\bPRIVATE_KEY\s*[:=]\s*['\"]?-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----['\"]?/g },
|
|
81
|
+
|
|
82
|
+
{ type: "generic_token_assignment", regex: /\b(?:token|secret|passwd|password|api[_-]?key|client[_-]?secret)\b\s*[:=]\s*['\"]?[A-Za-z0-9._\-\/+=]{8,}['\"]?/gi },
|
|
83
|
+
{ type: "npm_token", regex: /\bnpm_[A-Za-z0-9]{20,120}\b/g },
|
|
84
|
+
{ type: "github_fine_grained_token", regex: /\bghu_[A-Za-z0-9]{20,255}\b/g },
|
|
85
|
+
{ type: "azure_storage_key", regex: /\bDefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{40,};EndpointSuffix=core\.windows\.net\b/g },
|
|
86
|
+
{ type: "gcp_service_account_json_key", regex: /"private_key_id"\s*:\s*"[a-f0-9]{30,}"/g },
|
|
87
|
+
{ type: "oauth_refresh_token", regex: /\b1\/\/[A-Za-z0-9_-]{20,}\b/g },
|
|
88
|
+
{ type: "mailgun_api_key", regex: /\bkey-[A-Za-z0-9]{32}\b/g },
|
|
89
|
+
{ type: "shopify_access_token", regex: /\bshpat_[A-Za-z0-9]{20,120}\b/g },
|
|
90
|
+
{ type: "gitlab_token", regex: /\bglpat-[A-Za-z0-9_-]{20,120}\b/g }
|
|
91
|
+
];
|
package/src/redact.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { credentialPatterns } from "./patterns";
|
|
3
|
+
|
|
4
|
+
export type SecretDetection = {
|
|
5
|
+
type: string;
|
|
6
|
+
value: string;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function runPatternScan(text: string): SecretDetection[] {
|
|
12
|
+
const hits: SecretDetection[] = [];
|
|
13
|
+
|
|
14
|
+
for (const pattern of credentialPatterns) {
|
|
15
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
16
|
+
regex.lastIndex = 0;
|
|
17
|
+
|
|
18
|
+
let match: RegExpExecArray | null;
|
|
19
|
+
while ((match = regex.exec(text)) !== null) {
|
|
20
|
+
const value = match[0];
|
|
21
|
+
const start = match.index;
|
|
22
|
+
const end = start + value.length;
|
|
23
|
+
hits.push({ type: pattern.type, value, start, end });
|
|
24
|
+
|
|
25
|
+
if (value.length === 0) {
|
|
26
|
+
regex.lastIndex += 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return hits;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function collapseOverlaps(detections: SecretDetection[]): SecretDetection[] {
|
|
35
|
+
const sorted = [...detections].sort((a, b) => {
|
|
36
|
+
if (a.start !== b.start) {
|
|
37
|
+
return a.start - b.start;
|
|
38
|
+
}
|
|
39
|
+
return b.end - a.end;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const accepted: SecretDetection[] = [];
|
|
43
|
+
for (const detection of sorted) {
|
|
44
|
+
const previous = accepted[accepted.length - 1];
|
|
45
|
+
if (!previous || detection.start >= previous.end) {
|
|
46
|
+
accepted.push(detection);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return accepted;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function scanText(text: string): SecretDetection[] {
|
|
54
|
+
return collapseOverlaps(runPatternScan(text));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function redactText(text: string): string {
|
|
58
|
+
const detections = scanText(text);
|
|
59
|
+
if (detections.length === 0) {
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let result = "";
|
|
64
|
+
let cursor = 0;
|
|
65
|
+
|
|
66
|
+
for (const detection of detections) {
|
|
67
|
+
result += text.slice(cursor, detection.start);
|
|
68
|
+
result += `[REDACTED:${detection.type}]`;
|
|
69
|
+
cursor = detection.end;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
result += text.slice(cursor);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function redactFile(filePath: string): string {
|
|
77
|
+
const content = readFileSync(filePath, "utf8");
|
|
78
|
+
return redactText(content);
|
|
79
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { redactFile, redactText, scanText } from "../src/redact";
|
|
5
|
+
|
|
6
|
+
function run(): void {
|
|
7
|
+
const fixturePath = join(__dirname, "..", "test", "sample-secrets.txt");
|
|
8
|
+
const fixture = readFileSync(fixturePath, "utf8");
|
|
9
|
+
|
|
10
|
+
const detections = scanText(fixture);
|
|
11
|
+
assert.ok(detections.length >= 10, "should detect multiple secrets");
|
|
12
|
+
|
|
13
|
+
const redacted = redactText(fixture);
|
|
14
|
+
assert.ok(redacted.includes("[REDACTED:openai_project_key]"));
|
|
15
|
+
assert.ok(redacted.includes("[REDACTED:anthropic_api_key]"));
|
|
16
|
+
assert.ok(redacted.includes("[REDACTED:github_token_ghp]"));
|
|
17
|
+
assert.ok(redacted.includes("[REDACTED:aws_access_key]"));
|
|
18
|
+
assert.ok(redacted.includes("[REDACTED:sendgrid_api_key]"));
|
|
19
|
+
|
|
20
|
+
const fromFile = redactFile(fixturePath);
|
|
21
|
+
assert.equal(fromFile, redacted, "redactFile should match redactText output");
|
|
22
|
+
|
|
23
|
+
const codeFixturePath = join(__dirname, "..", "test", "sample-code.ts");
|
|
24
|
+
const codeFixture = readFileSync(codeFixturePath, "utf8");
|
|
25
|
+
const codeRedacted = redactText(codeFixture);
|
|
26
|
+
assert.ok(codeRedacted.includes("[REDACTED:openai_project_key]"));
|
|
27
|
+
assert.ok(codeRedacted.includes("[REDACTED:github_token_ghp]"));
|
|
28
|
+
assert.ok(codeRedacted.includes("[REDACTED:postgres_url]"));
|
|
29
|
+
|
|
30
|
+
console.log("All redact tests passed.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
run();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const openaiKey = 'sk-proj-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQ';
|
|
2
|
+
const githubToken = 'ghp_xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZ';
|
|
3
|
+
headers: { 'Authorization': 'Bearer sk-proj-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQ' }
|
|
4
|
+
const db = new Database('postgres://admin:SuperSecret123!@db.example.com:5432/proddb');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
OPENAI_API_KEY=sk-proj-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQ
|
|
2
|
+
ANTHROPIC_API_KEY=sk-ant-api03-xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQabcdef1234567890abcdef12
|
|
3
|
+
GITHUB_TOKEN=ghp_xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZ
|
|
4
|
+
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
|
5
|
+
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
|
6
|
+
STRIPE_SECRET_KEY=sk_live_xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZ
|
|
7
|
+
DATABASE_URL=postgres://admin:SuperSecret123!@db.example.com:5432/proddb
|
|
8
|
+
REDIS_URL=redis://:myredispassword@redis.example.com:6379
|
|
9
|
+
SLACK_BOT_TOKEN=xoxb-1234567890-1234567890123-xK9mR2vNqL4pW8yT3uC7
|
|
10
|
+
SENDGRID_API_KEY=SG.xK9mR2vNqL4pW8yT3uC7aE1sD6fH5jB0nZwQabcdef12
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "test/redact.test.ts"]
|
|
16
|
+
}
|