revxl-devtools 1.0.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 +84 -0
- package/checkout/index.html +195 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +77 -0
- package/dist/codegen/cron-codegen.d.ts +1 -0
- package/dist/codegen/cron-codegen.js +56 -0
- package/dist/codegen/regex-codegen.d.ts +1 -0
- package/dist/codegen/regex-codegen.js +125 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +100 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.js +13 -0
- package/dist/tools/base64.d.ts +1 -0
- package/dist/tools/base64.js +29 -0
- package/dist/tools/batch.d.ts +1 -0
- package/dist/tools/batch.js +56 -0
- package/dist/tools/chmod.d.ts +1 -0
- package/dist/tools/chmod.js +115 -0
- package/dist/tools/cron.d.ts +1 -0
- package/dist/tools/cron.js +311 -0
- package/dist/tools/hash.d.ts +1 -0
- package/dist/tools/hash.js +25 -0
- package/dist/tools/http-status.d.ts +1 -0
- package/dist/tools/http-status.js +59 -0
- package/dist/tools/json-diff.d.ts +1 -0
- package/dist/tools/json-diff.js +131 -0
- package/dist/tools/json-format.d.ts +1 -0
- package/dist/tools/json-format.js +38 -0
- package/dist/tools/json-query.d.ts +1 -0
- package/dist/tools/json-query.js +114 -0
- package/dist/tools/jwt.d.ts +1 -0
- package/dist/tools/jwt.js +177 -0
- package/dist/tools/regex.d.ts +1 -0
- package/dist/tools/regex.js +116 -0
- package/dist/tools/secrets-scan.d.ts +1 -0
- package/dist/tools/secrets-scan.js +173 -0
- package/dist/tools/sql-format.d.ts +1 -0
- package/dist/tools/sql-format.js +157 -0
- package/dist/tools/timestamp.d.ts +1 -0
- package/dist/tools/timestamp.js +72 -0
- package/dist/tools/url-encode.d.ts +1 -0
- package/dist/tools/url-encode.js +26 -0
- package/dist/tools/uuid.d.ts +1 -0
- package/dist/tools/uuid.js +24 -0
- package/dist/tools/yaml-convert.d.ts +1 -0
- package/dist/tools/yaml-convert.js +371 -0
- package/package.json +29 -0
- package/src/auth.ts +99 -0
- package/src/codegen/cron-codegen.ts +66 -0
- package/src/codegen/regex-codegen.ts +132 -0
- package/src/index.ts +134 -0
- package/src/registry.ts +25 -0
- package/src/tools/base64.ts +32 -0
- package/src/tools/batch.ts +69 -0
- package/src/tools/chmod.ts +133 -0
- package/src/tools/cron.ts +365 -0
- package/src/tools/hash.ts +26 -0
- package/src/tools/http-status.ts +63 -0
- package/src/tools/json-diff.ts +153 -0
- package/src/tools/json-format.ts +43 -0
- package/src/tools/json-query.ts +126 -0
- package/src/tools/jwt.ts +193 -0
- package/src/tools/regex.ts +131 -0
- package/src/tools/secrets-scan.ts +212 -0
- package/src/tools/sql-format.ts +178 -0
- package/src/tools/timestamp.ts +74 -0
- package/src/tools/url-encode.ts +29 -0
- package/src/tools/uuid.ts +25 -0
- package/src/tools/yaml-convert.ts +383 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Simple JSON path query — supports dot notation, [N] indexing, * wildcard
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
function queryPath(data: unknown, segments: string[]): unknown[] {
|
|
8
|
+
if (segments.length === 0) return [data];
|
|
9
|
+
|
|
10
|
+
const [head, ...rest] = segments;
|
|
11
|
+
|
|
12
|
+
if (data === null || data === undefined || typeof data !== "object") {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Wildcard: expand all array items or object values
|
|
17
|
+
if (head === "*") {
|
|
18
|
+
const values = Array.isArray(data) ? data : Object.values(data);
|
|
19
|
+
const results: unknown[] = [];
|
|
20
|
+
for (const val of values) {
|
|
21
|
+
results.push(...queryPath(val, rest));
|
|
22
|
+
}
|
|
23
|
+
return results;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Array index: [N]
|
|
27
|
+
const indexMatch = head.match(/^\[(\d+)]$/);
|
|
28
|
+
if (indexMatch) {
|
|
29
|
+
const idx = parseInt(indexMatch[1], 10);
|
|
30
|
+
if (Array.isArray(data) && idx < data.length) {
|
|
31
|
+
return queryPath(data[idx], rest);
|
|
32
|
+
}
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Object key
|
|
37
|
+
const obj = data as Record<string, unknown>;
|
|
38
|
+
if (head in obj) {
|
|
39
|
+
return queryPath(obj[head], rest);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseQuery(query: string): string[] {
|
|
46
|
+
const segments: string[] = [];
|
|
47
|
+
let current = "";
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < query.length; i++) {
|
|
50
|
+
const ch = query[i];
|
|
51
|
+
if (ch === ".") {
|
|
52
|
+
if (current) segments.push(current);
|
|
53
|
+
current = "";
|
|
54
|
+
} else if (ch === "[") {
|
|
55
|
+
if (current) segments.push(current);
|
|
56
|
+
current = "";
|
|
57
|
+
const end = query.indexOf("]", i);
|
|
58
|
+
if (end === -1) throw new Error(`Unmatched '[' at position ${i}`);
|
|
59
|
+
segments.push(query.slice(i, end + 1));
|
|
60
|
+
i = end;
|
|
61
|
+
} else {
|
|
62
|
+
current += ch;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (current) segments.push(current);
|
|
66
|
+
|
|
67
|
+
return segments;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Tool registration
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
registerTool({
|
|
75
|
+
name: "json_query",
|
|
76
|
+
description:
|
|
77
|
+
"Query JSON data with dot-path expressions — supports nested keys, array indices [N], and wildcard * expansion",
|
|
78
|
+
pro: true,
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
json: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "JSON string to query",
|
|
85
|
+
},
|
|
86
|
+
query: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description:
|
|
89
|
+
'Dot-path query like "data.users[0].name" or "items.*.id" — use * for wildcard, [N] for array index',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ["json", "query"],
|
|
93
|
+
},
|
|
94
|
+
handler: async (args) => {
|
|
95
|
+
const jsonStr = args.json as string;
|
|
96
|
+
const query = args.query as string;
|
|
97
|
+
|
|
98
|
+
let data: unknown;
|
|
99
|
+
try {
|
|
100
|
+
data = JSON.parse(jsonStr);
|
|
101
|
+
} catch {
|
|
102
|
+
throw new Error("Invalid JSON input");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const segments = parseQuery(query);
|
|
106
|
+
const results = queryPath(data, segments);
|
|
107
|
+
|
|
108
|
+
if (results.length === 0) {
|
|
109
|
+
return `No results for query: ${query}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sections: string[] = [
|
|
113
|
+
`=== Query: ${query} ===`,
|
|
114
|
+
`${results.length} result${results.length === 1 ? "" : "s"}`,
|
|
115
|
+
"",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
if (results.length === 1) {
|
|
119
|
+
sections.push(JSON.stringify(results[0], null, 2));
|
|
120
|
+
} else {
|
|
121
|
+
sections.push(JSON.stringify(results, null, 2));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return sections.join("\n");
|
|
125
|
+
},
|
|
126
|
+
});
|
package/src/tools/jwt.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { registerTool } from "../registry.js";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Base64url helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
function base64urlDecode(input: string): string {
|
|
9
|
+
let base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
10
|
+
const pad = base64.length % 4;
|
|
11
|
+
if (pad === 2) base64 += "==";
|
|
12
|
+
else if (pad === 3) base64 += "=";
|
|
13
|
+
return Buffer.from(base64, "base64").toString("utf-8");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function base64urlEncode(input: string | Buffer): string {
|
|
17
|
+
const buf = typeof input === "string" ? Buffer.from(input, "utf-8") : input;
|
|
18
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Known JWT claims
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const CLAIM_NAMES: Record<string, string> = {
|
|
26
|
+
iss: "Issuer",
|
|
27
|
+
sub: "Subject",
|
|
28
|
+
aud: "Audience",
|
|
29
|
+
exp: "Expiration Time",
|
|
30
|
+
nbf: "Not Before",
|
|
31
|
+
iat: "Issued At",
|
|
32
|
+
jti: "JWT ID",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function describeClaims(payload: Record<string, unknown>): string[] {
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
for (const [key, label] of Object.entries(CLAIM_NAMES)) {
|
|
38
|
+
if (key in payload) {
|
|
39
|
+
const val = payload[key];
|
|
40
|
+
if (["exp", "nbf", "iat"].includes(key) && typeof val === "number") {
|
|
41
|
+
const date = new Date(val * 1000);
|
|
42
|
+
lines.push(` ${key} (${label}): ${val} — ${date.toISOString()}`);
|
|
43
|
+
} else {
|
|
44
|
+
lines.push(` ${key} (${label}): ${JSON.stringify(val)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function expiryStatus(payload: Record<string, unknown>): string {
|
|
52
|
+
if (typeof payload.exp !== "number") return "No expiration set";
|
|
53
|
+
const now = Math.floor(Date.now() / 1000);
|
|
54
|
+
const diff = payload.exp - now;
|
|
55
|
+
if (diff <= 0) {
|
|
56
|
+
const mins = Math.abs(Math.round(diff / 60));
|
|
57
|
+
return `EXPIRED ${mins} minute${mins === 1 ? "" : "s"} ago`;
|
|
58
|
+
}
|
|
59
|
+
const mins = Math.round(diff / 60);
|
|
60
|
+
return `Valid for ${mins} minute${mins === 1 ? "" : "s"}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// HMAC signing
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function sign(header: string, payload: string, secret: string, algorithm: string): string {
|
|
68
|
+
const algoMap: Record<string, string> = {
|
|
69
|
+
HS256: "sha256",
|
|
70
|
+
HS384: "sha384",
|
|
71
|
+
HS512: "sha512",
|
|
72
|
+
};
|
|
73
|
+
const hashAlgo = algoMap[algorithm];
|
|
74
|
+
if (!hashAlgo) throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
75
|
+
const data = `${header}.${payload}`;
|
|
76
|
+
const hmac = createHmac(hashAlgo, secret).update(data).digest();
|
|
77
|
+
return base64urlEncode(hmac);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Tool registration
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
registerTool({
|
|
85
|
+
name: "jwt",
|
|
86
|
+
description:
|
|
87
|
+
"Decode a JWT to inspect header/payload/expiry, or create a new HMAC-signed JWT",
|
|
88
|
+
pro: true,
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
action: {
|
|
93
|
+
type: "string",
|
|
94
|
+
enum: ["decode", "create"],
|
|
95
|
+
description: "decode: inspect a JWT | create: generate a new JWT",
|
|
96
|
+
},
|
|
97
|
+
token: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "(decode) The JWT string to decode",
|
|
100
|
+
},
|
|
101
|
+
payload: {
|
|
102
|
+
type: "object",
|
|
103
|
+
description: "(create) Claims object for the JWT payload",
|
|
104
|
+
},
|
|
105
|
+
secret: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "(create) HMAC secret for signing",
|
|
108
|
+
},
|
|
109
|
+
algorithm: {
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: ["HS256", "HS384", "HS512"],
|
|
112
|
+
description: "(create) Signing algorithm, default HS256",
|
|
113
|
+
},
|
|
114
|
+
expires_in: {
|
|
115
|
+
type: "number",
|
|
116
|
+
description: "(create) Seconds until expiration",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: ["action"],
|
|
120
|
+
},
|
|
121
|
+
handler: async (args) => {
|
|
122
|
+
const action = args.action as string;
|
|
123
|
+
|
|
124
|
+
if (action === "decode") {
|
|
125
|
+
const token = args.token as string | undefined;
|
|
126
|
+
if (!token) throw new Error("token is required for decode action");
|
|
127
|
+
|
|
128
|
+
const parts = token.split(".");
|
|
129
|
+
if (parts.length !== 3)
|
|
130
|
+
throw new Error("Invalid JWT: expected 3 dot-separated parts");
|
|
131
|
+
|
|
132
|
+
const header = JSON.parse(base64urlDecode(parts[0])) as Record<string, unknown>;
|
|
133
|
+
const payload = JSON.parse(base64urlDecode(parts[1])) as Record<string, unknown>;
|
|
134
|
+
|
|
135
|
+
const lines: string[] = [
|
|
136
|
+
"=== JWT Decoded ===",
|
|
137
|
+
"",
|
|
138
|
+
"--- Header ---",
|
|
139
|
+
JSON.stringify(header, null, 2),
|
|
140
|
+
"",
|
|
141
|
+
"--- Payload ---",
|
|
142
|
+
JSON.stringify(payload, null, 2),
|
|
143
|
+
"",
|
|
144
|
+
"--- Known Claims ---",
|
|
145
|
+
...describeClaims(payload),
|
|
146
|
+
"",
|
|
147
|
+
`--- Expiry: ${expiryStatus(payload)} ---`,
|
|
148
|
+
"",
|
|
149
|
+
"(Signature not verified — provide secret separately if needed)",
|
|
150
|
+
];
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action === "create") {
|
|
155
|
+
const payloadObj = (args.payload as Record<string, unknown>) || {};
|
|
156
|
+
const secret = args.secret as string | undefined;
|
|
157
|
+
if (!secret) throw new Error("secret is required for create action");
|
|
158
|
+
const algorithm = (args.algorithm as string) || "HS256";
|
|
159
|
+
const expiresIn = args.expires_in as number | undefined;
|
|
160
|
+
|
|
161
|
+
const now = Math.floor(Date.now() / 1000);
|
|
162
|
+
const claims: Record<string, unknown> = { ...payloadObj, iat: now };
|
|
163
|
+
if (expiresIn) claims.exp = now + expiresIn;
|
|
164
|
+
|
|
165
|
+
const headerB64 = base64urlEncode(
|
|
166
|
+
JSON.stringify({ alg: algorithm, typ: "JWT" }),
|
|
167
|
+
);
|
|
168
|
+
const payloadB64 = base64urlEncode(JSON.stringify(claims));
|
|
169
|
+
const signature = sign(headerB64, payloadB64, secret, algorithm);
|
|
170
|
+
|
|
171
|
+
const token = `${headerB64}.${payloadB64}.${signature}`;
|
|
172
|
+
|
|
173
|
+
const lines: string[] = [
|
|
174
|
+
"=== JWT Created ===",
|
|
175
|
+
"",
|
|
176
|
+
token,
|
|
177
|
+
"",
|
|
178
|
+
"--- Header ---",
|
|
179
|
+
JSON.stringify({ alg: algorithm, typ: "JWT" }, null, 2),
|
|
180
|
+
"",
|
|
181
|
+
"--- Payload ---",
|
|
182
|
+
JSON.stringify(claims, null, 2),
|
|
183
|
+
"",
|
|
184
|
+
`Algorithm: ${algorithm}`,
|
|
185
|
+
];
|
|
186
|
+
if (expiresIn) lines.push(`Expires in: ${expiresIn} seconds`);
|
|
187
|
+
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw new Error(`Unknown action: ${action}. Use "decode" or "create".`);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
import { generateRegexCode } from "../codegen/regex-codegen.js";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Regex tester + multi-language code generator
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
interface MatchResult {
|
|
9
|
+
match: string;
|
|
10
|
+
index: number;
|
|
11
|
+
groups: Record<string, string> | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerTool({
|
|
15
|
+
name: "regex",
|
|
16
|
+
description:
|
|
17
|
+
"Test a regex pattern against a string (with match details, groups, indices) and generate working code in JS/Python/Go/Rust/Java",
|
|
18
|
+
pro: true,
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
pattern: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Regular expression pattern (without delimiters)",
|
|
25
|
+
},
|
|
26
|
+
test_string: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "String to test against (omit to just validate the pattern)",
|
|
29
|
+
},
|
|
30
|
+
flags: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Regex flags, e.g. 'gi' for global+case-insensitive (default: 'g')",
|
|
33
|
+
},
|
|
34
|
+
generate_code: {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
description: "Generate code snippets in multiple languages (default: false)",
|
|
37
|
+
},
|
|
38
|
+
languages: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: { type: "string" },
|
|
41
|
+
description:
|
|
42
|
+
"Languages for code generation: javascript, python, go, rust, java (default: all)",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["pattern"],
|
|
46
|
+
},
|
|
47
|
+
handler: async (args) => {
|
|
48
|
+
const pattern = args.pattern as string;
|
|
49
|
+
const testString = args.test_string as string | undefined;
|
|
50
|
+
const flags = (args.flags as string) || "g";
|
|
51
|
+
const generateCode = (args.generate_code as boolean) || false;
|
|
52
|
+
const languages = args.languages as string[] | undefined;
|
|
53
|
+
|
|
54
|
+
// Validate the pattern first
|
|
55
|
+
let regex: RegExp;
|
|
56
|
+
try {
|
|
57
|
+
regex = new RegExp(pattern, flags);
|
|
58
|
+
} catch (err: unknown) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
return `Invalid regex pattern: ${msg}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sections: string[] = [
|
|
64
|
+
`=== Regex: /${pattern}/${flags} ===`,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
if (testString !== undefined) {
|
|
68
|
+
const matches: MatchResult[] = [];
|
|
69
|
+
|
|
70
|
+
if (flags.includes("g")) {
|
|
71
|
+
// Global: iterate all matches
|
|
72
|
+
let m: RegExpExecArray | null;
|
|
73
|
+
while ((m = regex.exec(testString)) !== null) {
|
|
74
|
+
matches.push({
|
|
75
|
+
match: m[0],
|
|
76
|
+
index: m.index,
|
|
77
|
+
groups: m.groups ? { ...m.groups } : null,
|
|
78
|
+
});
|
|
79
|
+
// Safety: avoid infinite loop on zero-length matches
|
|
80
|
+
if (m[0].length === 0) regex.lastIndex++;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Non-global: single match with capture groups
|
|
84
|
+
const m = regex.exec(testString);
|
|
85
|
+
if (m) {
|
|
86
|
+
matches.push({
|
|
87
|
+
match: m[0],
|
|
88
|
+
index: m.index,
|
|
89
|
+
groups: m.groups ? { ...m.groups } : null,
|
|
90
|
+
});
|
|
91
|
+
// Show capture groups
|
|
92
|
+
if (m.length > 1) {
|
|
93
|
+
sections.push("");
|
|
94
|
+
sections.push("--- Capture Groups ---");
|
|
95
|
+
for (let i = 1; i < m.length; i++) {
|
|
96
|
+
sections.push(` Group ${i}: ${JSON.stringify(m[i])}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sections.push("");
|
|
103
|
+
if (matches.length === 0) {
|
|
104
|
+
sections.push("No matches found.");
|
|
105
|
+
} else {
|
|
106
|
+
sections.push(`${matches.length} match${matches.length === 1 ? "" : "es"} found:`);
|
|
107
|
+
sections.push("");
|
|
108
|
+
for (const match of matches) {
|
|
109
|
+
sections.push(` [${match.index}] "${match.match}"`);
|
|
110
|
+
if (match.groups) {
|
|
111
|
+
for (const [name, value] of Object.entries(match.groups)) {
|
|
112
|
+
sections.push(` ${name}: "${value}"`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
sections.push("");
|
|
119
|
+
sections.push("Pattern is valid. Provide test_string to see matches.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (generateCode) {
|
|
123
|
+
sections.push("");
|
|
124
|
+
sections.push("=== Code Snippets ===");
|
|
125
|
+
sections.push("");
|
|
126
|
+
sections.push(generateRegexCode(pattern, flags, languages));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return sections.join("\n");
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Secrets scanner — detect leaked credentials in text
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
interface SecretPattern {
|
|
8
|
+
type: string;
|
|
9
|
+
pattern: RegExp;
|
|
10
|
+
severity: "critical" | "high" | "medium";
|
|
11
|
+
recommendation: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PATTERNS: SecretPattern[] = [
|
|
15
|
+
{
|
|
16
|
+
type: "AWS Access Key",
|
|
17
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
18
|
+
severity: "critical",
|
|
19
|
+
recommendation: "Rotate this AWS access key immediately in IAM console",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: "AWS Secret Key",
|
|
23
|
+
pattern: /(?:aws_secret_access_key|secret_access_key|aws_secret)\s*[=:]\s*["']?([A-Za-z0-9/+=]{40})["']?/gi,
|
|
24
|
+
severity: "critical",
|
|
25
|
+
recommendation: "Rotate AWS credentials and revoke the exposed secret key",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: "GitHub Token",
|
|
29
|
+
pattern: /gh[pors]_[A-Za-z0-9_]{36,255}/g,
|
|
30
|
+
severity: "critical",
|
|
31
|
+
recommendation: "Revoke this GitHub token at github.com/settings/tokens",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "Stripe Secret Key",
|
|
35
|
+
pattern: /sk_live_[A-Za-z0-9]{24,}/g,
|
|
36
|
+
severity: "critical",
|
|
37
|
+
recommendation: "Roll this Stripe key in the Dashboard immediately",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "Stripe Publishable Key",
|
|
41
|
+
pattern: /pk_live_[A-Za-z0-9]{24,}/g,
|
|
42
|
+
severity: "high",
|
|
43
|
+
recommendation: "While publishable, review if this key should be public",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "Slack Token",
|
|
47
|
+
pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g,
|
|
48
|
+
severity: "critical",
|
|
49
|
+
recommendation: "Revoke this Slack token in your workspace admin settings",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "Private Key",
|
|
53
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
54
|
+
severity: "critical",
|
|
55
|
+
recommendation: "Remove this private key and generate a new key pair",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "JWT Token",
|
|
59
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
60
|
+
severity: "high",
|
|
61
|
+
recommendation: "Revoke this JWT and rotate the signing secret",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: "Anthropic API Key",
|
|
65
|
+
pattern: /sk-ant-[A-Za-z0-9_-]{20,}/g,
|
|
66
|
+
severity: "critical",
|
|
67
|
+
recommendation: "Rotate this Anthropic API key in your account settings",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: "OpenAI API Key",
|
|
71
|
+
pattern: /sk-[A-Za-z0-9]{20,}/g,
|
|
72
|
+
severity: "critical",
|
|
73
|
+
recommendation: "Rotate this OpenAI API key at platform.openai.com",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "Generic API Key/Token",
|
|
77
|
+
pattern: /(?:api[_-]?key|api[_-]?token|access[_-]?token|auth[_-]?token)\s*[=:]\s*["']?([A-Za-z0-9_\-/.+=]{16,})["']?/gi,
|
|
78
|
+
severity: "medium",
|
|
79
|
+
recommendation: "Review if this API key/token should be in source code",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "Generic Password",
|
|
83
|
+
pattern: /(?:password|passwd|pwd)\s*[=:]\s*["']([^"'\s]{8,})["']/gi,
|
|
84
|
+
severity: "high",
|
|
85
|
+
recommendation: "Move this password to a secrets manager or environment variable",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: "Generic Secret",
|
|
89
|
+
pattern: /(?:secret|secret_key)\s*[=:]\s*["']([^"'\s]{8,})["']/gi,
|
|
90
|
+
severity: "high",
|
|
91
|
+
recommendation: "Move this secret to a secrets manager or environment variable",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: "Connection String",
|
|
95
|
+
pattern: /(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"']{10,}/gi,
|
|
96
|
+
severity: "critical",
|
|
97
|
+
recommendation: "Move this connection string to an environment variable. It may contain credentials.",
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
interface Finding {
|
|
102
|
+
type: string;
|
|
103
|
+
masked: string;
|
|
104
|
+
line: number;
|
|
105
|
+
severity: "critical" | "high" | "medium";
|
|
106
|
+
recommendation: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function maskSecret(secret: string): string {
|
|
110
|
+
if (secret.length <= 12) return secret.slice(0, 4) + "****";
|
|
111
|
+
return secret.slice(0, 8) + "..." + secret.slice(-4);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function scanText(text: string): Finding[] {
|
|
115
|
+
const lines = text.split("\n");
|
|
116
|
+
const findings: Finding[] = [];
|
|
117
|
+
const seen = new Set<string>();
|
|
118
|
+
|
|
119
|
+
for (const patternDef of PATTERNS) {
|
|
120
|
+
// Reset regex lastIndex for global patterns
|
|
121
|
+
patternDef.pattern.lastIndex = 0;
|
|
122
|
+
|
|
123
|
+
let match: RegExpExecArray | null;
|
|
124
|
+
while ((match = patternDef.pattern.exec(text)) !== null) {
|
|
125
|
+
const fullMatch = match[0];
|
|
126
|
+
const secret = match[1] || fullMatch;
|
|
127
|
+
|
|
128
|
+
// Deduplicate
|
|
129
|
+
const key = `${patternDef.type}:${fullMatch}`;
|
|
130
|
+
if (seen.has(key)) continue;
|
|
131
|
+
seen.add(key);
|
|
132
|
+
|
|
133
|
+
// Find line number
|
|
134
|
+
const offset = match.index;
|
|
135
|
+
let lineNum = 1;
|
|
136
|
+
for (let i = 0; i < offset && i < text.length; i++) {
|
|
137
|
+
if (text[i] === "\n") lineNum++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
findings.push({
|
|
141
|
+
type: patternDef.type,
|
|
142
|
+
masked: maskSecret(secret),
|
|
143
|
+
line: lineNum,
|
|
144
|
+
severity: patternDef.severity,
|
|
145
|
+
recommendation: patternDef.recommendation,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Sort by severity then line number
|
|
151
|
+
const severityOrder = { critical: 0, high: 1, medium: 2 };
|
|
152
|
+
findings.sort(
|
|
153
|
+
(a, b) =>
|
|
154
|
+
severityOrder[a.severity] - severityOrder[b.severity] || a.line - b.line,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return findings;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
registerTool({
|
|
161
|
+
name: "secrets_scan",
|
|
162
|
+
description:
|
|
163
|
+
"Scan text for leaked secrets, API keys, tokens, passwords, and connection strings",
|
|
164
|
+
pro: true,
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
text: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Text content to scan for secrets",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
required: ["text"],
|
|
174
|
+
},
|
|
175
|
+
handler: async (args) => {
|
|
176
|
+
const text = args.text as string;
|
|
177
|
+
|
|
178
|
+
if (!text.trim()) throw new Error("Text is empty");
|
|
179
|
+
|
|
180
|
+
const findings = scanText(text);
|
|
181
|
+
|
|
182
|
+
if (findings.length === 0) {
|
|
183
|
+
return "No secrets detected. The text appears clean.";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const critical = findings.filter((f) => f.severity === "critical").length;
|
|
187
|
+
const high = findings.filter((f) => f.severity === "high").length;
|
|
188
|
+
const medium = findings.filter((f) => f.severity === "medium").length;
|
|
189
|
+
|
|
190
|
+
const lines: string[] = [
|
|
191
|
+
`=== Secrets Scan: Found ${findings.length} secret${findings.length === 1 ? "" : "s"}: ${critical} critical, ${high} high, ${medium} medium ===`,
|
|
192
|
+
"",
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
for (const finding of findings) {
|
|
196
|
+
const severityLabel =
|
|
197
|
+
finding.severity === "critical"
|
|
198
|
+
? "CRITICAL"
|
|
199
|
+
: finding.severity === "high"
|
|
200
|
+
? "HIGH"
|
|
201
|
+
: "MEDIUM";
|
|
202
|
+
|
|
203
|
+
lines.push(`[${severityLabel}] ${finding.type}`);
|
|
204
|
+
lines.push(` Line: ${finding.line}`);
|
|
205
|
+
lines.push(` Preview: ${finding.masked}`);
|
|
206
|
+
lines.push(` Action: ${finding.recommendation}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return lines.join("\n").trimEnd();
|
|
211
|
+
},
|
|
212
|
+
});
|