@a13xu/lucid 1.4.0 → 1.9.1
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 +118 -14
- package/build/config.d.ts +37 -0
- package/build/config.js +45 -0
- package/build/database.d.ts +36 -1
- package/build/database.js +85 -1
- package/build/guardian/coding-analyzer.d.ts +11 -0
- package/build/guardian/coding-analyzer.js +393 -0
- package/build/guardian/coding-rules.d.ts +1 -0
- package/build/guardian/coding-rules.js +97 -0
- package/build/index.js +164 -3
- package/build/indexer/ast.d.ts +9 -0
- package/build/indexer/ast.js +158 -0
- package/build/indexer/project.js +21 -13
- package/build/memory/experience.d.ts +11 -0
- package/build/memory/experience.js +85 -0
- package/build/retrieval/context.d.ts +29 -0
- package/build/retrieval/context.js +219 -0
- package/build/retrieval/qdrant.d.ts +16 -0
- package/build/retrieval/qdrant.js +135 -0
- package/build/retrieval/tfidf.d.ts +14 -0
- package/build/retrieval/tfidf.js +64 -0
- package/build/security/alerts.d.ts +44 -0
- package/build/security/alerts.js +228 -0
- package/build/security/env.d.ts +24 -0
- package/build/security/env.js +85 -0
- package/build/security/guard.d.ts +35 -0
- package/build/security/guard.js +133 -0
- package/build/security/ratelimit.d.ts +34 -0
- package/build/security/ratelimit.js +105 -0
- package/build/security/smtp.d.ts +26 -0
- package/build/security/smtp.js +125 -0
- package/build/security/ssrf.d.ts +18 -0
- package/build/security/ssrf.js +109 -0
- package/build/security/waf.d.ts +33 -0
- package/build/security/waf.js +174 -0
- package/build/tools/coding-guard.d.ts +24 -0
- package/build/tools/coding-guard.js +82 -0
- package/build/tools/context.d.ts +39 -0
- package/build/tools/context.js +105 -0
- package/build/tools/init.d.ts +41 -1
- package/build/tools/init.js +124 -22
- package/build/tools/remember.d.ts +4 -4
- package/build/tools/reward.d.ts +29 -0
- package/build/tools/reward.js +154 -0
- package/build/tools/sync.js +15 -0
- package/package.json +9 -2
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal SMTP client — Node.js built-ins only (net + tls + crypto).
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Direct TLS (port 465, implicit SSL)
|
|
6
|
+
* - STARTTLS (port 587, explicit upgrade)
|
|
7
|
+
* - AUTH LOGIN
|
|
8
|
+
* - Plain-text message body
|
|
9
|
+
*/
|
|
10
|
+
import * as net from "net";
|
|
11
|
+
import * as tls from "tls";
|
|
12
|
+
function readResponse(socket, timeoutMs) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let buf = "";
|
|
15
|
+
const timer = setTimeout(() => reject(new Error("SMTP read timeout")), timeoutMs);
|
|
16
|
+
const handler = (chunk) => {
|
|
17
|
+
buf += chunk.toString("utf-8");
|
|
18
|
+
// A complete (possibly multi-line) SMTP response ends with "DDD text\r\n"
|
|
19
|
+
// where DDD is followed by a space (not a dash, which denotes continuation)
|
|
20
|
+
if (/\r?\n$/.test(buf)) {
|
|
21
|
+
const rawLines = buf.split(/\r?\n/).filter((l) => l.length > 0);
|
|
22
|
+
const last = rawLines[rawLines.length - 1];
|
|
23
|
+
// Final line: code + space (e.g. "250 OK")
|
|
24
|
+
if (/^\d{3} /.test(last)) {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
socket.removeListener("data", handler);
|
|
27
|
+
const code = parseInt(last.slice(0, 3), 10);
|
|
28
|
+
resolve({ code, lines: rawLines });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
socket.on("data", handler);
|
|
33
|
+
socket.once("error", (e) => { clearTimeout(timer); reject(e); });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function cmd(socket, command, timeout) {
|
|
37
|
+
await new Promise((res, rej) => {
|
|
38
|
+
socket.write(command + "\r\n", "utf-8", (err) => (err ? rej(err) : res()));
|
|
39
|
+
});
|
|
40
|
+
return readResponse(socket, timeout);
|
|
41
|
+
}
|
|
42
|
+
function expect(res, ...codes) {
|
|
43
|
+
if (!codes.includes(res.code)) {
|
|
44
|
+
throw new Error(`SMTP unexpected response ${res.code}: ${res.lines.join(" | ")}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function b64(s) {
|
|
48
|
+
return Buffer.from(s, "utf-8").toString("base64");
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Main send function
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
export async function sendEmail(cfg, msg) {
|
|
54
|
+
const timeout = cfg.timeoutMs ?? 15_000;
|
|
55
|
+
let socket;
|
|
56
|
+
if (cfg.secure) {
|
|
57
|
+
// Direct TLS — connect already encrypted
|
|
58
|
+
socket = await new Promise((resolve, reject) => {
|
|
59
|
+
const s = tls.connect({ host: cfg.host, port: cfg.port, servername: cfg.host });
|
|
60
|
+
s.once("secureConnect", () => resolve(s));
|
|
61
|
+
s.once("error", reject);
|
|
62
|
+
setTimeout(() => reject(new Error("TLS connect timeout")), timeout);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Plain TCP first (STARTTLS later)
|
|
67
|
+
socket = await new Promise((resolve, reject) => {
|
|
68
|
+
const s = net.createConnection({ host: cfg.host, port: cfg.port });
|
|
69
|
+
s.once("connect", () => resolve(s));
|
|
70
|
+
s.once("error", reject);
|
|
71
|
+
setTimeout(() => reject(new Error("TCP connect timeout")), timeout);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
// 1. Server greeting
|
|
76
|
+
expect(await readResponse(socket, timeout), 220);
|
|
77
|
+
// 2. EHLO
|
|
78
|
+
const ehlo = await cmd(socket, `EHLO lucid-security`, timeout);
|
|
79
|
+
expect(ehlo, 250);
|
|
80
|
+
// 3. STARTTLS upgrade (plain TCP only)
|
|
81
|
+
if (!cfg.secure) {
|
|
82
|
+
expect(await cmd(socket, "STARTTLS", timeout), 220);
|
|
83
|
+
// Wrap plain socket in TLS
|
|
84
|
+
socket = await new Promise((resolve, reject) => {
|
|
85
|
+
const upgraded = tls.connect({
|
|
86
|
+
socket: socket,
|
|
87
|
+
host: cfg.host,
|
|
88
|
+
servername: cfg.host,
|
|
89
|
+
});
|
|
90
|
+
upgraded.once("secureConnect", () => resolve(upgraded));
|
|
91
|
+
upgraded.once("error", reject);
|
|
92
|
+
setTimeout(() => reject(new Error("STARTTLS upgrade timeout")), timeout);
|
|
93
|
+
});
|
|
94
|
+
// EHLO again after TLS
|
|
95
|
+
expect(await cmd(socket, `EHLO lucid-security`, timeout), 250);
|
|
96
|
+
}
|
|
97
|
+
// 4. AUTH LOGIN
|
|
98
|
+
expect(await cmd(socket, "AUTH LOGIN", timeout), 334);
|
|
99
|
+
expect(await cmd(socket, b64(cfg.user), timeout), 334);
|
|
100
|
+
expect(await cmd(socket, b64(cfg.pass), timeout), 235);
|
|
101
|
+
// 5. Envelope
|
|
102
|
+
expect(await cmd(socket, `MAIL FROM:<${cfg.from}>`, timeout), 250);
|
|
103
|
+
expect(await cmd(socket, `RCPT TO:<${msg.to}>`, timeout), 250);
|
|
104
|
+
// 6. Message body
|
|
105
|
+
expect(await cmd(socket, "DATA", timeout), 354);
|
|
106
|
+
const date = new Date().toUTCString();
|
|
107
|
+
const body = [
|
|
108
|
+
`Date: ${date}`,
|
|
109
|
+
`From: ${cfg.from}`,
|
|
110
|
+
`To: ${msg.to}`,
|
|
111
|
+
`Subject: ${msg.subject}`,
|
|
112
|
+
`MIME-Version: 1.0`,
|
|
113
|
+
`Content-Type: text/plain; charset=UTF-8`,
|
|
114
|
+
``,
|
|
115
|
+
msg.body,
|
|
116
|
+
`.`,
|
|
117
|
+
].join("\r\n");
|
|
118
|
+
expect(await cmd(socket, body, timeout), 250);
|
|
119
|
+
// 7. Quit
|
|
120
|
+
await cmd(socket, "QUIT", timeout);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
socket.destroy();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF (Server-Side Request Forgery) prevention.
|
|
3
|
+
*
|
|
4
|
+
* Validates URLs before any outbound fetch call to ensure they point to
|
|
5
|
+
* legitimate external services and not to internal network resources.
|
|
6
|
+
*/
|
|
7
|
+
/** Register a trusted host (called once at startup with Qdrant URL etc.) */
|
|
8
|
+
export declare function allowHost(urlOrHost: string): void;
|
|
9
|
+
export declare function resetAllowedHosts(): void;
|
|
10
|
+
export interface UrlCheckResult {
|
|
11
|
+
allowed: boolean;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function validateUrl(rawUrl: string): UrlCheckResult;
|
|
15
|
+
/** Throw if URL is not allowed. Use before every outbound fetch. */
|
|
16
|
+
export declare function assertSafeUrl(url: string): void;
|
|
17
|
+
/** Resolve and return timeout-wrapped fetch (default 10s). */
|
|
18
|
+
export declare function safeFetch(url: string, init?: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF (Server-Side Request Forgery) prevention.
|
|
3
|
+
*
|
|
4
|
+
* Validates URLs before any outbound fetch call to ensure they point to
|
|
5
|
+
* legitimate external services and not to internal network resources.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Private/reserved IP ranges to block
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// IPv4 ranges that should never be reachable from outside
|
|
11
|
+
const BLOCKED_IPV4_PATTERNS = [
|
|
12
|
+
/^127\./, // loopback
|
|
13
|
+
/^10\./, // RFC-1918 class A
|
|
14
|
+
/^172\.(1[6-9]|2\d|3[01])\./, // RFC-1918 class B
|
|
15
|
+
/^192\.168\./, // RFC-1918 class C
|
|
16
|
+
/^169\.254\./, // link-local / AWS metadata
|
|
17
|
+
/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./, // CGNAT (RFC-6598)
|
|
18
|
+
/^0\./, // "this" network
|
|
19
|
+
/^255\./, // broadcast
|
|
20
|
+
/^::1$/, // IPv6 loopback
|
|
21
|
+
/^fc00:/i, // IPv6 unique local
|
|
22
|
+
/^fe80:/i, // IPv6 link-local
|
|
23
|
+
];
|
|
24
|
+
// Hostnames that are always blocked
|
|
25
|
+
const BLOCKED_HOSTS = new Set([
|
|
26
|
+
"localhost",
|
|
27
|
+
"metadata.google.internal",
|
|
28
|
+
"metadata",
|
|
29
|
+
]);
|
|
30
|
+
// Cloud metadata endpoints
|
|
31
|
+
const BLOCKED_PATHS_PREFIX = [
|
|
32
|
+
"/latest/meta-data", // AWS EC2 metadata
|
|
33
|
+
"/computeMetadata", // GCP metadata
|
|
34
|
+
"/metadata/instance", // Azure IMDS
|
|
35
|
+
"/odata/", // Azure (generic)
|
|
36
|
+
];
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Allowlist (set from env/config at startup)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
let _allowedHosts = new Set();
|
|
41
|
+
/** Register a trusted host (called once at startup with Qdrant URL etc.) */
|
|
42
|
+
export function allowHost(urlOrHost) {
|
|
43
|
+
try {
|
|
44
|
+
const host = new URL(urlOrHost).hostname.toLowerCase();
|
|
45
|
+
_allowedHosts.add(host);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
_allowedHosts.add(urlOrHost.toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function resetAllowedHosts() {
|
|
52
|
+
_allowedHosts = new Set();
|
|
53
|
+
}
|
|
54
|
+
export function validateUrl(rawUrl) {
|
|
55
|
+
// 1. Must be parseable
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = new URL(rawUrl);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { allowed: false, reason: "Malformed URL" };
|
|
62
|
+
}
|
|
63
|
+
// 2. Only http/https allowed
|
|
64
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
65
|
+
return { allowed: false, reason: `Protocol not allowed: ${parsed.protocol}` };
|
|
66
|
+
}
|
|
67
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
68
|
+
const path = parsed.pathname;
|
|
69
|
+
// 3. Check blocked hostnames
|
|
70
|
+
if (BLOCKED_HOSTS.has(hostname)) {
|
|
71
|
+
return { allowed: false, reason: `Blocked hostname: ${hostname}` };
|
|
72
|
+
}
|
|
73
|
+
// 4. Check blocked IPv4 patterns
|
|
74
|
+
for (const pattern of BLOCKED_IPV4_PATTERNS) {
|
|
75
|
+
if (pattern.test(hostname)) {
|
|
76
|
+
return { allowed: false, reason: `Blocked IP range: ${hostname}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 5. Check cloud metadata paths
|
|
80
|
+
for (const prefix of BLOCKED_PATHS_PREFIX) {
|
|
81
|
+
if (path.startsWith(prefix)) {
|
|
82
|
+
return { allowed: false, reason: `Blocked metadata path: ${path}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 6. If allowlist is populated, host must be in it
|
|
86
|
+
if (_allowedHosts.size > 0 && !_allowedHosts.has(hostname)) {
|
|
87
|
+
return { allowed: false, reason: `Host not in allowlist: ${hostname}` };
|
|
88
|
+
}
|
|
89
|
+
return { allowed: true };
|
|
90
|
+
}
|
|
91
|
+
/** Throw if URL is not allowed. Use before every outbound fetch. */
|
|
92
|
+
export function assertSafeUrl(url) {
|
|
93
|
+
const result = validateUrl(url);
|
|
94
|
+
if (!result.allowed) {
|
|
95
|
+
throw new Error(`SSRF guard blocked request: ${result.reason}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Resolve and return timeout-wrapped fetch (default 10s). */
|
|
99
|
+
export async function safeFetch(url, init = {}, timeoutMs = 10_000) {
|
|
100
|
+
assertSafeUrl(url);
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
|
+
try {
|
|
104
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAF (Web Application Firewall) rules for MCP tool inputs.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Path traversal & directory escape
|
|
6
|
+
* - Null-byte & CRLF injection
|
|
7
|
+
* - ReDoS (catastrophic backtracking) detection
|
|
8
|
+
* - Input size limits (DoS prevention)
|
|
9
|
+
* - Suspicious injection patterns (SQLi, command injection)
|
|
10
|
+
* - Sensitive data leakage in outputs
|
|
11
|
+
*/
|
|
12
|
+
export type Severity = "low" | "medium" | "high" | "critical";
|
|
13
|
+
export interface WafViolation {
|
|
14
|
+
rule: string;
|
|
15
|
+
severity: Severity;
|
|
16
|
+
detail: string;
|
|
17
|
+
}
|
|
18
|
+
export interface WafResult {
|
|
19
|
+
blocked: boolean;
|
|
20
|
+
violations: WafViolation[];
|
|
21
|
+
}
|
|
22
|
+
export declare function checkSize(field: string, value: string): WafResult;
|
|
23
|
+
export declare function checkInjection(value: string): WafResult;
|
|
24
|
+
export declare function checkPath(inputPath: string, allowedRoot?: string): WafResult;
|
|
25
|
+
export declare function checkReDoS(pattern: string): WafResult;
|
|
26
|
+
export declare function checkInjectionPatterns(value: string): WafResult;
|
|
27
|
+
/** Check if a text blob about to be returned contains secrets. */
|
|
28
|
+
export declare function checkOutputLeakage(text: string): WafViolation[];
|
|
29
|
+
export declare function checkStringField(field: string, value: string, opts?: {
|
|
30
|
+
isPath?: boolean;
|
|
31
|
+
isRegex?: boolean;
|
|
32
|
+
allowedRoot?: string;
|
|
33
|
+
}): WafResult;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAF (Web Application Firewall) rules for MCP tool inputs.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Path traversal & directory escape
|
|
6
|
+
* - Null-byte & CRLF injection
|
|
7
|
+
* - ReDoS (catastrophic backtracking) detection
|
|
8
|
+
* - Input size limits (DoS prevention)
|
|
9
|
+
* - Suspicious injection patterns (SQLi, command injection)
|
|
10
|
+
* - Sensitive data leakage in outputs
|
|
11
|
+
*/
|
|
12
|
+
import { resolve, normalize } from "path";
|
|
13
|
+
const PASS = { blocked: false, violations: [] };
|
|
14
|
+
function block(rule, severity, detail) {
|
|
15
|
+
return { blocked: true, violations: [{ rule, severity, detail }] };
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Input size limits
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const MAX_LENGTHS = {
|
|
21
|
+
query: 2_000,
|
|
22
|
+
pattern: 500,
|
|
23
|
+
path: 1_000,
|
|
24
|
+
code: 200_000, // check_drift: allow large snippets
|
|
25
|
+
observation: 10_000,
|
|
26
|
+
entity: 500,
|
|
27
|
+
command: 2_000,
|
|
28
|
+
default: 50_000,
|
|
29
|
+
};
|
|
30
|
+
export function checkSize(field, value) {
|
|
31
|
+
const limit = MAX_LENGTHS[field] ?? MAX_LENGTHS["default"];
|
|
32
|
+
if (value.length > limit) {
|
|
33
|
+
return block("SIZE_LIMIT", "medium", `Field "${field}" exceeds max length (${value.length} > ${limit})`);
|
|
34
|
+
}
|
|
35
|
+
return PASS;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Null-byte & CRLF injection
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
export function checkInjection(value) {
|
|
41
|
+
if (value.includes("\0")) {
|
|
42
|
+
return block("NULL_BYTE", "high", "Null byte detected in input");
|
|
43
|
+
}
|
|
44
|
+
if (/\r\n|\r/.test(value) && value.includes("HTTP/")) {
|
|
45
|
+
return block("CRLF_INJECTION", "high", "CRLF injection pattern detected");
|
|
46
|
+
}
|
|
47
|
+
return PASS;
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Path traversal
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Normalize and verify path stays within an allowed root
|
|
53
|
+
export function checkPath(inputPath, allowedRoot) {
|
|
54
|
+
const injection = checkInjection(inputPath);
|
|
55
|
+
if (injection.blocked)
|
|
56
|
+
return injection;
|
|
57
|
+
// Detect traversal patterns before resolution
|
|
58
|
+
if (/\.\.[/\\]/.test(inputPath) || inputPath.includes("..%2F") || inputPath.includes("..%5C")) {
|
|
59
|
+
return block("PATH_TRAVERSAL", "critical", "Directory traversal sequence detected");
|
|
60
|
+
}
|
|
61
|
+
// Detect absolute paths to sensitive OS locations
|
|
62
|
+
const normalized = normalize(inputPath).replace(/\\/g, "/");
|
|
63
|
+
const sensitiveRoots = ["/etc/", "/proc/", "/sys/", "/dev/", "/root/",
|
|
64
|
+
"C:/Windows/", "C:\\Windows\\"];
|
|
65
|
+
for (const root of sensitiveRoots) {
|
|
66
|
+
if (normalized.toLowerCase().startsWith(root.toLowerCase())) {
|
|
67
|
+
return block("SENSITIVE_PATH", "critical", `Access to sensitive system path blocked`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// If an allowed root is provided, verify path stays within it
|
|
71
|
+
if (allowedRoot) {
|
|
72
|
+
const resolvedPath = resolve(inputPath);
|
|
73
|
+
const resolvedRoot = resolve(allowedRoot);
|
|
74
|
+
if (!resolvedPath.startsWith(resolvedRoot)) {
|
|
75
|
+
return block("PATH_ESCAPE", "critical", `Path escapes allowed root (${resolvedRoot})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return PASS;
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// ReDoS detection (for regex inputs in grep_code)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Patterns that indicate catastrophic backtracking risk
|
|
84
|
+
const REDOS_PATTERNS = [
|
|
85
|
+
{ re: /\([^)]*[+*][^)]*\)[+*{]/, name: "nested-quantifier" }, // (a+)+
|
|
86
|
+
{ re: /\([^)]*\|[^)]*\)[+*{]/, name: "alternation-quantifier" }, // (a|b)+
|
|
87
|
+
{ re: /[+*]\s*[+*]/, name: "consecutive-quantifiers" }, // .* .*
|
|
88
|
+
{ re: /\{[0-9]{3,},/, name: "large-repetition" }, // {1000,}
|
|
89
|
+
{ re: /(\(\?[^)]*\)){3,}/, name: "excessive-lookahead" }, // (?=...)(?=...)(?=...)
|
|
90
|
+
];
|
|
91
|
+
export function checkReDoS(pattern) {
|
|
92
|
+
for (const { re, name } of REDOS_PATTERNS) {
|
|
93
|
+
if (re.test(pattern)) {
|
|
94
|
+
return block("REDOS", "high", `Regex pattern may cause catastrophic backtracking (rule: ${name})`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Also attempt to measure compile time as a secondary check
|
|
98
|
+
try {
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
new RegExp(pattern);
|
|
101
|
+
if (Date.now() - start > 100) {
|
|
102
|
+
return block("REDOS", "medium", "Regex compilation was unexpectedly slow");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return block("INVALID_REGEX", "low", `Invalid regex pattern: ${e.message}`);
|
|
107
|
+
}
|
|
108
|
+
return PASS;
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Command/SQL injection patterns (defensive — prepared stmts are primary guard)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
const INJECTION_PATTERNS = [
|
|
114
|
+
{ re: /;\s*(DROP|DELETE|TRUNCATE|ALTER)\s+/i, rule: "SQL_INJECTION", severity: "critical" },
|
|
115
|
+
{ re: /UNION\s+(?:ALL\s+)?SELECT/i, rule: "SQL_UNION", severity: "high" },
|
|
116
|
+
{ re: /'\s*OR\s+'1'\s*=\s*'1/i, rule: "SQL_TAUTOLOGY", severity: "high" },
|
|
117
|
+
{ re: /`[^`]*`|;\s*[a-z]+\s+\/|&&|\|\|/, rule: "CMD_INJECTION", severity: "high" },
|
|
118
|
+
{ re: /\$\([^)]+\)|`[^`]+`/, rule: "SHELL_SUBSTITUTION", severity: "high" },
|
|
119
|
+
];
|
|
120
|
+
export function checkInjectionPatterns(value) {
|
|
121
|
+
for (const { re, rule, severity } of INJECTION_PATTERNS) {
|
|
122
|
+
if (re.test(value)) {
|
|
123
|
+
return block(rule, severity, `Injection pattern detected (rule: ${rule})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return PASS;
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Sensitive data leak detection (for outbound content)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
const SENSITIVE_LEAK_PATTERNS = [
|
|
132
|
+
/sk-[a-zA-Z0-9]{20,}/, // OpenAI API key
|
|
133
|
+
/AKIA[0-9A-Z]{16}/, // AWS access key
|
|
134
|
+
/(?:eyJ)[a-zA-Z0-9_-]{10,}\.(?:eyJ)[a-zA-Z0-9_-]{10,}/, // JWT
|
|
135
|
+
/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, // PEM key
|
|
136
|
+
/(?:password|passwd|secret|token)\s*[=:]\s*['"]?[^\s'"]{8,}/i, // generic
|
|
137
|
+
];
|
|
138
|
+
/** Check if a text blob about to be returned contains secrets. */
|
|
139
|
+
export function checkOutputLeakage(text) {
|
|
140
|
+
const violations = [];
|
|
141
|
+
for (const pattern of SENSITIVE_LEAK_PATTERNS) {
|
|
142
|
+
if (pattern.test(text)) {
|
|
143
|
+
violations.push({
|
|
144
|
+
rule: "DATA_LEAKAGE",
|
|
145
|
+
severity: "critical",
|
|
146
|
+
detail: "Output may contain sensitive credentials or keys",
|
|
147
|
+
});
|
|
148
|
+
break; // one warning is enough
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return violations;
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Composite check for common string fields
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
export function checkStringField(field, value, opts) {
|
|
157
|
+
const size = checkSize(field, value);
|
|
158
|
+
if (size.blocked)
|
|
159
|
+
return size;
|
|
160
|
+
const inj = checkInjection(value);
|
|
161
|
+
if (inj.blocked)
|
|
162
|
+
return inj;
|
|
163
|
+
if (opts?.isPath) {
|
|
164
|
+
const path = checkPath(value, opts.allowedRoot);
|
|
165
|
+
if (path.blocked)
|
|
166
|
+
return path;
|
|
167
|
+
}
|
|
168
|
+
if (opts?.isRegex) {
|
|
169
|
+
const redos = checkReDoS(value);
|
|
170
|
+
if (redos.blocked)
|
|
171
|
+
return redos;
|
|
172
|
+
}
|
|
173
|
+
return PASS;
|
|
174
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CheckCodeQualitySchema: z.ZodEffects<z.ZodObject<{
|
|
3
|
+
path: z.ZodOptional<z.ZodString>;
|
|
4
|
+
code: z.ZodOptional<z.ZodString>;
|
|
5
|
+
language: z.ZodOptional<z.ZodEnum<["python", "javascript", "typescript", "vue", "generic"]>>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
path?: string | undefined;
|
|
8
|
+
code?: string | undefined;
|
|
9
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
path?: string | undefined;
|
|
12
|
+
code?: string | undefined;
|
|
13
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
14
|
+
}>, {
|
|
15
|
+
path?: string | undefined;
|
|
16
|
+
code?: string | undefined;
|
|
17
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
path?: string | undefined;
|
|
20
|
+
code?: string | undefined;
|
|
21
|
+
language?: "python" | "javascript" | "typescript" | "generic" | "vue" | undefined;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function handleGetCodingRules(): string;
|
|
24
|
+
export declare function handleCheckCodeQuality(args: z.infer<typeof CheckCodeQualitySchema>): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { extname } from "path";
|
|
4
|
+
import { analyzeCodeQuality, formatQualityReport } from "../guardian/coding-analyzer.js";
|
|
5
|
+
import { CODING_RULES } from "../guardian/coding-rules.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Schemas
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
export const CheckCodeQualitySchema = z
|
|
10
|
+
.object({
|
|
11
|
+
path: z.string().optional().describe("Absolute or relative path to the file to analyze."),
|
|
12
|
+
code: z.string().optional().describe("Code snippet to analyze inline."),
|
|
13
|
+
language: z
|
|
14
|
+
.enum(["python", "javascript", "typescript", "vue", "generic"])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Language hint. Auto-detected from file extension if path is provided."),
|
|
17
|
+
})
|
|
18
|
+
.refine((args) => args.path !== undefined || args.code !== undefined, {
|
|
19
|
+
message: "Provide either path (file to read) or code (inline snippet).",
|
|
20
|
+
});
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Extension → language map for synthetic snippet paths
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const LANG_EXT = {
|
|
25
|
+
python: ".py",
|
|
26
|
+
javascript: ".js",
|
|
27
|
+
typescript: ".ts",
|
|
28
|
+
vue: ".vue",
|
|
29
|
+
generic: ".txt",
|
|
30
|
+
};
|
|
31
|
+
// Languages where frontend rules activate (by extension)
|
|
32
|
+
const FRONTEND_EXTS = new Set([".tsx", ".jsx", ".vue"]);
|
|
33
|
+
function syntheticPath(lang) {
|
|
34
|
+
const ext = LANG_EXT[lang] ?? ".txt";
|
|
35
|
+
return `<snippet>${ext}`;
|
|
36
|
+
}
|
|
37
|
+
function inferLang(filepath) {
|
|
38
|
+
const ext = extname(filepath).toLowerCase();
|
|
39
|
+
const map = {
|
|
40
|
+
".py": "python",
|
|
41
|
+
".js": "javascript",
|
|
42
|
+
".jsx": "javascript",
|
|
43
|
+
".ts": "typescript",
|
|
44
|
+
".tsx": "typescript",
|
|
45
|
+
".vue": "vue",
|
|
46
|
+
};
|
|
47
|
+
return map[ext];
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Handlers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export function handleGetCodingRules() {
|
|
53
|
+
return CODING_RULES;
|
|
54
|
+
}
|
|
55
|
+
export function handleCheckCodeQuality(args) {
|
|
56
|
+
let source;
|
|
57
|
+
let filepath;
|
|
58
|
+
let lang;
|
|
59
|
+
if (args.path !== undefined) {
|
|
60
|
+
try {
|
|
61
|
+
source = readFileSync(args.path, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return `❌ Cannot read file "${args.path}": ${err instanceof Error ? err.message : String(err)}`;
|
|
65
|
+
}
|
|
66
|
+
filepath = args.path;
|
|
67
|
+
// Explicit language overrides, otherwise infer from extension
|
|
68
|
+
lang = args.language ?? inferLang(args.path);
|
|
69
|
+
// Warn if extension is frontend but language was explicitly set to non-frontend
|
|
70
|
+
const ext = extname(args.path).toLowerCase();
|
|
71
|
+
if (FRONTEND_EXTS.has(ext) && lang && !["typescript", "javascript", "vue"].includes(lang)) {
|
|
72
|
+
lang = inferLang(args.path) ?? lang;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
source = args.code;
|
|
77
|
+
lang = args.language ?? "generic";
|
|
78
|
+
filepath = syntheticPath(lang);
|
|
79
|
+
}
|
|
80
|
+
const issues = analyzeCodeQuality(filepath, source, lang);
|
|
81
|
+
return formatQualityReport(filepath, issues);
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Statements } from "../database.js";
|
|
3
|
+
export declare const GetContextSchema: z.ZodObject<{
|
|
4
|
+
query: z.ZodString;
|
|
5
|
+
maxTokens: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
dirs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
recentOnly: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
recentHours: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
skeletonOnly: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
+
topK: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
query: string;
|
|
13
|
+
maxTokens?: number | undefined;
|
|
14
|
+
dirs?: string[] | undefined;
|
|
15
|
+
recentOnly?: boolean | undefined;
|
|
16
|
+
recentHours?: number | undefined;
|
|
17
|
+
skeletonOnly?: boolean | undefined;
|
|
18
|
+
topK?: number | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
query: string;
|
|
21
|
+
maxTokens?: number | undefined;
|
|
22
|
+
dirs?: string[] | undefined;
|
|
23
|
+
recentOnly?: boolean | undefined;
|
|
24
|
+
recentHours?: number | undefined;
|
|
25
|
+
skeletonOnly?: boolean | undefined;
|
|
26
|
+
topK?: number | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function handleGetContext(stmts: Statements, args: z.infer<typeof GetContextSchema>): Promise<string>;
|
|
29
|
+
export declare const GetRecentSchema: z.ZodObject<{
|
|
30
|
+
hours: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
withDiffs: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
hours?: number | undefined;
|
|
34
|
+
withDiffs?: boolean | undefined;
|
|
35
|
+
}, {
|
|
36
|
+
hours?: number | undefined;
|
|
37
|
+
withDiffs?: boolean | undefined;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function handleGetRecent(stmts: Statements, args: z.infer<typeof GetRecentSchema>): string;
|