@codeastra/proxy 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/bin/codeastra.js +475 -0
- package/package.json +39 -0
package/bin/codeastra.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { program } = require("commander");
|
|
7
|
+
const chalk = require("chalk");
|
|
8
|
+
|
|
9
|
+
const CODEASTRA_URL = process.env.CODEASTRA_URL || "https://app.codeastra.dev";
|
|
10
|
+
|
|
11
|
+
const AI_PROVIDERS = {
|
|
12
|
+
"api.openai.com": { name: "OpenAI" },
|
|
13
|
+
"api.anthropic.com": { name: "Anthropic" },
|
|
14
|
+
"generativelanguage.googleapis.com": { name: "Gemini" },
|
|
15
|
+
"api.groq.com": { name: "Groq" },
|
|
16
|
+
"api.mistral.ai": { name: "Mistral" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const EXT_TYPE = {
|
|
20
|
+
jpg:"image",jpeg:"image",png:"image",gif:"image",webp:"image",bmp:"image",tiff:"image",
|
|
21
|
+
mp3:"audio",wav:"audio",ogg:"audio",m4a:"audio",flac:"audio",aac:"audio",
|
|
22
|
+
mp4:"video",avi:"video",mov:"video",mkv:"video",webm:"video",
|
|
23
|
+
js:"code",ts:"code",py:"code",rb:"code",go:"code",java:"code",cs:"code",
|
|
24
|
+
cpp:"code",c:"code",php:"code",sh:"code",sql:"code",env:"code",
|
|
25
|
+
yaml:"code",yml:"code",tf:"code",toml:"code",ini:"code",
|
|
26
|
+
pdf:"document",doc:"document",docx:"document",xls:"document",
|
|
27
|
+
xlsx:"document",csv:"document",txt:"document",rtf:"document",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const MIME_TYPE = {
|
|
31
|
+
"image/jpeg":"image","image/png":"image","image/gif":"image","image/webp":"image",
|
|
32
|
+
"audio/mpeg":"audio","audio/wav":"audio","audio/ogg":"audio","audio/mp4":"audio",
|
|
33
|
+
"video/mp4":"video","video/webm":"video","video/quicktime":"video",
|
|
34
|
+
"application/pdf":"document","text/csv":"document","text/plain":"document",
|
|
35
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":"document",
|
|
36
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":"document",
|
|
37
|
+
"application/javascript":"code","text/javascript":"code",
|
|
38
|
+
"text/x-python":"code","application/json":"code","text/x-sql":"code",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const CREDENTIAL_PATTERNS = [
|
|
42
|
+
{ name:"api_key", re:/(?:api[_-]?key|apikey)\s*[=:]\s*["']?([a-zA-Z0-9_\-]{20,})["']?/gi, severity:"CRITICAL" },
|
|
43
|
+
{ name:"aws_key", re:/AKIA[0-9A-Z]{16}/g, severity:"CRITICAL" },
|
|
44
|
+
{ name:"openai_key", re:/sk-[a-zA-Z0-9]{20,}/g, severity:"CRITICAL" },
|
|
45
|
+
{ name:"anthropic_key", re:/sk-ant-[a-zA-Z0-9\-_]{20,}/g, severity:"CRITICAL" },
|
|
46
|
+
{ name:"google_key", re:/AIza[0-9A-Za-z\-_]{35}/g, severity:"CRITICAL" },
|
|
47
|
+
{ name:"github_token", re:/ghp_[a-zA-Z0-9]{36}/g, severity:"CRITICAL" },
|
|
48
|
+
{ name:"stripe_key", re:/(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{24,}/g, severity:"CRITICAL" },
|
|
49
|
+
{ name:"private_key", re:/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, severity:"CRITICAL" },
|
|
50
|
+
{ name:"password", re:/(?:password|passwd|pwd)\s*[=:]\s*["']([^"']{8,})["']/gi, severity:"HIGH" },
|
|
51
|
+
{ name:"db_connection", re:/(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/gi, severity:"HIGH" },
|
|
52
|
+
{ name:"jwt", re:/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,}/g, severity:"HIGH" },
|
|
53
|
+
{ name:"ssn_literal", re:/['"](?!000|666)\d{3}-(?!00)\d{2}-(?!0000)\d{4}['"]/g, severity:"CRITICAL" },
|
|
54
|
+
{ name:"card_literal", re:/['"]4[0-9]{12}(?:[0-9]{3})?['"]/g, severity:"CRITICAL" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.name("codeastra")
|
|
59
|
+
.description("Zero-code data protection proxy for AI. Text, images, audio, video, code, and documents — all scanned before reaching any LLM.")
|
|
60
|
+
.version("2.0.0");
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command("proxy")
|
|
64
|
+
.description("Start the data protection proxy")
|
|
65
|
+
.option("--target <url>", "Your app URL to forward requests to")
|
|
66
|
+
.option("--port <port>", "Port to listen on", "4000")
|
|
67
|
+
.option("--api-key <key>", "Codeastra API key (or set CODEASTRA_API_KEY)")
|
|
68
|
+
.option("--template <id>", "Compliance template: hipaa | pci_dss | gdpr | soc2 | legal")
|
|
69
|
+
.option("--passthrough", "Fail-open: allow requests if Codeastra unreachable", false)
|
|
70
|
+
.option("--verbose", "Show per-item scan detail", false)
|
|
71
|
+
.option("--block-images", "Block all image uploads", false)
|
|
72
|
+
.option("--block-audio", "Block all audio uploads", false)
|
|
73
|
+
.option("--block-video", "Block all video uploads", false)
|
|
74
|
+
.option("--block-code", "Block all code uploads", false)
|
|
75
|
+
.action(runProxy);
|
|
76
|
+
|
|
77
|
+
program.parse();
|
|
78
|
+
|
|
79
|
+
async function runProxy(opts) {
|
|
80
|
+
const apiKey = opts.apiKey || process.env.CODEASTRA_API_KEY;
|
|
81
|
+
const port = parseInt(opts.port, 10);
|
|
82
|
+
|
|
83
|
+
console.log();
|
|
84
|
+
console.log(chalk.bold.cyan(" 🛡 Codeastra Data Protection Proxy v2.0.0"));
|
|
85
|
+
console.log(chalk.dim(" ─────────────────────────────────────────────────────"));
|
|
86
|
+
console.log(` ${chalk.bold("Listening")} → ${chalk.cyan("http://localhost:" + port)}`);
|
|
87
|
+
if (opts.target) console.log(` ${chalk.bold("Target")} → ${chalk.cyan(opts.target)}`);
|
|
88
|
+
if (opts.template) console.log(` ${chalk.bold("Template")} → ${chalk.cyan(opts.template.toUpperCase())}`);
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(chalk.dim(" Scanning: 📝 Text 🖼 Images 🎵 Audio 🎬 Video 💻 Code 📄 Documents"));
|
|
91
|
+
console.log();
|
|
92
|
+
|
|
93
|
+
if (!apiKey) {
|
|
94
|
+
console.log(chalk.red(" ✗ No API key. Set CODEASTRA_API_KEY or pass --api-key sk-guard-YOUR_KEY\n"));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
console.log(chalk.green(" ✓") + " API key loaded\n");
|
|
98
|
+
|
|
99
|
+
const server = http.createServer(async (req, res) => {
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
103
|
+
const rawBody = Buffer.concat(chunks);
|
|
104
|
+
const contentType = req.headers["content-type"] || "";
|
|
105
|
+
const host = (req.headers["host"] || "").split(":")[0];
|
|
106
|
+
const provider = AI_PROVIDERS[host] || null;
|
|
107
|
+
|
|
108
|
+
let scanResult = { blocked: false, findings: [], contentTypes: [], redactedBody: rawBody };
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
scanResult = await scanAllContent(rawBody, contentType, apiKey, opts);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log(chalk.yellow(" ⚠ Scan error: " + err.message));
|
|
114
|
+
if (!opts.passthrough) {
|
|
115
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
116
|
+
res.end(JSON.stringify({ error: "scan_unavailable", message: err.message }));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ms = Date.now() - start;
|
|
122
|
+
const types = [...new Set(scanResult.contentTypes)];
|
|
123
|
+
const typeStr = types.length ? chalk.dim(" [" + types.join(",") + "]") : "";
|
|
124
|
+
const findStr = scanResult.findings.length
|
|
125
|
+
? chalk.yellow(" " + scanResult.findings.length + " finding(s)")
|
|
126
|
+
: chalk.dim(" clean");
|
|
127
|
+
const provStr = provider ? chalk.dim(" → " + provider.name) : "";
|
|
128
|
+
const icon = scanResult.blocked ? chalk.red("✗ BLOCKED") : chalk.green("✓ CLEAN ");
|
|
129
|
+
|
|
130
|
+
console.log(" " + icon + typeStr + findStr + provStr + " " + chalk.dim(ms + "ms"));
|
|
131
|
+
|
|
132
|
+
if (scanResult.blocked) {
|
|
133
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
134
|
+
res.end(JSON.stringify({
|
|
135
|
+
error: "blocked_by_compliance",
|
|
136
|
+
reason: scanResult.blockReason,
|
|
137
|
+
findings: scanResult.findings,
|
|
138
|
+
}));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const target = opts.target || (provider ? "https://" + host : null);
|
|
143
|
+
if (!target) {
|
|
144
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
145
|
+
res.end(JSON.stringify({ error: "no_target", message: "Pass --target <url>" }));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const headers = buildHeaders(req.headers, host);
|
|
150
|
+
headers["content-length"] = scanResult.redactedBody.length.toString();
|
|
151
|
+
|
|
152
|
+
const upstream = await forwardRequest(target, req.method, req.url, headers, scanResult.redactedBody);
|
|
153
|
+
|
|
154
|
+
res.writeHead(upstream.status, {
|
|
155
|
+
"content-type": upstream.headers["content-type"] || "application/json",
|
|
156
|
+
"x-codeastra": "protected",
|
|
157
|
+
"x-scan-result": scanResult.contentTypes.join(",") || "text",
|
|
158
|
+
});
|
|
159
|
+
res.end(upstream.body);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.on("error", (err) => {
|
|
163
|
+
console.log(chalk.red(" ✗ " + (err.code === "EADDRINUSE" ? "Port " + port + " in use. Try --port " + (port+1) : err.message)));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
server.listen(port, () => console.log(chalk.green(" ✓ Proxy ready\n")));
|
|
168
|
+
process.on("SIGINT", () => { console.log(chalk.dim("\n Stopping...\n")); server.close(() => process.exit(0)); });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function scanAllContent(rawBody, contentType, apiKey, opts) {
|
|
172
|
+
const result = { blocked: false, blockReason: "", findings: [], contentTypes: [], redactedBody: rawBody };
|
|
173
|
+
|
|
174
|
+
if (contentType.includes("multipart/form-data")) {
|
|
175
|
+
const boundary = (contentType.match(/boundary=([^\s;]+)/) || [])[1];
|
|
176
|
+
if (boundary) {
|
|
177
|
+
const parts = parseMultipart(rawBody, boundary);
|
|
178
|
+
const scannedParts = [];
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
const r = await scanPart(part, apiKey, opts);
|
|
181
|
+
result.findings.push(...r.findings);
|
|
182
|
+
result.contentTypes.push(...r.contentTypes);
|
|
183
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.blockReason; return result; }
|
|
184
|
+
scannedParts.push(r.redactedPart || part);
|
|
185
|
+
}
|
|
186
|
+
result.redactedBody = rebuildMultipart(scannedParts, boundary);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (contentType.includes("application/json") || (rawBody[0] === 0x7b)) {
|
|
192
|
+
let body = {};
|
|
193
|
+
try { body = JSON.parse(rawBody.toString()); } catch {}
|
|
194
|
+
|
|
195
|
+
const images = extractBase64Images(body);
|
|
196
|
+
for (const img of images) {
|
|
197
|
+
result.contentTypes.push("image");
|
|
198
|
+
if (opts.blockImages) { result.blocked = true; result.blockReason = "Image uploads blocked by policy"; return result; }
|
|
199
|
+
const r = await scanImageViaBackend(img.data, img.mimeType, apiKey);
|
|
200
|
+
result.findings.push(...r.findings);
|
|
201
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.blockReason; return result; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const text = extractText(body);
|
|
205
|
+
if (text) {
|
|
206
|
+
result.contentTypes.push("text");
|
|
207
|
+
const r = await classifyText(apiKey, text);
|
|
208
|
+
result.findings.push(...(r.findings_summary || []));
|
|
209
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.block_reason || r.explanation; return result; }
|
|
210
|
+
if (r.finding_count > 0 && r.redacted_text) {
|
|
211
|
+
result.redactedBody = Buffer.from(JSON.stringify(replaceText(body, r.redacted_text)));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const mime = contentType.split(";")[0].trim();
|
|
218
|
+
const ctype = MIME_TYPE[mime];
|
|
219
|
+
if (ctype) {
|
|
220
|
+
result.contentTypes.push(ctype);
|
|
221
|
+
if (ctype === "image" && opts.blockImages) { result.blocked = true; result.blockReason = "Image uploads blocked"; return result; }
|
|
222
|
+
if (ctype === "audio" && opts.blockAudio) { result.blocked = true; result.blockReason = "Audio uploads blocked"; return result; }
|
|
223
|
+
if (ctype === "video" && opts.blockVideo) { result.blocked = true; result.blockReason = "Video uploads blocked"; return result; }
|
|
224
|
+
if (ctype === "code" && opts.blockCode) { result.blocked = true; result.blockReason = "Code uploads blocked"; return result; }
|
|
225
|
+
if (ctype === "code") {
|
|
226
|
+
const codeText = rawBody.toString("utf-8");
|
|
227
|
+
const creds = scanCredentials(codeText, "upload");
|
|
228
|
+
result.findings.push(...creds);
|
|
229
|
+
if (creds.some(f => f.severity === "CRITICAL")) {
|
|
230
|
+
result.blocked = true;
|
|
231
|
+
result.blockReason = "Hardcoded credentials detected: " + creds[0].name;
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (ctype === "document" && mime.includes("text")) {
|
|
236
|
+
const text = rawBody.toString("utf-8");
|
|
237
|
+
const r = await classifyText(apiKey, text);
|
|
238
|
+
result.findings.push(...(r.findings_summary || []));
|
|
239
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.block_reason; return result; }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function scanPart(part, apiKey, opts) {
|
|
246
|
+
const result = { findings: [], contentTypes: [], blocked: false, blockReason: "", redactedPart: part };
|
|
247
|
+
const ext = path.extname(part.filename || "").replace(".", "").toLowerCase();
|
|
248
|
+
const ctype = EXT_TYPE[ext] || MIME_TYPE[part.contentType] || "unknown";
|
|
249
|
+
result.contentTypes.push(ctype);
|
|
250
|
+
|
|
251
|
+
if (ctype === "image" && opts.blockImages) return { ...result, blocked: true, blockReason: "Image uploads blocked: " + part.filename };
|
|
252
|
+
if (ctype === "audio" && opts.blockAudio) return { ...result, blocked: true, blockReason: "Audio uploads blocked: " + part.filename };
|
|
253
|
+
if (ctype === "video" && opts.blockVideo) return { ...result, blocked: true, blockReason: "Video uploads blocked: " + part.filename };
|
|
254
|
+
if (ctype === "code" && opts.blockCode) return { ...result, blocked: true, blockReason: "Code uploads blocked: " + part.filename };
|
|
255
|
+
|
|
256
|
+
if (ctype === "code") {
|
|
257
|
+
const text = part.data.toString("utf-8");
|
|
258
|
+
const creds = scanCredentials(text, part.filename);
|
|
259
|
+
result.findings.push(...creds);
|
|
260
|
+
if (creds.some(f => f.severity === "CRITICAL")) {
|
|
261
|
+
result.blocked = true;
|
|
262
|
+
result.blockReason = "Hardcoded credentials in " + part.filename + ": " + creds[0].name;
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
const textScan = await classifyText(apiKey, text);
|
|
266
|
+
result.findings.push(...(textScan.findings_summary || []));
|
|
267
|
+
if (textScan.finding_count > 0 && textScan.redacted_text) {
|
|
268
|
+
result.redactedPart = { ...part, data: Buffer.from(textScan.redacted_text) };
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (ctype === "image") {
|
|
274
|
+
const r = await scanImageViaBackend(part.data.toString("base64"), part.contentType, apiKey);
|
|
275
|
+
result.findings.push(...r.findings);
|
|
276
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.blockReason; }
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (ctype === "document") {
|
|
281
|
+
if (part.contentType && (part.contentType.includes("text") || part.contentType.includes("csv"))) {
|
|
282
|
+
const text = part.data.toString("utf-8");
|
|
283
|
+
const r = await classifyText(apiKey, text);
|
|
284
|
+
result.findings.push(...(r.findings_summary || []));
|
|
285
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.block_reason; }
|
|
286
|
+
} else {
|
|
287
|
+
const r = await scanDocViaBackend(part.data.toString("base64"), part.contentType, part.filename, apiKey);
|
|
288
|
+
result.findings.push(...r.findings);
|
|
289
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.blockReason; }
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (ctype === "audio" || ctype === "video") {
|
|
295
|
+
result.findings.push({
|
|
296
|
+
type: ctype, pattern: ctype + "_upload", severity: "MEDIUM",
|
|
297
|
+
detail: ctype + " file intercepted: " + part.filename + " — sent to backend for analysis",
|
|
298
|
+
});
|
|
299
|
+
const r = await scanMediaViaBackend(part.data.toString("base64"), part.contentType, ctype, part.filename, apiKey);
|
|
300
|
+
result.findings.push(...r.findings);
|
|
301
|
+
if (r.blocked) { result.blocked = true; result.blockReason = r.blockReason; }
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function classifyText(apiKey, text) {
|
|
309
|
+
const res = await fetchTimeout(CODEASTRA_URL + "/classify", {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
312
|
+
body: JSON.stringify({ text }),
|
|
313
|
+
}, 8000);
|
|
314
|
+
if (!res.ok) throw new Error("Classify " + res.status);
|
|
315
|
+
return res.json();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function scanImageViaBackend(base64, mimeType, apiKey) {
|
|
319
|
+
try {
|
|
320
|
+
const res = await fetchTimeout(CODEASTRA_URL + "/classify/image", {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
323
|
+
body: JSON.stringify({ content_base64: base64, mime_type: mimeType }),
|
|
324
|
+
}, 15000);
|
|
325
|
+
if (!res.ok) return { findings: [{ type:"image", pattern:"unscanned", severity:"MEDIUM", detail:"Image flagged for review" }], blocked: false };
|
|
326
|
+
return res.json();
|
|
327
|
+
} catch { return { findings: [], blocked: false }; }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function scanDocViaBackend(base64, mimeType, filename, apiKey) {
|
|
331
|
+
try {
|
|
332
|
+
const res = await fetchTimeout(CODEASTRA_URL + "/classify/document", {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
335
|
+
body: JSON.stringify({ content_base64: base64, mime_type: mimeType, filename }),
|
|
336
|
+
}, 20000);
|
|
337
|
+
if (!res.ok) return { findings: [], blocked: false };
|
|
338
|
+
return res.json();
|
|
339
|
+
} catch { return { findings: [], blocked: false }; }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function scanMediaViaBackend(base64, mimeType, mediaType, filename, apiKey) {
|
|
343
|
+
try {
|
|
344
|
+
const res = await fetchTimeout(CODEASTRA_URL + "/classify/" + mediaType, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
|
|
347
|
+
body: JSON.stringify({ content_base64: base64, mime_type: mimeType, filename }),
|
|
348
|
+
}, 30000);
|
|
349
|
+
if (!res.ok) return { findings: [], blocked: false };
|
|
350
|
+
return res.json();
|
|
351
|
+
} catch { return { findings: [], blocked: false }; }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function scanCredentials(code, filename) {
|
|
355
|
+
const findings = [];
|
|
356
|
+
for (const p of CREDENTIAL_PATTERNS) {
|
|
357
|
+
p.re.lastIndex = 0;
|
|
358
|
+
for (const m of code.matchAll(p.re)) {
|
|
359
|
+
findings.push({ type:"code", name: p.name, severity: p.severity, filename, pattern: p.name,
|
|
360
|
+
detail: p.name + " found in " + filename });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return findings;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function extractBase64Images(body) {
|
|
367
|
+
const imgs = [];
|
|
368
|
+
if (!body || typeof body !== "object") return imgs;
|
|
369
|
+
if (Array.isArray(body.messages)) {
|
|
370
|
+
for (const msg of body.messages) {
|
|
371
|
+
if (Array.isArray(msg.content)) {
|
|
372
|
+
for (const block of msg.content) {
|
|
373
|
+
if (block.type === "image_url" && block.image_url?.url?.startsWith("data:")) {
|
|
374
|
+
const [hdr, data] = block.image_url.url.split(",");
|
|
375
|
+
imgs.push({ data, mimeType: (hdr.match(/data:([^;]+)/) || [])[1] || "image/jpeg" });
|
|
376
|
+
}
|
|
377
|
+
if (block.type === "image" && block.source?.type === "base64") {
|
|
378
|
+
imgs.push({ data: block.source.data, mimeType: block.source.media_type });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return imgs;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function extractText(body) {
|
|
388
|
+
if (!body || typeof body !== "object") return null;
|
|
389
|
+
if (Array.isArray(body.messages)) {
|
|
390
|
+
return body.messages.filter(m => m.role === "user").map(m => typeof m.content === "string" ? m.content : "").join("\n") || null;
|
|
391
|
+
}
|
|
392
|
+
return body.prompt || body.text || (typeof body.content === "string" ? body.content : null) || body.query || null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function replaceText(body, redacted) {
|
|
396
|
+
if (Array.isArray(body.messages)) {
|
|
397
|
+
const msgs = [...body.messages];
|
|
398
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
399
|
+
if (msgs[i].role === "user") { msgs[i] = { ...msgs[i], content: redacted }; break; }
|
|
400
|
+
}
|
|
401
|
+
return { ...body, messages: msgs };
|
|
402
|
+
}
|
|
403
|
+
if (body.prompt) return { ...body, prompt: redacted };
|
|
404
|
+
if (body.text) return { ...body, text: redacted };
|
|
405
|
+
return body;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function buildHeaders(reqHeaders, host) {
|
|
409
|
+
const h = { ...reqHeaders };
|
|
410
|
+
["connection","keep-alive","te","trailers","transfer-encoding","upgrade"].forEach(k => delete h[k]);
|
|
411
|
+
h["x-no-training"] = "true";
|
|
412
|
+
h["x-data-usage-policy"] = "api-only-no-training";
|
|
413
|
+
if (host.includes("anthropic")) h["anthropic-beta"] = "no-training-1";
|
|
414
|
+
return h;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function forwardRequest(target, method, urlPath, headers, body) {
|
|
418
|
+
const url = target.replace(/\/$/, "") + urlPath;
|
|
419
|
+
const res = await fetchTimeout(url, {
|
|
420
|
+
method,
|
|
421
|
+
headers,
|
|
422
|
+
body: ["GET","HEAD"].includes(method.toUpperCase()) ? undefined : body,
|
|
423
|
+
}, 60000);
|
|
424
|
+
const respBody = Buffer.from(await res.arrayBuffer());
|
|
425
|
+
const respHeaders = {};
|
|
426
|
+
res.headers.forEach((v, k) => { respHeaders[k] = v; });
|
|
427
|
+
return { status: res.status, headers: respHeaders, body: respBody };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function parseMultipart(body, boundary) {
|
|
431
|
+
const parts = []; const delim = Buffer.from("--" + boundary);
|
|
432
|
+
let offset = 0;
|
|
433
|
+
while (offset < body.length) {
|
|
434
|
+
const di = body.indexOf(delim, offset);
|
|
435
|
+
if (di === -1) break;
|
|
436
|
+
offset = di + delim.length + 2;
|
|
437
|
+
if (body.slice(di, di + delim.length + 2).indexOf(Buffer.from("--")) !== -1 && offset > body.length - 10) break;
|
|
438
|
+
const he = body.indexOf(Buffer.from("\r\n\r\n"), offset);
|
|
439
|
+
if (he === -1) break;
|
|
440
|
+
const hstr = body.slice(offset, he).toString();
|
|
441
|
+
const ds = he + 4;
|
|
442
|
+
const nd = body.indexOf(delim, ds);
|
|
443
|
+
const de = nd !== -1 ? nd - 2 : body.length;
|
|
444
|
+
const dispM = hstr.match(/Content-Disposition: ([^\r\n]+)/i);
|
|
445
|
+
const typeM = hstr.match(/Content-Type: ([^\r\n]+)/i);
|
|
446
|
+
parts.push({
|
|
447
|
+
name: (dispM?.[1].match(/name="([^"]+)"/) || [])[1] || "",
|
|
448
|
+
filename: (dispM?.[1].match(/filename="([^"]+)"/) || [])[1] || "",
|
|
449
|
+
contentType: typeM?.[1]?.trim() || "text/plain",
|
|
450
|
+
data: body.slice(ds, de), headerStr: hstr,
|
|
451
|
+
});
|
|
452
|
+
offset = nd !== -1 ? nd : body.length;
|
|
453
|
+
}
|
|
454
|
+
return parts;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function rebuildMultipart(parts, boundary) {
|
|
458
|
+
const chunks = [];
|
|
459
|
+
for (const p of parts) {
|
|
460
|
+
chunks.push(Buffer.from("--" + boundary + "\r\n"));
|
|
461
|
+
chunks.push(Buffer.from(p.headerStr));
|
|
462
|
+
chunks.push(Buffer.from("\r\n\r\n"));
|
|
463
|
+
chunks.push(p.data);
|
|
464
|
+
chunks.push(Buffer.from("\r\n"));
|
|
465
|
+
}
|
|
466
|
+
chunks.push(Buffer.from("--" + boundary + "--\r\n"));
|
|
467
|
+
return Buffer.concat(chunks);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function fetchTimeout(url, options, timeout) {
|
|
471
|
+
const ctrl = new AbortController();
|
|
472
|
+
const timer = setTimeout(() => ctrl.abort(), timeout);
|
|
473
|
+
try { return await fetch(url, { ...options, signal: ctrl.signal }); }
|
|
474
|
+
finally { clearTimeout(timer); }
|
|
475
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeastra/proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-code data protection proxy for AI. PHI, PCI, PII, images, audio, video, code and documents — all scanned and redacted before reaching any LLM.",
|
|
5
|
+
"main": "bin/codeastra.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codeastra": "./bin/codeastra.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"hipaa",
|
|
11
|
+
"pci-dss",
|
|
12
|
+
"gdpr",
|
|
13
|
+
"pii",
|
|
14
|
+
"phi",
|
|
15
|
+
"ai",
|
|
16
|
+
"llm",
|
|
17
|
+
"openai",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"proxy",
|
|
20
|
+
"compliance",
|
|
21
|
+
"data-protection",
|
|
22
|
+
"security"
|
|
23
|
+
],
|
|
24
|
+
"author": "Codeastra",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^4.1.2",
|
|
28
|
+
"commander": "^11.0.0",
|
|
29
|
+
"http-proxy": "^1.18.1"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/codeastra/codeastra"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://app.codeastra.dev"
|
|
39
|
+
}
|