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,510 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Async scanning pipeline for non-blocking, Promise-based scans.
|
|
7
|
+
*
|
|
8
|
+
* Provides AsyncNucleiRunner and AsyncKatanaRunner that wrap
|
|
9
|
+
* child_process.spawn in Promise APIs, plus AsyncScanManager with
|
|
10
|
+
* semaphore-based concurrency control.
|
|
11
|
+
*
|
|
12
|
+
* Ported from pipeline/async_scanner.py (331 LOC Python).
|
|
13
|
+
* Python used asyncio — Node.js uses Promises + spawn.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { randomBytes } from 'node:crypto';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Enums
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** @enum {string} Status of an async scan job. */
|
|
24
|
+
const AsyncScanStatus = Object.freeze({
|
|
25
|
+
QUEUED: 'queued',
|
|
26
|
+
RUNNING: 'running',
|
|
27
|
+
COMPLETED: 'completed',
|
|
28
|
+
FAILED: 'failed',
|
|
29
|
+
CANCELLED: 'cancelled',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Data classes
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A finding from an async scan.
|
|
38
|
+
*/
|
|
39
|
+
class AsyncFinding {
|
|
40
|
+
constructor(opts = {}) {
|
|
41
|
+
this.id = opts.id ?? '';
|
|
42
|
+
this.name = opts.name ?? '';
|
|
43
|
+
this.severity = opts.severity ?? 'info';
|
|
44
|
+
this.url = opts.url ?? '';
|
|
45
|
+
this.description = opts.description ?? '';
|
|
46
|
+
this.matchedAt = opts.matchedAt ?? '';
|
|
47
|
+
this.template = opts.template ?? '';
|
|
48
|
+
this.timestamp = opts.timestamp ?? 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Result of an async scan job.
|
|
54
|
+
*/
|
|
55
|
+
class AsyncScanResult {
|
|
56
|
+
constructor(opts = {}) {
|
|
57
|
+
this.scanId = opts.scanId ?? '';
|
|
58
|
+
this.target = opts.target ?? '';
|
|
59
|
+
this.status = opts.status ?? AsyncScanStatus.QUEUED;
|
|
60
|
+
this.findings = opts.findings ?? [];
|
|
61
|
+
this.startedAt = opts.startedAt ?? 0;
|
|
62
|
+
this.completedAt = opts.completedAt ?? 0;
|
|
63
|
+
this.error = opts.error ?? '';
|
|
64
|
+
this.profile = opts.profile ?? 'standard';
|
|
65
|
+
this.totalRequests = opts.totalRequests ?? 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Semaphore
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Promise-based semaphore for concurrency control.
|
|
75
|
+
* acquire() returns a Promise that resolves when a slot is available.
|
|
76
|
+
* release() frees a slot and resolves the next queued waiter.
|
|
77
|
+
*/
|
|
78
|
+
class Semaphore {
|
|
79
|
+
/**
|
|
80
|
+
* @param {number} max - Maximum concurrent slots
|
|
81
|
+
*/
|
|
82
|
+
constructor(max) {
|
|
83
|
+
this._max = max;
|
|
84
|
+
this._count = 0;
|
|
85
|
+
this._queue = [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Acquire a slot. Resolves immediately if available, otherwise queues.
|
|
90
|
+
* @returns {Promise<void>}
|
|
91
|
+
*/
|
|
92
|
+
acquire() {
|
|
93
|
+
if (this._count < this._max) {
|
|
94
|
+
this._count++;
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
this._queue.push(resolve);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Release a slot. If waiters are queued, the next one is resolved.
|
|
104
|
+
*/
|
|
105
|
+
release() {
|
|
106
|
+
if (this._queue.length > 0) {
|
|
107
|
+
const next = this._queue.shift();
|
|
108
|
+
next();
|
|
109
|
+
} else {
|
|
110
|
+
this._count--;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Current number of active slots. */
|
|
115
|
+
get active() {
|
|
116
|
+
return this._count;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Number of waiters in the queue. */
|
|
120
|
+
get waiting() {
|
|
121
|
+
return this._queue.length;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// whichSync (local utility)
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
import { execFileSync } from 'node:child_process';
|
|
130
|
+
|
|
131
|
+
function whichSync(binary) {
|
|
132
|
+
try {
|
|
133
|
+
return execFileSync('which', [binary], { encoding: 'utf8' }).trim() || null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// AsyncNucleiRunner
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
class AsyncNucleiRunner {
|
|
144
|
+
/**
|
|
145
|
+
* @param {object} opts
|
|
146
|
+
* @param {string} [opts.binary='nuclei']
|
|
147
|
+
* @param {number} [opts.timeout=300]
|
|
148
|
+
*/
|
|
149
|
+
constructor(opts = {}) {
|
|
150
|
+
this.binary = opts.binary ?? 'nuclei';
|
|
151
|
+
this.timeout = opts.timeout ?? 300;
|
|
152
|
+
this._available = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if nuclei binary is in PATH.
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
isAvailable() {
|
|
160
|
+
if (this._available === null) {
|
|
161
|
+
this._available = whichSync(this.binary) !== null;
|
|
162
|
+
}
|
|
163
|
+
return this._available;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run nuclei scan asynchronously.
|
|
168
|
+
* @param {string} target
|
|
169
|
+
* @param {object} opts
|
|
170
|
+
* @param {string} [opts.severity='medium,high,critical']
|
|
171
|
+
* @param {string} [opts.templates='']
|
|
172
|
+
* @param {number} [opts.rateLimit=150]
|
|
173
|
+
* @returns {Promise<AsyncFinding[]>}
|
|
174
|
+
*/
|
|
175
|
+
scan(target, opts = {}) {
|
|
176
|
+
if (!this.isAvailable()) {
|
|
177
|
+
return Promise.reject(new Error('nuclei binary not found in PATH'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const severity = opts.severity ?? 'medium,high,critical';
|
|
181
|
+
const rateLimit = opts.rateLimit ?? 150;
|
|
182
|
+
|
|
183
|
+
const cmd = [
|
|
184
|
+
this.binary, '-target', target,
|
|
185
|
+
'-severity', severity,
|
|
186
|
+
'-rate-limit', String(rateLimit),
|
|
187
|
+
'-jsonl', '-silent',
|
|
188
|
+
];
|
|
189
|
+
if (opts.templates) cmd.push('-t', opts.templates);
|
|
190
|
+
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const findings = [];
|
|
193
|
+
let proc;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
proc = spawn(cmd[0], cmd.slice(1), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return reject(err);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
proc.on('error', reject);
|
|
202
|
+
|
|
203
|
+
let stdoutBuf = '';
|
|
204
|
+
proc.stdout.on('data', (chunk) => {
|
|
205
|
+
stdoutBuf += chunk.toString();
|
|
206
|
+
let nl;
|
|
207
|
+
while ((nl = stdoutBuf.indexOf('\n')) !== -1) {
|
|
208
|
+
const line = stdoutBuf.slice(0, nl);
|
|
209
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
210
|
+
try {
|
|
211
|
+
const data = JSON.parse(line);
|
|
212
|
+
findings.push(new AsyncFinding({
|
|
213
|
+
id: data['template-id'] ?? '',
|
|
214
|
+
name: data.info?.name ?? '',
|
|
215
|
+
severity: data.info?.severity ?? 'info',
|
|
216
|
+
url: data['matched-at'] ?? target,
|
|
217
|
+
description: data.info?.description ?? '',
|
|
218
|
+
matchedAt: data['matched-at'] ?? '',
|
|
219
|
+
template: data.template ?? '',
|
|
220
|
+
timestamp: Date.now() / 1000,
|
|
221
|
+
}));
|
|
222
|
+
} catch { /* skip malformed lines */ }
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const timer = setTimeout(() => {
|
|
227
|
+
proc.kill('SIGKILL');
|
|
228
|
+
}, this.timeout * 1000);
|
|
229
|
+
|
|
230
|
+
proc.on('close', () => {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
resolve(findings);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// AsyncKatanaRunner
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
class AsyncKatanaRunner {
|
|
243
|
+
/**
|
|
244
|
+
* @param {object} opts
|
|
245
|
+
* @param {string} [opts.binary='katana']
|
|
246
|
+
* @param {number} [opts.timeout=120]
|
|
247
|
+
*/
|
|
248
|
+
constructor(opts = {}) {
|
|
249
|
+
this.binary = opts.binary ?? 'katana';
|
|
250
|
+
this.timeout = opts.timeout ?? 120;
|
|
251
|
+
this._available = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
isAvailable() {
|
|
255
|
+
if (this._available === null) {
|
|
256
|
+
this._available = whichSync(this.binary) !== null;
|
|
257
|
+
}
|
|
258
|
+
return this._available;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Run katana crawl asynchronously.
|
|
263
|
+
* @param {string} target
|
|
264
|
+
* @param {object} opts
|
|
265
|
+
* @param {number} [opts.depth=3]
|
|
266
|
+
* @param {number} [opts.concurrency=10]
|
|
267
|
+
* @returns {Promise<string[]>}
|
|
268
|
+
*/
|
|
269
|
+
crawl(target, opts = {}) {
|
|
270
|
+
if (!this.isAvailable()) {
|
|
271
|
+
return Promise.reject(new Error('katana binary not found in PATH'));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const depth = opts.depth ?? 3;
|
|
275
|
+
const concurrency = opts.concurrency ?? 10;
|
|
276
|
+
|
|
277
|
+
const cmd = [
|
|
278
|
+
this.binary, '-u', target,
|
|
279
|
+
'-depth', String(depth),
|
|
280
|
+
'-concurrency', String(concurrency),
|
|
281
|
+
'-silent',
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
const urls = [];
|
|
286
|
+
let proc;
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
proc = spawn(cmd[0], cmd.slice(1), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return reject(err);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
proc.on('error', reject);
|
|
295
|
+
|
|
296
|
+
let stdoutBuf = '';
|
|
297
|
+
proc.stdout.on('data', (chunk) => {
|
|
298
|
+
stdoutBuf += chunk.toString();
|
|
299
|
+
let nl;
|
|
300
|
+
while ((nl = stdoutBuf.indexOf('\n')) !== -1) {
|
|
301
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
302
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
303
|
+
if (line) urls.push(line);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
proc.kill('SIGKILL');
|
|
309
|
+
}, this.timeout * 1000);
|
|
310
|
+
|
|
311
|
+
proc.on('close', () => {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
resolve(urls);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// AsyncScanManager
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Map scan profile name → Nuclei severity filter string.
|
|
325
|
+
*/
|
|
326
|
+
function severityForProfile(profile) {
|
|
327
|
+
const map = {
|
|
328
|
+
quick: 'critical,high',
|
|
329
|
+
standard: 'medium,high,critical',
|
|
330
|
+
deep: 'info,low,medium,high,critical',
|
|
331
|
+
pentest: 'info,low,medium,high,critical',
|
|
332
|
+
};
|
|
333
|
+
return map[profile] ?? 'medium,high,critical';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
class AsyncScanManager {
|
|
337
|
+
/**
|
|
338
|
+
* @param {object} opts
|
|
339
|
+
* @param {number} [opts.maxConcurrent=3]
|
|
340
|
+
*/
|
|
341
|
+
constructor(opts = {}) {
|
|
342
|
+
this.maxConcurrent = opts.maxConcurrent ?? 3;
|
|
343
|
+
this._jobs = new Map();
|
|
344
|
+
this._semaphore = new Semaphore(this.maxConcurrent);
|
|
345
|
+
this._nuclei = new AsyncNucleiRunner();
|
|
346
|
+
this._katana = new AsyncKatanaRunner();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Submit a scan job. Returns immediately with QUEUED status.
|
|
351
|
+
* @param {string} scanId
|
|
352
|
+
* @param {string} target
|
|
353
|
+
* @param {object} opts
|
|
354
|
+
* @param {string} [opts.profile='standard']
|
|
355
|
+
* @returns {AsyncScanResult}
|
|
356
|
+
*/
|
|
357
|
+
submitScan(scanId, target, opts = {}) {
|
|
358
|
+
const result = new AsyncScanResult({
|
|
359
|
+
scanId,
|
|
360
|
+
target,
|
|
361
|
+
profile: opts.profile ?? 'standard',
|
|
362
|
+
});
|
|
363
|
+
this._jobs.set(scanId, result);
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Execute a scan job asynchronously, respecting the semaphore.
|
|
369
|
+
* @param {string} scanId
|
|
370
|
+
* @returns {Promise<AsyncScanResult>}
|
|
371
|
+
*/
|
|
372
|
+
async runScan(scanId) {
|
|
373
|
+
const result = this._jobs.get(scanId);
|
|
374
|
+
if (!result) throw new Error(`Scan ${scanId} not found`);
|
|
375
|
+
|
|
376
|
+
await this._semaphore.acquire();
|
|
377
|
+
try {
|
|
378
|
+
// Check if cancelled while waiting
|
|
379
|
+
if (result.status === AsyncScanStatus.CANCELLED) {
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
result.status = AsyncScanStatus.RUNNING;
|
|
383
|
+
result.startedAt = Date.now() / 1000;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
if (this._nuclei.isAvailable()) {
|
|
387
|
+
const findings = await this._nuclei.scan(result.target, {
|
|
388
|
+
severity: severityForProfile(result.profile),
|
|
389
|
+
});
|
|
390
|
+
result.findings = findings;
|
|
391
|
+
} else {
|
|
392
|
+
result.error = 'nuclei not available';
|
|
393
|
+
}
|
|
394
|
+
result.status = AsyncScanStatus.COMPLETED;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
result.status = AsyncScanStatus.FAILED;
|
|
397
|
+
result.error = err.message;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
result.completedAt = Date.now() / 1000;
|
|
401
|
+
} finally {
|
|
402
|
+
this._semaphore.release();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get current status of a scan job.
|
|
410
|
+
* @param {string} scanId
|
|
411
|
+
* @returns {AsyncScanResult|null}
|
|
412
|
+
*/
|
|
413
|
+
getStatus(scanId) {
|
|
414
|
+
return this._jobs.get(scanId) ?? null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get summary of all jobs.
|
|
419
|
+
* @returns {object}
|
|
420
|
+
*/
|
|
421
|
+
getAllJobs() {
|
|
422
|
+
const out = {};
|
|
423
|
+
for (const [sid, r] of this._jobs) {
|
|
424
|
+
out[sid] = {
|
|
425
|
+
scanId: r.scanId,
|
|
426
|
+
target: r.target,
|
|
427
|
+
status: r.status,
|
|
428
|
+
findingsCount: r.findings.length,
|
|
429
|
+
error: r.error,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Cancel a queued scan.
|
|
437
|
+
* @param {string} scanId
|
|
438
|
+
* @returns {boolean}
|
|
439
|
+
*/
|
|
440
|
+
cancelScan(scanId) {
|
|
441
|
+
const result = this._jobs.get(scanId);
|
|
442
|
+
if (result && result.status === AsyncScanStatus.QUEUED) {
|
|
443
|
+
result.status = AsyncScanStatus.CANCELLED;
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Convenience function
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Run a one-shot async scan and return results.
|
|
456
|
+
* @param {string} target
|
|
457
|
+
* @param {object} opts
|
|
458
|
+
* @param {string} [opts.profile='standard']
|
|
459
|
+
* @param {string} [opts.scanId='']
|
|
460
|
+
* @returns {Promise<object>}
|
|
461
|
+
*/
|
|
462
|
+
async function asyncScan(target, opts = {}) {
|
|
463
|
+
const scanId = opts.scanId || `scan-${randomBytes(4).toString('hex')}`;
|
|
464
|
+
const profile = opts.profile ?? 'standard';
|
|
465
|
+
|
|
466
|
+
const mgr = new AsyncScanManager();
|
|
467
|
+
mgr.submitScan(scanId, target, { profile });
|
|
468
|
+
const result = await mgr.runScan(scanId);
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
scanId: result.scanId,
|
|
472
|
+
target: result.target,
|
|
473
|
+
status: result.status,
|
|
474
|
+
profile: result.profile,
|
|
475
|
+
findings: result.findings.map(f => ({
|
|
476
|
+
id: f.id,
|
|
477
|
+
name: f.name,
|
|
478
|
+
severity: f.severity,
|
|
479
|
+
url: f.url,
|
|
480
|
+
description: f.description,
|
|
481
|
+
})),
|
|
482
|
+
totalFindings: result.findings.length,
|
|
483
|
+
durationS: result.completedAt
|
|
484
|
+
? Math.round((result.completedAt - result.startedAt) * 100) / 100
|
|
485
|
+
: 0,
|
|
486
|
+
error: result.error,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// Exports
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
export {
|
|
495
|
+
// Enums
|
|
496
|
+
AsyncScanStatus,
|
|
497
|
+
// Data classes
|
|
498
|
+
AsyncFinding,
|
|
499
|
+
AsyncScanResult,
|
|
500
|
+
// Runners
|
|
501
|
+
AsyncNucleiRunner,
|
|
502
|
+
AsyncKatanaRunner,
|
|
503
|
+
// Manager
|
|
504
|
+
AsyncScanManager,
|
|
505
|
+
Semaphore,
|
|
506
|
+
// Convenience
|
|
507
|
+
asyncScan,
|
|
508
|
+
// Utility
|
|
509
|
+
severityForProfile,
|
|
510
|
+
};
|