cipher-security 2.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 +566 -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 +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -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 +991 -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 +288 -0
- package/package.json +31 -0
|
@@ -0,0 +1,880 @@
|
|
|
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
|
+
* CIPHER Scanning Pipeline — Nuclei/Katana integration layer.
|
|
7
|
+
*
|
|
8
|
+
* Defines all shared data classes (Finding, ScanResult, CrawlResult,
|
|
9
|
+
* PipelineResult, ScanProfile, ScanDomain) and the core subprocess
|
|
10
|
+
* spawning pattern for the pipeline module. Every other pipeline module
|
|
11
|
+
* references these types.
|
|
12
|
+
*
|
|
13
|
+
* Ported from pipeline/scanner.py (845 LOC Python).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TIMEOUT = 600; // 10 minutes
|
|
23
|
+
const TOOL_MISSING = (tool, path) =>
|
|
24
|
+
`${tool} not found at '${path}'. Install: https://github.com/projectdiscovery/${tool}#installation`;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Enums
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** @enum {string} CIPHER operating domains mapped to scan profiles. */
|
|
31
|
+
const ScanDomain = Object.freeze({
|
|
32
|
+
RED: 'red',
|
|
33
|
+
BLUE: 'blue',
|
|
34
|
+
RECON: 'recon',
|
|
35
|
+
PRIVACY: 'privacy',
|
|
36
|
+
ARCHITECT: 'architect',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Profile configs
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const PROFILE_CONFIGS = Object.freeze({
|
|
44
|
+
red: {
|
|
45
|
+
name: 'red-team',
|
|
46
|
+
tags: ['cve', 'rce', 'sqli', 'xss', 'ssrf', 'lfi', 'auth-bypass'],
|
|
47
|
+
severity: ['critical', 'high', 'medium'],
|
|
48
|
+
rateLimit: 100,
|
|
49
|
+
bulkSize: 15,
|
|
50
|
+
headless: true,
|
|
51
|
+
},
|
|
52
|
+
blue: {
|
|
53
|
+
name: 'hardening-audit',
|
|
54
|
+
tags: ['misconfig', 'exposure', 'default-login', 'unauth'],
|
|
55
|
+
severity: ['critical', 'high', 'medium', 'low'],
|
|
56
|
+
rateLimit: 200,
|
|
57
|
+
bulkSize: 30,
|
|
58
|
+
headless: false,
|
|
59
|
+
},
|
|
60
|
+
recon: {
|
|
61
|
+
name: 'recon-sweep',
|
|
62
|
+
tags: ['tech', 'token', 'exposure', 'dns'],
|
|
63
|
+
severity: ['info', 'low', 'medium'],
|
|
64
|
+
rateLimit: 250,
|
|
65
|
+
bulkSize: 50,
|
|
66
|
+
headless: false,
|
|
67
|
+
},
|
|
68
|
+
privacy: {
|
|
69
|
+
name: 'privacy-audit',
|
|
70
|
+
tags: ['exposure', 'token', 'misconfig', 'unauth'],
|
|
71
|
+
severity: ['critical', 'high', 'medium'],
|
|
72
|
+
rateLimit: 150,
|
|
73
|
+
bulkSize: 25,
|
|
74
|
+
headless: false,
|
|
75
|
+
},
|
|
76
|
+
pentest: {
|
|
77
|
+
name: 'pentest',
|
|
78
|
+
tags: ['cve', 'rce', 'sqli', 'xss', 'ssrf', 'misconfig'],
|
|
79
|
+
severity: ['critical', 'high', 'medium'],
|
|
80
|
+
rateLimit: 150,
|
|
81
|
+
bulkSize: 25,
|
|
82
|
+
headless: false,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Data classes
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Single vulnerability finding from Nuclei.
|
|
92
|
+
*/
|
|
93
|
+
class Finding {
|
|
94
|
+
constructor(opts = {}) {
|
|
95
|
+
this.templateId = opts.templateId ?? '';
|
|
96
|
+
this.name = opts.name ?? '';
|
|
97
|
+
this.severity = opts.severity ?? '';
|
|
98
|
+
this.host = opts.host ?? '';
|
|
99
|
+
this.matchedAt = opts.matchedAt ?? '';
|
|
100
|
+
this.matcherName = opts.matcherName ?? '';
|
|
101
|
+
this.description = opts.description ?? '';
|
|
102
|
+
this.reference = opts.reference ?? [];
|
|
103
|
+
this.tags = opts.tags ?? [];
|
|
104
|
+
this.cveIds = opts.cveIds ?? [];
|
|
105
|
+
this.cweIds = opts.cweIds ?? [];
|
|
106
|
+
this.raw = opts.raw ?? {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a single Nuclei JSONL object.
|
|
111
|
+
* @param {object} data - Parsed JSON object from Nuclei output
|
|
112
|
+
* @returns {Finding}
|
|
113
|
+
*/
|
|
114
|
+
static fromNucleiJson(data) {
|
|
115
|
+
const info = data.info ?? {};
|
|
116
|
+
const classif = info.classification ?? {};
|
|
117
|
+
return new Finding({
|
|
118
|
+
templateId: data['template-id'] ?? data.template_id ?? '',
|
|
119
|
+
name: info.name ?? '',
|
|
120
|
+
severity: info.severity ?? 'unknown',
|
|
121
|
+
host: data.host ?? '',
|
|
122
|
+
matchedAt: data['matched-at'] ?? data.matched_at ?? '',
|
|
123
|
+
matcherName: data['matcher-name'] ?? data.matcher_name ?? '',
|
|
124
|
+
description: info.description ?? '',
|
|
125
|
+
reference: info.reference ?? [],
|
|
126
|
+
tags: info.tags ?? [],
|
|
127
|
+
cveIds: classif['cve-id'] ?? [],
|
|
128
|
+
cweIds: (classif['cwe-id'] ?? []).map(String),
|
|
129
|
+
raw: data,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Aggregated result from a Nuclei scan.
|
|
136
|
+
*/
|
|
137
|
+
class ScanResult {
|
|
138
|
+
constructor(opts = {}) {
|
|
139
|
+
this.target = opts.target ?? '';
|
|
140
|
+
this.findings = opts.findings ?? [];
|
|
141
|
+
this.stats = opts.stats ?? {};
|
|
142
|
+
this.errors = opts.errors ?? [];
|
|
143
|
+
this.command = opts.command ?? '';
|
|
144
|
+
this.durationSeconds = opts.durationSeconds ?? 0;
|
|
145
|
+
this.success = opts.success ?? false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get criticalCount() {
|
|
149
|
+
return this.findings.filter(f => f.severity === 'critical').length;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get highCount() {
|
|
153
|
+
return this.findings.filter(f => f.severity === 'high').length;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Export scan results as SARIF v2.1.0 JSON string.
|
|
158
|
+
* Lazy-imports ./sarif.js — may not exist until T03.
|
|
159
|
+
* @returns {Promise<string>}
|
|
160
|
+
*/
|
|
161
|
+
async toSarif() {
|
|
162
|
+
const { SarifReport } = await import('./sarif.js');
|
|
163
|
+
const report = new SarifReport();
|
|
164
|
+
for (const f of this.findings) {
|
|
165
|
+
report.addFinding({
|
|
166
|
+
templateId: f.templateId,
|
|
167
|
+
name: f.name,
|
|
168
|
+
description: f.description,
|
|
169
|
+
severity: f.severity,
|
|
170
|
+
host: f.host,
|
|
171
|
+
matchedAt: f.matchedAt,
|
|
172
|
+
tags: f.tags,
|
|
173
|
+
cveIds: f.cveIds,
|
|
174
|
+
reference: f.reference,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return report.toJson();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Auto-generate nuclei templates from critical/high findings.
|
|
182
|
+
* Lazy-imports ./template-manager.js — may not exist until T03.
|
|
183
|
+
* @returns {Promise<string[]>}
|
|
184
|
+
*/
|
|
185
|
+
async generateCustomTemplates() {
|
|
186
|
+
const { NucleiTemplateManager } = await import('./template-manager.js');
|
|
187
|
+
const mgr = new NucleiTemplateManager();
|
|
188
|
+
const templates = [];
|
|
189
|
+
for (const f of this.findings) {
|
|
190
|
+
if (f.severity === 'critical' || f.severity === 'high') {
|
|
191
|
+
let path = '/';
|
|
192
|
+
if (f.matchedAt && f.matchedAt.includes('//')) {
|
|
193
|
+
const afterScheme = f.matchedAt.split('//').slice(1).join('//');
|
|
194
|
+
const parts = afterScheme.split('/');
|
|
195
|
+
path = parts.length > 1 ? '/' + parts.slice(1).join('/') : '/';
|
|
196
|
+
}
|
|
197
|
+
const templateYaml = mgr.generateTemplate({
|
|
198
|
+
title: f.name,
|
|
199
|
+
description: f.description,
|
|
200
|
+
severity: f.severity,
|
|
201
|
+
path,
|
|
202
|
+
matchers: f.name ? [f.name] : [],
|
|
203
|
+
});
|
|
204
|
+
templates.push(templateYaml);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return templates;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Aggregated result from a Katana crawl.
|
|
213
|
+
*/
|
|
214
|
+
class CrawlResult {
|
|
215
|
+
constructor(opts = {}) {
|
|
216
|
+
this.target = opts.target ?? '';
|
|
217
|
+
this.urls = opts.urls ?? [];
|
|
218
|
+
this.endpoints = opts.endpoints ?? [];
|
|
219
|
+
this.forms = opts.forms ?? [];
|
|
220
|
+
this.jsFiles = opts.jsFiles ?? [];
|
|
221
|
+
this.errors = opts.errors ?? [];
|
|
222
|
+
this.command = opts.command ?? '';
|
|
223
|
+
this.success = opts.success ?? false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Combined result from a full scan pipeline run.
|
|
229
|
+
*/
|
|
230
|
+
class PipelineResult {
|
|
231
|
+
constructor(opts = {}) {
|
|
232
|
+
this.target = opts.target ?? '';
|
|
233
|
+
this.crawl = opts.crawl ?? null;
|
|
234
|
+
this.scan = opts.scan ?? null;
|
|
235
|
+
this.storedEntryIds = opts.storedEntryIds ?? [];
|
|
236
|
+
this.errors = opts.errors ?? [];
|
|
237
|
+
this.success = opts.success ?? false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
get urlsCrawled() {
|
|
241
|
+
return this.crawl ? this.crawl.urls.length : 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
get findingsCount() {
|
|
245
|
+
return this.scan ? this.scan.findings.length : 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
get findingsStored() {
|
|
249
|
+
return this.storedEntryIds.length;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
get duration() {
|
|
253
|
+
return this.scan ? this.scan.durationSeconds : 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
get findings() {
|
|
257
|
+
return this.scan ? this.scan.findings : [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// ScanProfile
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
class ScanProfile {
|
|
266
|
+
constructor(opts = {}) {
|
|
267
|
+
this.name = opts.name ?? 'pentest';
|
|
268
|
+
this.tags = opts.tags ?? [];
|
|
269
|
+
this.severity = opts.severity ?? [];
|
|
270
|
+
this.rateLimit = opts.rateLimit ?? 150;
|
|
271
|
+
this.bulkSize = opts.bulkSize ?? 25;
|
|
272
|
+
this.headless = opts.headless ?? false;
|
|
273
|
+
this.extraArgs = opts.extraArgs ?? [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Build a ScanProfile from a CIPHER domain name.
|
|
278
|
+
* Falls back to 'pentest' profile for unknown domains.
|
|
279
|
+
* @param {string} domain
|
|
280
|
+
* @returns {ScanProfile}
|
|
281
|
+
*/
|
|
282
|
+
static fromDomain(domain) {
|
|
283
|
+
const key = (domain ?? '').toLowerCase().trim();
|
|
284
|
+
const cfg = PROFILE_CONFIGS[key] ?? PROFILE_CONFIGS.pentest;
|
|
285
|
+
return new ScanProfile({
|
|
286
|
+
name: cfg.name,
|
|
287
|
+
tags: [...cfg.tags],
|
|
288
|
+
severity: [...cfg.severity],
|
|
289
|
+
rateLimit: cfg.rateLimit,
|
|
290
|
+
bulkSize: cfg.bulkSize,
|
|
291
|
+
headless: cfg.headless,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// NucleiRunner
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Check if a binary is available on PATH.
|
|
302
|
+
* @param {string} binary
|
|
303
|
+
* @returns {string|null} Resolved path or null
|
|
304
|
+
*/
|
|
305
|
+
function whichSync(binary) {
|
|
306
|
+
try {
|
|
307
|
+
return execFileSync('which', [binary], { encoding: 'utf8' }).trim() || null;
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class NucleiRunner {
|
|
314
|
+
/**
|
|
315
|
+
* @param {object} opts
|
|
316
|
+
* @param {string} [opts.nucleiPath='nuclei']
|
|
317
|
+
* @param {string} [opts.templatesDir='']
|
|
318
|
+
* @param {string} [opts.outputDir='']
|
|
319
|
+
*/
|
|
320
|
+
constructor(opts = {}) {
|
|
321
|
+
this.nucleiPath = opts.nucleiPath ?? 'nuclei';
|
|
322
|
+
this.templatesDir = opts.templatesDir ?? '';
|
|
323
|
+
this.outputDir = opts.outputDir ?? '';
|
|
324
|
+
this._resolved = whichSync(this.nucleiPath);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get available() {
|
|
328
|
+
return this._resolved !== null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_fail(target) {
|
|
332
|
+
return new ScanResult({
|
|
333
|
+
target,
|
|
334
|
+
success: false,
|
|
335
|
+
errors: [TOOL_MISSING('nuclei', this.nucleiPath)],
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Run Nuclei against a target using a named scan profile.
|
|
341
|
+
* @param {string} target
|
|
342
|
+
* @param {object} opts
|
|
343
|
+
* @param {string|ScanProfile} [opts.profile='pentest']
|
|
344
|
+
* @param {string[]} [opts.tags]
|
|
345
|
+
* @param {string[]} [opts.severity]
|
|
346
|
+
* @param {number} [opts.timeout=600]
|
|
347
|
+
* @returns {Promise<ScanResult>}
|
|
348
|
+
*/
|
|
349
|
+
scan(target, opts = {}) {
|
|
350
|
+
if (!this.available) return Promise.resolve(this._fail(target));
|
|
351
|
+
|
|
352
|
+
const profile = opts.profile ?? 'pentest';
|
|
353
|
+
const sp = typeof profile === 'string' ? ScanProfile.fromDomain(profile) : profile;
|
|
354
|
+
const effTags = [...new Set([...(opts.tags ?? []), ...sp.tags])];
|
|
355
|
+
const effSev = opts.severity ?? sp.severity;
|
|
356
|
+
|
|
357
|
+
const bin = this._resolved || this.nucleiPath;
|
|
358
|
+
const cmd = [bin, '-jsonl', '-silent'];
|
|
359
|
+
if (this.templatesDir) cmd.push('-t', this.templatesDir);
|
|
360
|
+
cmd.push('-u', target);
|
|
361
|
+
if (effTags.length) cmd.push('-tags', effTags.join(','));
|
|
362
|
+
if (effSev.length) cmd.push('-severity', effSev.join(','));
|
|
363
|
+
cmd.push('-rl', String(sp.rateLimit), '-bs', String(sp.bulkSize));
|
|
364
|
+
if (sp.headless) cmd.push('-headless');
|
|
365
|
+
|
|
366
|
+
return this._execute(cmd, target, opts.timeout ?? DEFAULT_TIMEOUT);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Run Nuclei with explicit template paths.
|
|
371
|
+
* @param {string} target
|
|
372
|
+
* @param {object} opts
|
|
373
|
+
* @param {string[]} [opts.templatePaths]
|
|
374
|
+
* @param {number} [opts.timeout=600]
|
|
375
|
+
* @returns {Promise<ScanResult>}
|
|
376
|
+
*/
|
|
377
|
+
scanWithTemplates(target, opts = {}) {
|
|
378
|
+
if (!this.available) return Promise.resolve(this._fail(target));
|
|
379
|
+
const bin = this._resolved || this.nucleiPath;
|
|
380
|
+
const cmd = [bin, '-jsonl', '-silent', '-u', target];
|
|
381
|
+
for (const tp of opts.templatePaths ?? []) {
|
|
382
|
+
cmd.push('-t', tp);
|
|
383
|
+
}
|
|
384
|
+
return this._execute(cmd, target, opts.timeout ?? DEFAULT_TIMEOUT);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Enumerate available Nuclei templates.
|
|
389
|
+
* @param {object} [opts]
|
|
390
|
+
* @param {string[]} [opts.tags]
|
|
391
|
+
* @param {string[]} [opts.severity]
|
|
392
|
+
* @returns {object[]}
|
|
393
|
+
*/
|
|
394
|
+
listTemplates(opts = {}) {
|
|
395
|
+
if (!this.available) return [];
|
|
396
|
+
const bin = this._resolved || this.nucleiPath;
|
|
397
|
+
const cmd = [bin, '-tl', '-jsonl'];
|
|
398
|
+
if (opts.tags?.length) cmd.push('-tags', opts.tags.join(','));
|
|
399
|
+
if (opts.severity?.length) cmd.push('-severity', opts.severity.join(','));
|
|
400
|
+
try {
|
|
401
|
+
const out = execFileSync(cmd[0], cmd.slice(1), {
|
|
402
|
+
encoding: 'utf8',
|
|
403
|
+
timeout: 120_000,
|
|
404
|
+
});
|
|
405
|
+
const results = [];
|
|
406
|
+
for (const line of out.trim().split('\n')) {
|
|
407
|
+
const trimmed = line.trim();
|
|
408
|
+
if (!trimmed) continue;
|
|
409
|
+
if (trimmed.startsWith('{')) {
|
|
410
|
+
try {
|
|
411
|
+
results.push(JSON.parse(trimmed));
|
|
412
|
+
} catch { /* skip malformed */ }
|
|
413
|
+
} else {
|
|
414
|
+
results.push({ id: trimmed });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return results;
|
|
418
|
+
} catch {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Validate Nuclei template syntax in a directory.
|
|
425
|
+
* @param {string} templateDir
|
|
426
|
+
* @returns {{ valid: boolean, count: number, errors: string[] }}
|
|
427
|
+
*/
|
|
428
|
+
validateTemplates(templateDir) {
|
|
429
|
+
if (!this.available) {
|
|
430
|
+
return { valid: false, count: 0, errors: [TOOL_MISSING('nuclei', this.nucleiPath)] };
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const bin = this._resolved || this.nucleiPath;
|
|
434
|
+
const result = execFileSync(bin, ['-validate', '-t', templateDir], {
|
|
435
|
+
encoding: 'utf8',
|
|
436
|
+
timeout: 120_000,
|
|
437
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
438
|
+
});
|
|
439
|
+
const lines = result.trim().split('\n').filter(l => l.trim());
|
|
440
|
+
return { valid: true, count: lines.length, errors: [] };
|
|
441
|
+
} catch (err) {
|
|
442
|
+
const stderr = err.stderr?.toString() ?? '';
|
|
443
|
+
const errors = stderr.split('\n').filter(ln => ln.toLowerCase().includes('error'));
|
|
444
|
+
return { valid: false, count: 0, errors };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Execute a Nuclei command, streaming JSONL from stdout.
|
|
450
|
+
* Replaces Python's select.select with Node.js readline + event streams.
|
|
451
|
+
* @param {string[]} cmd - Full command array
|
|
452
|
+
* @param {string} target
|
|
453
|
+
* @param {number} timeout - Seconds
|
|
454
|
+
* @returns {Promise<ScanResult>}
|
|
455
|
+
*/
|
|
456
|
+
_execute(cmd, target, timeout) {
|
|
457
|
+
const cmdStr = cmd.join(' ');
|
|
458
|
+
const start = Date.now();
|
|
459
|
+
|
|
460
|
+
return new Promise((resolve) => {
|
|
461
|
+
const findings = [];
|
|
462
|
+
const parseErrors = [];
|
|
463
|
+
const stderrLines = [];
|
|
464
|
+
let proc;
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
proc = spawn(cmd[0], cmd.slice(1), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
468
|
+
} catch (err) {
|
|
469
|
+
return resolve(new ScanResult({
|
|
470
|
+
target,
|
|
471
|
+
command: cmdStr,
|
|
472
|
+
errors: [`Failed to execute nuclei: ${err.message}`],
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Handle spawn error (e.g. ENOENT)
|
|
477
|
+
proc.on('error', (err) => {
|
|
478
|
+
resolve(new ScanResult({
|
|
479
|
+
target,
|
|
480
|
+
command: cmdStr,
|
|
481
|
+
findings,
|
|
482
|
+
errors: [`Failed to execute nuclei: ${err.message}`, ...parseErrors],
|
|
483
|
+
}));
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Read stdout line-by-line for JSONL findings (manual line buffer — no readline needed)
|
|
487
|
+
let stdoutBuf = '';
|
|
488
|
+
proc.stdout.on('data', (chunk) => {
|
|
489
|
+
stdoutBuf += chunk.toString();
|
|
490
|
+
let nl;
|
|
491
|
+
while ((nl = stdoutBuf.indexOf('\n')) !== -1) {
|
|
492
|
+
const line = stdoutBuf.slice(0, nl);
|
|
493
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
494
|
+
if (line.startsWith('{')) {
|
|
495
|
+
try {
|
|
496
|
+
findings.push(Finding.fromNucleiJson(JSON.parse(line)));
|
|
497
|
+
} catch (err) {
|
|
498
|
+
parseErrors.push(`Parse error: ${err.message}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Capture stderr
|
|
505
|
+
proc.stderr.on('data', (chunk) => {
|
|
506
|
+
for (const line of chunk.toString().split('\n')) {
|
|
507
|
+
if (line.trim()) stderrLines.push(line);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Timeout guard
|
|
512
|
+
const timer = setTimeout(() => {
|
|
513
|
+
proc.kill('SIGKILL');
|
|
514
|
+
}, timeout * 1000);
|
|
515
|
+
|
|
516
|
+
proc.on('close', (code, signal) => {
|
|
517
|
+
clearTimeout(timer);
|
|
518
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
519
|
+
const stderrErrors = stderrLines.filter(ln => ln.toLowerCase().includes('error'));
|
|
520
|
+
const timedOut = signal === 'SIGKILL';
|
|
521
|
+
|
|
522
|
+
const sevCounts = {};
|
|
523
|
+
for (const f of findings) {
|
|
524
|
+
sevCounts[f.severity] = (sevCounts[f.severity] || 0) + 1;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
resolve(new ScanResult({
|
|
528
|
+
target,
|
|
529
|
+
findings,
|
|
530
|
+
stats: { total: findings.length, ...sevCounts },
|
|
531
|
+
errors: [
|
|
532
|
+
...parseErrors,
|
|
533
|
+
...stderrErrors,
|
|
534
|
+
...(timedOut ? [`Scan timed out after ${timeout}s`] : []),
|
|
535
|
+
],
|
|
536
|
+
command: cmdStr,
|
|
537
|
+
durationSeconds: Math.round(elapsed * 100) / 100,
|
|
538
|
+
success: !timedOut && code === 0,
|
|
539
|
+
}));
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
// KatanaRunner
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
|
|
549
|
+
/** API endpoint path indicators. */
|
|
550
|
+
const API_INDICATORS = ['/api/', '/v1/', '/v2/', '/v3/', '/graphql', '/rest/'];
|
|
551
|
+
|
|
552
|
+
class KatanaRunner {
|
|
553
|
+
/**
|
|
554
|
+
* @param {object} opts
|
|
555
|
+
* @param {string} [opts.katanaPath='katana']
|
|
556
|
+
*/
|
|
557
|
+
constructor(opts = {}) {
|
|
558
|
+
this.katanaPath = opts.katanaPath ?? 'katana';
|
|
559
|
+
this._resolved = whichSync(this.katanaPath);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
get available() {
|
|
563
|
+
return this._resolved !== null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
_fail(target) {
|
|
567
|
+
return new CrawlResult({
|
|
568
|
+
target,
|
|
569
|
+
success: false,
|
|
570
|
+
errors: [TOOL_MISSING('katana', this.katanaPath)],
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Crawl a target with Katana, returning discovered URLs and assets.
|
|
576
|
+
* Handles both JSONL and plain-text output modes.
|
|
577
|
+
* @param {string} target
|
|
578
|
+
* @param {object} opts
|
|
579
|
+
* @param {number} [opts.depth=3]
|
|
580
|
+
* @param {boolean} [opts.headless=false]
|
|
581
|
+
* @param {string} [opts.scope='']
|
|
582
|
+
* @param {boolean} [opts.jsCrawl=true]
|
|
583
|
+
* @param {number} [opts.timeout=600]
|
|
584
|
+
* @returns {Promise<CrawlResult>}
|
|
585
|
+
*/
|
|
586
|
+
crawl(target, opts = {}) {
|
|
587
|
+
if (!this.available) return Promise.resolve(this._fail(target));
|
|
588
|
+
|
|
589
|
+
const depth = opts.depth ?? 3;
|
|
590
|
+
const headless = opts.headless ?? false;
|
|
591
|
+
const scope = opts.scope ?? '';
|
|
592
|
+
const jsCrawl = opts.jsCrawl !== false;
|
|
593
|
+
const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
594
|
+
|
|
595
|
+
const bin = this._resolved || this.katanaPath;
|
|
596
|
+
const cmd = [bin, '-u', target, '-d', String(depth), '-jsonl', '-silent'];
|
|
597
|
+
if (headless) cmd.push('-headless');
|
|
598
|
+
if (scope) cmd.push('-cs', scope);
|
|
599
|
+
if (jsCrawl) cmd.push('-jc');
|
|
600
|
+
|
|
601
|
+
const cmdStr = cmd.join(' ');
|
|
602
|
+
|
|
603
|
+
return new Promise((resolve) => {
|
|
604
|
+
const stdoutLines = [];
|
|
605
|
+
const stderrLines = [];
|
|
606
|
+
let proc;
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
proc = spawn(cmd[0], cmd.slice(1), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
610
|
+
} catch (err) {
|
|
611
|
+
return resolve(new CrawlResult({
|
|
612
|
+
target,
|
|
613
|
+
command: cmdStr,
|
|
614
|
+
errors: [`Failed to execute katana: ${err.message}`],
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
proc.on('error', (err) => {
|
|
619
|
+
resolve(new CrawlResult({
|
|
620
|
+
target,
|
|
621
|
+
command: cmdStr,
|
|
622
|
+
errors: [`Failed to execute katana: ${err.message}`],
|
|
623
|
+
}));
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
let stdoutBuf = '';
|
|
627
|
+
proc.stdout.on('data', (chunk) => {
|
|
628
|
+
stdoutBuf += chunk.toString();
|
|
629
|
+
let nl;
|
|
630
|
+
while ((nl = stdoutBuf.indexOf('\n')) !== -1) {
|
|
631
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
632
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
633
|
+
if (line) stdoutLines.push(line);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
proc.stderr.on('data', (chunk) => {
|
|
638
|
+
for (const line of chunk.toString().split('\n')) {
|
|
639
|
+
if (line.trim()) stderrLines.push(line.trim());
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const timer = setTimeout(() => {
|
|
644
|
+
proc.kill('SIGKILL');
|
|
645
|
+
}, timeout * 1000);
|
|
646
|
+
|
|
647
|
+
proc.on('close', (code) => {
|
|
648
|
+
clearTimeout(timer);
|
|
649
|
+
resolve(this._classifyOutput(stdoutLines, target, cmdStr, code === 0));
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Classify raw output lines into urls, endpoints, js files, forms.
|
|
656
|
+
* Handles both JSONL (preferred) and plain-text output.
|
|
657
|
+
*/
|
|
658
|
+
_classifyOutput(lines, target, command, processSuccess) {
|
|
659
|
+
const urls = [];
|
|
660
|
+
const endpoints = [];
|
|
661
|
+
const forms = [];
|
|
662
|
+
const jsFiles = [];
|
|
663
|
+
|
|
664
|
+
for (const line of lines) {
|
|
665
|
+
let url = line;
|
|
666
|
+
if (line.startsWith('{')) {
|
|
667
|
+
try {
|
|
668
|
+
const entry = JSON.parse(line);
|
|
669
|
+
url = entry.request?.endpoint ?? line;
|
|
670
|
+
if (entry.request?.method?.toUpperCase() === 'POST') {
|
|
671
|
+
forms.push({
|
|
672
|
+
url,
|
|
673
|
+
method: 'POST',
|
|
674
|
+
body: entry.request?.body ?? '',
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
} catch { /* treat as plain text */ }
|
|
678
|
+
}
|
|
679
|
+
urls.push(url);
|
|
680
|
+
const lower = url.toLowerCase();
|
|
681
|
+
if (lower.endsWith('.js') || lower.endsWith('.mjs')) {
|
|
682
|
+
jsFiles.push(url);
|
|
683
|
+
}
|
|
684
|
+
if (API_INDICATORS.some(ind => lower.includes(ind))) {
|
|
685
|
+
endpoints.push(url);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return new CrawlResult({
|
|
690
|
+
target,
|
|
691
|
+
urls: [...new Set(urls)].sort(),
|
|
692
|
+
endpoints: [...new Set(endpoints)].sort(),
|
|
693
|
+
forms,
|
|
694
|
+
jsFiles: [...new Set(jsFiles)].sort(),
|
|
695
|
+
command,
|
|
696
|
+
success: processSuccess,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Crawl and return only API-like endpoints.
|
|
702
|
+
* @param {string} target
|
|
703
|
+
* @param {object} opts
|
|
704
|
+
* @param {number} [opts.timeout=600]
|
|
705
|
+
* @returns {Promise<string[]>}
|
|
706
|
+
*/
|
|
707
|
+
async extractEndpoints(target, opts = {}) {
|
|
708
|
+
const result = await this.crawl(target, { depth: 2, jsCrawl: true, timeout: opts.timeout });
|
|
709
|
+
return result.endpoints;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
// File extension → Nuclei tag mapping for PR scans
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
const EXT_TAG_MAP = Object.freeze({
|
|
718
|
+
'.yaml': ['misconfig'],
|
|
719
|
+
'.yml': ['misconfig'],
|
|
720
|
+
'.json': ['misconfig', 'exposure'],
|
|
721
|
+
'.toml': ['misconfig'],
|
|
722
|
+
'.env': ['exposure', 'token'],
|
|
723
|
+
'.py': ['sqli', 'xss', 'ssrf', 'rce'],
|
|
724
|
+
'.js': ['sqli', 'xss', 'ssrf', 'rce', 'prototype-pollution'],
|
|
725
|
+
'.ts': ['sqli', 'xss', 'ssrf', 'rce'],
|
|
726
|
+
'.go': ['sqli', 'ssrf', 'rce'],
|
|
727
|
+
'.java': ['sqli', 'xss', 'ssrf', 'rce', 'deserialization'],
|
|
728
|
+
'.php': ['sqli', 'xss', 'ssrf', 'rce', 'lfi'],
|
|
729
|
+
'.rb': ['sqli', 'xss', 'ssrf', 'rce'],
|
|
730
|
+
'.tf': ['misconfig', 'exposure'],
|
|
731
|
+
'.dockerfile': ['misconfig'],
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
// ScanPipeline
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
|
|
738
|
+
class ScanPipeline {
|
|
739
|
+
/**
|
|
740
|
+
* Orchestrates Katana crawl → Nuclei scan → memory storage.
|
|
741
|
+
* @param {object} opts
|
|
742
|
+
* @param {NucleiRunner} opts.nucleiRunner
|
|
743
|
+
* @param {KatanaRunner} opts.katanaRunner
|
|
744
|
+
* @param {function} [opts.memoryCallback] - fn(findings, target, context) => string[]
|
|
745
|
+
*/
|
|
746
|
+
constructor(opts = {}) {
|
|
747
|
+
this.nucleiRunner = opts.nucleiRunner;
|
|
748
|
+
this.katanaRunner = opts.katanaRunner;
|
|
749
|
+
this.memoryCallback = opts.memoryCallback ?? null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Full pipeline: crawl → scan → store findings.
|
|
754
|
+
* @param {string} target
|
|
755
|
+
* @param {object} opts
|
|
756
|
+
* @param {string} [opts.profile='pentest']
|
|
757
|
+
* @returns {Promise<PipelineResult>}
|
|
758
|
+
*/
|
|
759
|
+
async fullScan(target, opts = {}) {
|
|
760
|
+
const profile = opts.profile ?? 'pentest';
|
|
761
|
+
const errors = [];
|
|
762
|
+
|
|
763
|
+
const crawlResult = await this.katanaRunner.crawl(target);
|
|
764
|
+
if (!crawlResult.success) {
|
|
765
|
+
errors.push(...crawlResult.errors);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const scanResult = await this.nucleiRunner.scan(target, { profile });
|
|
769
|
+
if (!scanResult.success) {
|
|
770
|
+
errors.push(...scanResult.errors);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const stored = this._storeFindings(scanResult.findings, target, 'full_scan');
|
|
774
|
+
|
|
775
|
+
return new PipelineResult({
|
|
776
|
+
target,
|
|
777
|
+
crawl: crawlResult,
|
|
778
|
+
scan: scanResult,
|
|
779
|
+
storedEntryIds: stored,
|
|
780
|
+
errors,
|
|
781
|
+
success: scanResult.success,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Scan PR changes — derives Nuclei tags from changed file extensions.
|
|
787
|
+
* @param {string} repoUrl
|
|
788
|
+
* @param {number} prNumber
|
|
789
|
+
* @param {string[]} changedFiles
|
|
790
|
+
* @returns {Promise<PipelineResult>}
|
|
791
|
+
*/
|
|
792
|
+
async prScan(repoUrl, prNumber, changedFiles) {
|
|
793
|
+
const tags = new Set();
|
|
794
|
+
for (const fpath of changedFiles) {
|
|
795
|
+
const ext = extname(fpath).toLowerCase();
|
|
796
|
+
const mapped = EXT_TAG_MAP[ext];
|
|
797
|
+
if (mapped) mapped.forEach(t => tags.add(t));
|
|
798
|
+
const basename = fpath.split('/').pop().toLowerCase();
|
|
799
|
+
if (basename === 'dockerfile' || basename === 'docker-compose.yml') {
|
|
800
|
+
tags.add('misconfig');
|
|
801
|
+
tags.add('exposure');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const effTags = tags.size > 0 ? [...tags].sort() : ['cve', 'misconfig'];
|
|
805
|
+
|
|
806
|
+
const scanResult = await this.nucleiRunner.scan(repoUrl, { profile: 'pentest', tags: effTags });
|
|
807
|
+
const stored = this._storeFindings(scanResult.findings, repoUrl, `pr_scan:PR-${prNumber}`);
|
|
808
|
+
|
|
809
|
+
return new PipelineResult({
|
|
810
|
+
target: repoUrl,
|
|
811
|
+
scan: scanResult,
|
|
812
|
+
storedEntryIds: stored,
|
|
813
|
+
errors: scanResult.errors,
|
|
814
|
+
success: scanResult.success,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Re-run scans for prior engagement findings to verify remediation.
|
|
820
|
+
* @param {string} engagementId
|
|
821
|
+
* @returns {Promise<PipelineResult>}
|
|
822
|
+
*/
|
|
823
|
+
async regressionScan(engagementId) {
|
|
824
|
+
// Regression scan requires memory to load prior findings
|
|
825
|
+
return new PipelineResult({
|
|
826
|
+
target: engagementId,
|
|
827
|
+
success: false,
|
|
828
|
+
errors: ['Regression scan requires memory integration — not yet implemented in Node.js'],
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Persist findings via the memory callback if provided.
|
|
834
|
+
* @param {Finding[]} findings
|
|
835
|
+
* @param {string} target
|
|
836
|
+
* @param {string} context
|
|
837
|
+
* @returns {string[]} Stored entry IDs
|
|
838
|
+
*/
|
|
839
|
+
_storeFindings(findings, target, context) {
|
|
840
|
+
if (!this.memoryCallback || !findings.length) return [];
|
|
841
|
+
try {
|
|
842
|
+
return this.memoryCallback(findings, target, context);
|
|
843
|
+
} catch {
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Extract file extension from a path.
|
|
851
|
+
* @param {string} filePath
|
|
852
|
+
* @returns {string}
|
|
853
|
+
*/
|
|
854
|
+
function extname(filePath) {
|
|
855
|
+
const idx = filePath.lastIndexOf('.');
|
|
856
|
+
return idx >= 0 ? filePath.slice(idx) : '';
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ---------------------------------------------------------------------------
|
|
860
|
+
// Exports
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
|
|
863
|
+
export {
|
|
864
|
+
// Enums
|
|
865
|
+
ScanDomain,
|
|
866
|
+
PROFILE_CONFIGS,
|
|
867
|
+
EXT_TAG_MAP,
|
|
868
|
+
// Data classes
|
|
869
|
+
Finding,
|
|
870
|
+
ScanResult,
|
|
871
|
+
CrawlResult,
|
|
872
|
+
PipelineResult,
|
|
873
|
+
ScanProfile,
|
|
874
|
+
// Runners
|
|
875
|
+
NucleiRunner,
|
|
876
|
+
KatanaRunner,
|
|
877
|
+
ScanPipeline,
|
|
878
|
+
// Utilities
|
|
879
|
+
whichSync,
|
|
880
|
+
};
|