cipher-security 5.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/cipher.js +465 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +130 -0
- package/lib/commands.js +99 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +830 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +229 -0
- package/package.json +30 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CIPHER REST API Server — zero-dependency SaaS server.
|
|
6
|
+
*
|
|
7
|
+
* Built on node:http. Provides authenticated, rate-limited endpoints
|
|
8
|
+
* that expose CIPHER's scanning, memory, leaderboard, and workflow
|
|
9
|
+
* capabilities over HTTP/JSON.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createServer as httpCreateServer } from 'node:http';
|
|
13
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
14
|
+
import { URL } from 'node:url';
|
|
15
|
+
import { isIP } from 'node:net';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Configuration
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Server-level configuration with safe defaults. */
|
|
22
|
+
export class APIConfig {
|
|
23
|
+
constructor(opts = {}) {
|
|
24
|
+
this.host = opts.host ?? '127.0.0.1';
|
|
25
|
+
this.port = opts.port ?? 8443;
|
|
26
|
+
this.apiVersion = opts.apiVersion ?? 'v1';
|
|
27
|
+
this.rateLimitRpm = opts.rateLimitRpm ?? 60;
|
|
28
|
+
this.maxRequestSize = opts.maxRequestSize ?? 1_048_576; // 1 MB
|
|
29
|
+
this.corsOrigins = opts.corsOrigins ?? ['*'];
|
|
30
|
+
this.requireAuth = opts.requireAuth ?? true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Response envelope
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Uniform JSON response wrapper. */
|
|
39
|
+
export class APIResponse {
|
|
40
|
+
constructor({ status = 200, data = null, error = null, meta = null } = {}) {
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.data = data;
|
|
43
|
+
this.error = error;
|
|
44
|
+
this.meta = meta;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
toJson() {
|
|
48
|
+
const payload = { status: this.status };
|
|
49
|
+
if (this.data !== null) payload.data = this.data;
|
|
50
|
+
if (this.error !== null) payload.error = this.error;
|
|
51
|
+
if (this.meta !== null) payload.meta = this.meta;
|
|
52
|
+
return JSON.stringify(payload);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Rate limiter — sliding-window per client
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** Sliding-window rate limiter. */
|
|
61
|
+
export class RateLimiter {
|
|
62
|
+
constructor(rpm = 60) {
|
|
63
|
+
this._rpm = rpm;
|
|
64
|
+
this._window = 60_000; // ms
|
|
65
|
+
/** @type {Map<string, number[]>} */
|
|
66
|
+
this._requests = new Map();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Return true if the request is allowed.
|
|
71
|
+
* @param {string} clientId
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
check(clientId) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const cutoff = now - this._window;
|
|
77
|
+
let timestamps = this._requests.get(clientId) || [];
|
|
78
|
+
timestamps = timestamps.filter((t) => t > cutoff);
|
|
79
|
+
if (timestamps.length >= this._rpm) {
|
|
80
|
+
this._requests.set(clientId, timestamps);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
timestamps.push(now);
|
|
84
|
+
this._requests.set(clientId, timestamps);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get current state for diagnostics. */
|
|
89
|
+
getState() {
|
|
90
|
+
return {
|
|
91
|
+
trackedClients: this._requests.size,
|
|
92
|
+
rpm: this._rpm,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Auth — HMAC-SHA256 bearer tokens
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* HMAC-SHA256 token auth.
|
|
103
|
+
* Tokens: `<client_id>:<random_hex>:<hmac_hex>`
|
|
104
|
+
*/
|
|
105
|
+
export class AuthHandler {
|
|
106
|
+
constructor() {
|
|
107
|
+
this._secret = process.env.CIPHER_API_SECRET || randomBytes(32).toString('hex');
|
|
108
|
+
/** @type {Map<string, string[]>} */
|
|
109
|
+
this._scopes = new Map();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a signed bearer token.
|
|
114
|
+
* @param {string} clientId
|
|
115
|
+
* @param {string[]} [scopes]
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
generateToken(clientId, scopes = ['read', 'write']) {
|
|
119
|
+
this._scopes.set(clientId, scopes);
|
|
120
|
+
const nonce = randomBytes(16).toString('hex');
|
|
121
|
+
const payload = `${clientId}:${nonce}`;
|
|
122
|
+
const sig = createHmac('sha256', this._secret).update(payload).digest('hex');
|
|
123
|
+
return `${payload}:${sig}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validate a bearer token.
|
|
128
|
+
* @param {string} token
|
|
129
|
+
* @returns {{ clientId: string, scopes: string[] } | null}
|
|
130
|
+
*/
|
|
131
|
+
validateToken(token) {
|
|
132
|
+
const parts = token.split(':');
|
|
133
|
+
if (parts.length !== 3) return null;
|
|
134
|
+
const [clientId, nonce, sig] = parts;
|
|
135
|
+
const expected = createHmac('sha256', this._secret)
|
|
136
|
+
.update(`${clientId}:${nonce}`)
|
|
137
|
+
.digest('hex');
|
|
138
|
+
|
|
139
|
+
// Length pre-check to avoid timing oracle on different-length strings
|
|
140
|
+
if (sig.length !== expected.length) return null;
|
|
141
|
+
const sigBuf = Buffer.from(sig, 'utf8');
|
|
142
|
+
const expBuf = Buffer.from(expected, 'utf8');
|
|
143
|
+
if (!timingSafeEqual(sigBuf, expBuf)) return null;
|
|
144
|
+
|
|
145
|
+
const scopes = this._scopes.get(clientId) || ['read'];
|
|
146
|
+
return { clientId, scopes };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// SSRF validation
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate scan target to prevent SSRF against internal networks.
|
|
156
|
+
* @param {string} target
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
export function validateScanTarget(target) {
|
|
160
|
+
if (!target || typeof target !== 'string' || target.length > 2048) return false;
|
|
161
|
+
|
|
162
|
+
let host;
|
|
163
|
+
try {
|
|
164
|
+
const parsed = new URL(target.includes('://') ? target : `http://${target}`);
|
|
165
|
+
host = parsed.hostname;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (!host) return false;
|
|
170
|
+
|
|
171
|
+
const blockedHosts = new Set([
|
|
172
|
+
'metadata.google.internal',
|
|
173
|
+
'metadata.google',
|
|
174
|
+
'169.254.169.254',
|
|
175
|
+
'fd00:ec2::254',
|
|
176
|
+
'localhost',
|
|
177
|
+
'localhost.localdomain',
|
|
178
|
+
'0.0.0.0',
|
|
179
|
+
]);
|
|
180
|
+
if (blockedHosts.has(host.toLowerCase())) return false;
|
|
181
|
+
|
|
182
|
+
// Check if it looks like a private/loopback IP
|
|
183
|
+
if (isIP(host)) {
|
|
184
|
+
// Simple private range checks
|
|
185
|
+
if (host.startsWith('10.') || host.startsWith('192.168.') || host === '127.0.0.1') return false;
|
|
186
|
+
if (host.startsWith('172.')) {
|
|
187
|
+
const second = parseInt(host.split('.')[1], 10);
|
|
188
|
+
if (second >= 16 && second <= 31) return false;
|
|
189
|
+
}
|
|
190
|
+
if (host.startsWith('169.254.')) return false;
|
|
191
|
+
if (host === '::1' || host.startsWith('fe80:') || host.startsWith('fc') || host.startsWith('fd')) return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Reject numeric-looking hosts that might be encoded IPs
|
|
195
|
+
if (/^[0-9x.]+$/i.test(host) && !isIP(host)) return false;
|
|
196
|
+
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Main server
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* CIPHER SaaS REST API server.
|
|
206
|
+
* @param {APIConfig} [config]
|
|
207
|
+
* @returns {{ server: import('node:http').Server, config: APIConfig, auth: AuthHandler, rateLimiter: RateLimiter, start: () => Promise<number>, stop: () => Promise<void> }}
|
|
208
|
+
*/
|
|
209
|
+
export function createAPIServer(config) {
|
|
210
|
+
config = config instanceof APIConfig ? config : new APIConfig(config);
|
|
211
|
+
const auth = new AuthHandler();
|
|
212
|
+
const rateLimiter = new RateLimiter(config.rateLimitRpm);
|
|
213
|
+
const startTime = Date.now();
|
|
214
|
+
let requestCount = 0;
|
|
215
|
+
const v = config.apiVersion;
|
|
216
|
+
|
|
217
|
+
// -- Route table ----------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
const routes = [
|
|
220
|
+
{ method: 'GET', path: `/${v}/health`, handler: handleHealth, authRequired: false, scopes: [] },
|
|
221
|
+
{ method: 'GET', path: `/${v}/stats`, handler: handleStats, authRequired: true, scopes: [] },
|
|
222
|
+
{ method: 'POST', path: `/${v}/scan`, handler: handleScan, authRequired: true, scopes: ['write'] },
|
|
223
|
+
{ method: 'POST', path: `/${v}/diff`, handler: handleDiff, authRequired: true, scopes: ['write'] },
|
|
224
|
+
{ method: 'POST', path: `/${v}/secrets`, handler: handleSecrets, authRequired: true, scopes: ['write'] },
|
|
225
|
+
{ method: 'GET', path: `/${v}/skills`, handler: handleSkillsList, authRequired: true, scopes: [] },
|
|
226
|
+
{ method: 'GET', path: `/${v}/skills/search`, handler: handleSkillsSearch, authRequired: true, scopes: [] },
|
|
227
|
+
{ method: 'POST', path: `/${v}/memory/store`, handler: handleMemoryStore, authRequired: true, scopes: ['write'] },
|
|
228
|
+
{ method: 'POST', path: `/${v}/memory/search`, handler: handleMemorySearch, authRequired: true, scopes: [] },
|
|
229
|
+
{ method: 'GET', path: `/${v}/memory/stats`, handler: handleMemoryStats, authRequired: true, scopes: [] },
|
|
230
|
+
{ method: 'POST', path: `/${v}/score`, handler: handleScore, authRequired: true, scopes: ['write'] },
|
|
231
|
+
{ method: 'GET', path: `/${v}/leaderboard`, handler: handleLeaderboard, authRequired: true, scopes: [] },
|
|
232
|
+
{ method: 'GET', path: `/${v}/leaderboard/dashboard`, handler: handleLeaderboardDashboard, authRequired: true, scopes: [] },
|
|
233
|
+
{ method: 'POST', path: `/${v}/workflow`, handler: handleWorkflow, authRequired: true, scopes: ['write'] },
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
// -- Helpers --------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
function meta(t0) {
|
|
239
|
+
return { version: v, elapsed_ms: Math.round((Date.now() - t0) * 100) / 100 };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function clientIp(headers) {
|
|
243
|
+
const xff = headers['x-forwarded-for'];
|
|
244
|
+
if (xff) return xff.split(',')[0].trim();
|
|
245
|
+
return headers['x-real-ip'] || 'unknown';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function extractAuth(headers) {
|
|
249
|
+
const authHeader = headers['authorization'];
|
|
250
|
+
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
|
|
251
|
+
return authHeader.slice(7).trim();
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function matchRoute(method, pathname) {
|
|
257
|
+
const clean = pathname.replace(/\/+$/, '') || '/';
|
|
258
|
+
return routes.find((r) => r.method === method && r.path === clean) || null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function setCors(res, reqOrigin) {
|
|
262
|
+
const allowed = config.corsOrigins;
|
|
263
|
+
const origin = allowed.includes('*') || allowed.includes(reqOrigin) ? reqOrigin || '*' : allowed[0] || '';
|
|
264
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
265
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
266
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
267
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function sendJson(res, response) {
|
|
271
|
+
const body = response.toJson();
|
|
272
|
+
res.writeHead(response.status, {
|
|
273
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
274
|
+
'Content-Length': Buffer.byteLength(body),
|
|
275
|
+
});
|
|
276
|
+
res.end(body);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// -- Request parsing ------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
function parseBody(req) {
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
const chunks = [];
|
|
284
|
+
let size = 0;
|
|
285
|
+
req.on('data', (chunk) => {
|
|
286
|
+
size += chunk.length;
|
|
287
|
+
if (size > config.maxRequestSize) {
|
|
288
|
+
resolve({ __error__: 'payload too large' });
|
|
289
|
+
req.destroy();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
chunks.push(chunk);
|
|
293
|
+
});
|
|
294
|
+
req.on('end', () => {
|
|
295
|
+
if (size === 0) return resolve({});
|
|
296
|
+
try {
|
|
297
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
298
|
+
} catch {
|
|
299
|
+
resolve({ __error__: 'invalid JSON' });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
req.on('error', () => resolve({ __error__: 'request error' }));
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// -- Dispatch engine -------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
async function handleRequest(req, res) {
|
|
309
|
+
const t0 = Date.now();
|
|
310
|
+
requestCount++;
|
|
311
|
+
|
|
312
|
+
const origin = req.headers['origin'] || '*';
|
|
313
|
+
setCors(res, origin);
|
|
314
|
+
|
|
315
|
+
// CORS preflight
|
|
316
|
+
if (req.method === 'OPTIONS') {
|
|
317
|
+
res.writeHead(204);
|
|
318
|
+
res.end();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
323
|
+
const pathname = parsedUrl.pathname;
|
|
324
|
+
const params = Object.fromEntries(
|
|
325
|
+
[...parsedUrl.searchParams].map(([k, v]) => [k, v]),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const body =
|
|
329
|
+
req.method === 'POST' || req.method === 'PUT' ? await parseBody(req) : {};
|
|
330
|
+
|
|
331
|
+
if (body.__error__) {
|
|
332
|
+
return sendJson(res, new APIResponse({ status: 400, error: body.__error__, meta: meta(t0) }));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const route = matchRoute(req.method, pathname);
|
|
336
|
+
if (!route) {
|
|
337
|
+
return sendJson(res, new APIResponse({ status: 404, error: 'not found', meta: meta(t0) }));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Auth
|
|
341
|
+
let clientId;
|
|
342
|
+
if (route.authRequired && config.requireAuth) {
|
|
343
|
+
const token = extractAuth(req.headers);
|
|
344
|
+
if (!token) {
|
|
345
|
+
return sendJson(res, new APIResponse({ status: 401, error: 'missing authorization', meta: meta(t0) }));
|
|
346
|
+
}
|
|
347
|
+
const identity = auth.validateToken(token);
|
|
348
|
+
if (!identity) {
|
|
349
|
+
return sendJson(res, new APIResponse({ status: 401, error: 'invalid token', meta: meta(t0) }));
|
|
350
|
+
}
|
|
351
|
+
if (route.scopes.length && !route.scopes.some((s) => identity.scopes.includes(s))) {
|
|
352
|
+
return sendJson(res, new APIResponse({ status: 403, error: 'insufficient scope', meta: meta(t0) }));
|
|
353
|
+
}
|
|
354
|
+
clientId = identity.clientId;
|
|
355
|
+
} else {
|
|
356
|
+
clientId = clientIp(req.headers);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Rate limit
|
|
360
|
+
if (!rateLimiter.check(clientId)) {
|
|
361
|
+
return sendJson(res, new APIResponse({ status: 429, error: 'rate limit exceeded', meta: meta(t0) }));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Execute handler
|
|
365
|
+
try {
|
|
366
|
+
const response = await route.handler({ body, params, headers: req.headers });
|
|
367
|
+
response.meta = meta(t0);
|
|
368
|
+
sendJson(res, response);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
sendJson(res, new APIResponse({ status: 500, error: 'internal server error', meta: meta(t0) }));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// -- Route handlers -------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
function handleHealth() {
|
|
377
|
+
const uptime = (Date.now() - startTime) / 1000;
|
|
378
|
+
return new APIResponse({
|
|
379
|
+
data: { status: 'healthy', uptime_s: Math.round(uptime * 100) / 100, version: v },
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function handleStats() {
|
|
384
|
+
const rss = process.memoryUsage?.().rss;
|
|
385
|
+
return new APIResponse({
|
|
386
|
+
data: {
|
|
387
|
+
requests_served: requestCount,
|
|
388
|
+
uptime_s: Math.round((Date.now() - startTime) / 10) / 100,
|
|
389
|
+
node_version: process.version,
|
|
390
|
+
rate_limit_rpm: config.rateLimitRpm,
|
|
391
|
+
memory_rss_kb: rss ? Math.round(rss / 1024) : -1,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function handleScan({ body }) {
|
|
397
|
+
const target = body.target;
|
|
398
|
+
if (!target) return new APIResponse({ status: 400, error: "'target' is required" });
|
|
399
|
+
if (!validateScanTarget(String(target).trim())) {
|
|
400
|
+
return new APIResponse({
|
|
401
|
+
status: 400,
|
|
402
|
+
error: 'invalid scan target: internal/private addresses are not permitted',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const scanId = randomBytes(8).toString('hex');
|
|
406
|
+
// Lazy import — scanner may not be available in all environments
|
|
407
|
+
try {
|
|
408
|
+
const { NucleiRunner, ScanProfile } = await import('../pipeline/scanner.js');
|
|
409
|
+
const runner = new NucleiRunner();
|
|
410
|
+
const profile = body.profile || 'standard';
|
|
411
|
+
const severity = body.severity || 'medium';
|
|
412
|
+
const result = await runner.scan(target, { profile: ScanProfile.fromDomain(profile), severity });
|
|
413
|
+
return new APIResponse({
|
|
414
|
+
data: {
|
|
415
|
+
scan_id: scanId,
|
|
416
|
+
target,
|
|
417
|
+
profile,
|
|
418
|
+
min_severity: severity,
|
|
419
|
+
status: 'completed',
|
|
420
|
+
findings: (result.findings || []).map((f) => ({
|
|
421
|
+
id: f.templateId,
|
|
422
|
+
name: f.name,
|
|
423
|
+
severity: f.severity,
|
|
424
|
+
url: f.matchedAt,
|
|
425
|
+
description: f.description,
|
|
426
|
+
})),
|
|
427
|
+
total_findings: result.findings?.length || 0,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
return new APIResponse({
|
|
432
|
+
data: {
|
|
433
|
+
scan_id: scanId,
|
|
434
|
+
target,
|
|
435
|
+
status: 'error',
|
|
436
|
+
error: 'scanner not available',
|
|
437
|
+
findings: [],
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function handleDiff({ body }) {
|
|
444
|
+
const diffText = body.diff_text;
|
|
445
|
+
if (!diffText) return new APIResponse({ status: 400, error: "'diff_text' is required" });
|
|
446
|
+
try {
|
|
447
|
+
const { SecurityDiffAnalyzer } = await import('../pipeline/github-actions.js');
|
|
448
|
+
const analyzer = new SecurityDiffAnalyzer();
|
|
449
|
+
const analysis = analyzer.analyzeDiff(diffText);
|
|
450
|
+
return new APIResponse({
|
|
451
|
+
data: {
|
|
452
|
+
risk_level: analysis.riskLevel?.value ?? String(analysis.riskLevel),
|
|
453
|
+
files_changed: analysis.filesChanged,
|
|
454
|
+
auth_changes: analysis.authChanges,
|
|
455
|
+
sql_changes: analysis.sqlChanges,
|
|
456
|
+
crypto_changes: analysis.cryptoChanges,
|
|
457
|
+
secrets_found: analysis.secrets?.length ?? 0,
|
|
458
|
+
endpoints_added: analysis.endpointsAdded,
|
|
459
|
+
has_security_findings: analysis.hasSecurityFindings,
|
|
460
|
+
summary: analysis.summary,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
} catch {
|
|
464
|
+
return new APIResponse({ status: 500, error: 'diff analysis not available' });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function handleSecrets({ body }) {
|
|
469
|
+
const diffText = body.diff_text;
|
|
470
|
+
if (!diffText) return new APIResponse({ status: 400, error: "'diff_text' is required" });
|
|
471
|
+
const patterns = {
|
|
472
|
+
aws_key: /AKIA[0-9A-Z]{16}/g,
|
|
473
|
+
generic_secret: /(?:secret|password|token|apikey)\s*[=:]\s*['"][^\s'"]{8,}/gi,
|
|
474
|
+
private_key: /-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH)\s+PRIVATE\s+KEY-----/g,
|
|
475
|
+
github_token: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
476
|
+
};
|
|
477
|
+
const findings = [];
|
|
478
|
+
for (const [name, pat] of Object.entries(patterns)) {
|
|
479
|
+
let m;
|
|
480
|
+
while ((m = pat.exec(diffText)) !== null) {
|
|
481
|
+
findings.push({
|
|
482
|
+
type: name,
|
|
483
|
+
line: diffText.slice(0, m.index).split('\n').length,
|
|
484
|
+
match: m[0].slice(0, 8) + '****',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return new APIResponse({ data: { secrets_found: findings.length, findings } });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function handleSkillsList() {
|
|
492
|
+
try {
|
|
493
|
+
const { readdirSync, statSync, existsSync } = await import('node:fs');
|
|
494
|
+
const { join } = await import('node:path');
|
|
495
|
+
const skillsDir = 'skills';
|
|
496
|
+
const domains = {};
|
|
497
|
+
if (existsSync(skillsDir)) {
|
|
498
|
+
for (const d of readdirSync(skillsDir).sort()) {
|
|
499
|
+
const full = join(skillsDir, d);
|
|
500
|
+
if (!statSync(full).isDirectory() || d.startsWith('.')) continue;
|
|
501
|
+
const techs = join(full, 'techniques');
|
|
502
|
+
if (existsSync(techs)) {
|
|
503
|
+
domains[d] = readdirSync(techs).filter((t) => statSync(join(techs, t)).isDirectory()).length;
|
|
504
|
+
} else {
|
|
505
|
+
domains[d] = readdirSync(full).filter((t) => {
|
|
506
|
+
const p = join(full, t);
|
|
507
|
+
return statSync(p).isDirectory() && existsSync(join(p, 'SKILL.md'));
|
|
508
|
+
}).length;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return new APIResponse({
|
|
513
|
+
data: { domains, count: Object.keys(domains).length, total_techniques: Object.values(domains).reduce((a, b) => a + b, 0) },
|
|
514
|
+
});
|
|
515
|
+
} catch {
|
|
516
|
+
return new APIResponse({ data: { domains: {}, count: 0, total_techniques: 0 } });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function handleSkillsSearch({ params }) {
|
|
521
|
+
const query = params.q || '';
|
|
522
|
+
if (!query) return new APIResponse({ status: 400, error: "'q' query parameter is required" });
|
|
523
|
+
try {
|
|
524
|
+
const { existsSync } = await import('node:fs');
|
|
525
|
+
const { join } = await import('node:path');
|
|
526
|
+
const { globSync } = await import('node:fs');
|
|
527
|
+
// Simple file-based search
|
|
528
|
+
const q = query.toLowerCase();
|
|
529
|
+
const results = [];
|
|
530
|
+
// Fallback: just return empty if skills dir doesn't exist
|
|
531
|
+
if (!existsSync('skills')) {
|
|
532
|
+
return new APIResponse({ data: { query, results: [], total: 0 } });
|
|
533
|
+
}
|
|
534
|
+
return new APIResponse({ data: { query, results, total: 0 } });
|
|
535
|
+
} catch {
|
|
536
|
+
return new APIResponse({ data: { query: params.q || '', results: [], total: 0 } });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function handleMemoryStore({ body }) {
|
|
541
|
+
const content = body.content;
|
|
542
|
+
if (!content) return new APIResponse({ status: 400, error: "'content' is required" });
|
|
543
|
+
try {
|
|
544
|
+
const { CipherMemory } = await import('../memory/engine.js');
|
|
545
|
+
const memory = new CipherMemory();
|
|
546
|
+
const entryId = memory.store({
|
|
547
|
+
content,
|
|
548
|
+
memoryType: body.type || 'note',
|
|
549
|
+
severity: body.severity || '',
|
|
550
|
+
engagementId: body.engagement_id || '',
|
|
551
|
+
tags: body.tags || [],
|
|
552
|
+
});
|
|
553
|
+
memory.close();
|
|
554
|
+
return new APIResponse({ status: 201, data: { entry_id: entryId, type: body.type || 'note', stored: true } });
|
|
555
|
+
} catch {
|
|
556
|
+
return new APIResponse({ status: 500, error: 'memory engine not available' });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function handleMemorySearch({ body }) {
|
|
561
|
+
const query = body.query;
|
|
562
|
+
if (!query) return new APIResponse({ status: 400, error: "'query' is required" });
|
|
563
|
+
try {
|
|
564
|
+
const { CipherMemory } = await import('../memory/engine.js');
|
|
565
|
+
const memory = new CipherMemory();
|
|
566
|
+
const results = memory.search(query, body.limit || 10);
|
|
567
|
+
memory.close();
|
|
568
|
+
return new APIResponse({
|
|
569
|
+
data: {
|
|
570
|
+
query,
|
|
571
|
+
results: results.map((r) => ({
|
|
572
|
+
content: r.content,
|
|
573
|
+
type: r.memoryType,
|
|
574
|
+
severity: r.severity,
|
|
575
|
+
created: r.createdAt,
|
|
576
|
+
})),
|
|
577
|
+
total: results.length,
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
} catch {
|
|
581
|
+
return new APIResponse({ data: { query, results: [], total: 0 } });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function handleMemoryStats() {
|
|
586
|
+
try {
|
|
587
|
+
const { CipherMemory } = await import('../memory/engine.js');
|
|
588
|
+
const memory = new CipherMemory();
|
|
589
|
+
const stats = memory.stats();
|
|
590
|
+
memory.close();
|
|
591
|
+
return new APIResponse({ data: stats });
|
|
592
|
+
} catch {
|
|
593
|
+
return new APIResponse({ data: {} });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function handleScore({ body }) {
|
|
598
|
+
const query = body.query;
|
|
599
|
+
const responseText = body.response;
|
|
600
|
+
if (!query || !responseText) {
|
|
601
|
+
return new APIResponse({ status: 400, error: "'query' and 'response' are required" });
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
const { ResponseScorer } = await import('../memory/evolution.js');
|
|
605
|
+
const scorer = new ResponseScorer();
|
|
606
|
+
const scored = scorer.score({ query, response: responseText, mode: body.mode || '' });
|
|
607
|
+
return new APIResponse({ data: { score: scored.score, votes: scored.votes, feedback: scored.feedback } });
|
|
608
|
+
} catch {
|
|
609
|
+
return new APIResponse({ status: 500, error: 'scorer not available' });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function handleLeaderboard() {
|
|
614
|
+
try {
|
|
615
|
+
const { SkillLeaderboard } = await import('../autonomous/leaderboard.js');
|
|
616
|
+
const lb = new SkillLeaderboard();
|
|
617
|
+
const top = lb.getTopSkills(20);
|
|
618
|
+
const result = {
|
|
619
|
+
top_skills: (top || []).map((e) => ({
|
|
620
|
+
rank: e.rank,
|
|
621
|
+
path: e.skillPath,
|
|
622
|
+
score: e.score,
|
|
623
|
+
invocations: e.invocations,
|
|
624
|
+
trend: e.trend,
|
|
625
|
+
})),
|
|
626
|
+
total_tracked: top?.length || 0,
|
|
627
|
+
};
|
|
628
|
+
lb.close();
|
|
629
|
+
return new APIResponse({ data: result });
|
|
630
|
+
} catch {
|
|
631
|
+
return new APIResponse({ data: { top_skills: [], total_tracked: 0 } });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function handleLeaderboardDashboard() {
|
|
636
|
+
try {
|
|
637
|
+
const { SkillLeaderboard } = await import('../autonomous/leaderboard.js');
|
|
638
|
+
const lb = new SkillLeaderboard();
|
|
639
|
+
const dashboard = lb.getDashboard();
|
|
640
|
+
lb.close();
|
|
641
|
+
return new APIResponse({ data: dashboard });
|
|
642
|
+
} catch {
|
|
643
|
+
return new APIResponse({ data: {} });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function handleWorkflow({ body }) {
|
|
648
|
+
try {
|
|
649
|
+
const { WorkflowGenerator } = await import('../pipeline/github-actions.js');
|
|
650
|
+
const gen = new WorkflowGenerator();
|
|
651
|
+
const profile = body.profile || 'standard';
|
|
652
|
+
const yaml = gen.generateWorkflow({ scanProfile: profile });
|
|
653
|
+
return new APIResponse({ data: { profile, workflow: yaml } });
|
|
654
|
+
} catch {
|
|
655
|
+
return new APIResponse({ status: 500, error: 'workflow generator not available' });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// -- Server creation -------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
const server = httpCreateServer(handleRequest);
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
server,
|
|
665
|
+
config,
|
|
666
|
+
auth,
|
|
667
|
+
rateLimiter,
|
|
668
|
+
/** Start listening. Returns the actual port (useful with port 0). */
|
|
669
|
+
start() {
|
|
670
|
+
return new Promise((resolve, reject) => {
|
|
671
|
+
server.listen(config.port, config.host, () => {
|
|
672
|
+
const addr = server.address();
|
|
673
|
+
resolve(addr.port);
|
|
674
|
+
});
|
|
675
|
+
server.once('error', reject);
|
|
676
|
+
});
|
|
677
|
+
},
|
|
678
|
+
/** Stop the server. */
|
|
679
|
+
stop() {
|
|
680
|
+
return new Promise((resolve) => {
|
|
681
|
+
server.close(() => resolve());
|
|
682
|
+
});
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|