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,525 @@
|
|
|
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 Nuclei Template Manager — pull, version, and manage nuclei templates.
|
|
7
|
+
*
|
|
8
|
+
* Provides template management beyond basic execution:
|
|
9
|
+
* - Pull/update templates from nuclei-templates repository
|
|
10
|
+
* - Custom template generation from scan findings
|
|
11
|
+
* - Template categorization and search
|
|
12
|
+
* - Workflow chain composition
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { join, relative } from 'node:path';
|
|
17
|
+
import { execFileSync } from 'node:child_process';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
// Default template directory
|
|
21
|
+
const DEFAULT_TEMPLATE_DIR =
|
|
22
|
+
process.env.CIPHER_TEMPLATE_DIR ||
|
|
23
|
+
join(homedir(), '.config', 'cipher', 'nuclei-templates');
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Data classes
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
class TemplateInfo {
|
|
30
|
+
/**
|
|
31
|
+
* @param {object} opts
|
|
32
|
+
* @param {string} opts.id
|
|
33
|
+
* @param {string} opts.name
|
|
34
|
+
* @param {string} [opts.severity]
|
|
35
|
+
* @param {string[]} [opts.tags]
|
|
36
|
+
* @param {string} [opts.author]
|
|
37
|
+
* @param {string} [opts.description]
|
|
38
|
+
* @param {string[]} [opts.reference]
|
|
39
|
+
* @param {string} [opts.filePath]
|
|
40
|
+
* @param {string} [opts.protocol]
|
|
41
|
+
* @param {string} [opts.category]
|
|
42
|
+
*/
|
|
43
|
+
constructor(opts = {}) {
|
|
44
|
+
this.id = opts.id || '';
|
|
45
|
+
this.name = opts.name || '';
|
|
46
|
+
this.severity = opts.severity || 'info';
|
|
47
|
+
this.tags = opts.tags || [];
|
|
48
|
+
this.author = opts.author || '';
|
|
49
|
+
this.description = opts.description || '';
|
|
50
|
+
this.reference = opts.reference || [];
|
|
51
|
+
this.filePath = opts.filePath || '';
|
|
52
|
+
this.protocol = opts.protocol || 'http';
|
|
53
|
+
this.category = opts.category || '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
toDict() {
|
|
57
|
+
return {
|
|
58
|
+
id: this.id,
|
|
59
|
+
name: this.name,
|
|
60
|
+
severity: this.severity,
|
|
61
|
+
tags: this.tags,
|
|
62
|
+
author: this.author,
|
|
63
|
+
description: this.description,
|
|
64
|
+
reference: this.reference,
|
|
65
|
+
file_path: this.filePath,
|
|
66
|
+
protocol: this.protocol,
|
|
67
|
+
category: this.category,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class TemplateCollection {
|
|
73
|
+
/**
|
|
74
|
+
* @param {object} opts
|
|
75
|
+
* @param {string} [opts.name]
|
|
76
|
+
* @param {string} [opts.version]
|
|
77
|
+
* @param {TemplateInfo[]} [opts.templates]
|
|
78
|
+
* @param {string} [opts.updatedAt]
|
|
79
|
+
* @param {string} [opts.source]
|
|
80
|
+
*/
|
|
81
|
+
constructor(opts = {}) {
|
|
82
|
+
this.name = opts.name || '';
|
|
83
|
+
this.version = opts.version || '';
|
|
84
|
+
this.templates = opts.templates || [];
|
|
85
|
+
this.updatedAt = opts.updatedAt || new Date().toISOString();
|
|
86
|
+
this.source = opts.source || '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get count() {
|
|
90
|
+
return this.templates.length;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
bySeverity(severity) {
|
|
94
|
+
return this.templates.filter((t) => t.severity === severity);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
byTag(tag) {
|
|
98
|
+
return this.templates.filter((t) => t.tags.includes(tag));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
byProtocol(protocol) {
|
|
102
|
+
return this.templates.filter((t) => t.protocol === protocol);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
search(query) {
|
|
106
|
+
const q = query.toLowerCase();
|
|
107
|
+
return this.templates.filter(
|
|
108
|
+
(t) =>
|
|
109
|
+
t.id.toLowerCase().includes(q) ||
|
|
110
|
+
t.name.toLowerCase().includes(q) ||
|
|
111
|
+
t.description.toLowerCase().includes(q) ||
|
|
112
|
+
t.tags.some((tag) => tag.toLowerCase().includes(q)),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
toDict() {
|
|
117
|
+
return {
|
|
118
|
+
name: this.name,
|
|
119
|
+
version: this.version,
|
|
120
|
+
count: this.count,
|
|
121
|
+
updated_at: this.updatedAt,
|
|
122
|
+
source: this.source,
|
|
123
|
+
severity_breakdown: {
|
|
124
|
+
critical: this.bySeverity('critical').length,
|
|
125
|
+
high: this.bySeverity('high').length,
|
|
126
|
+
medium: this.bySeverity('medium').length,
|
|
127
|
+
low: this.bySeverity('low').length,
|
|
128
|
+
info: this.bySeverity('info').length,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// NucleiTemplateManager
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the nuclei binary path.
|
|
140
|
+
* @returns {string|null}
|
|
141
|
+
*/
|
|
142
|
+
function _whichNuclei() {
|
|
143
|
+
try {
|
|
144
|
+
const result = execFileSync('which', ['nuclei'], {
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
timeout: 5000,
|
|
147
|
+
});
|
|
148
|
+
return result.trim() || null;
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Recursively list all .yaml files under a directory.
|
|
156
|
+
* @param {string} dir
|
|
157
|
+
* @returns {string[]}
|
|
158
|
+
*/
|
|
159
|
+
function _walkYaml(dir) {
|
|
160
|
+
const results = [];
|
|
161
|
+
if (!existsSync(dir)) return results;
|
|
162
|
+
|
|
163
|
+
let entries;
|
|
164
|
+
try {
|
|
165
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
166
|
+
} catch {
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const fullPath = join(dir, entry.name);
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
results.push(..._walkYaml(fullPath));
|
|
174
|
+
} else if (entry.isFile() && entry.name.endsWith('.yaml')) {
|
|
175
|
+
results.push(fullPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
class NucleiTemplateManager {
|
|
182
|
+
/**
|
|
183
|
+
* @param {object} [opts]
|
|
184
|
+
* @param {string} [opts.templateDir]
|
|
185
|
+
*/
|
|
186
|
+
constructor(opts = {}) {
|
|
187
|
+
this.templateDir = opts.templateDir || DEFAULT_TEMPLATE_DIR;
|
|
188
|
+
this._nucleiPath = _whichNuclei();
|
|
189
|
+
/** @type {TemplateCollection|null} */
|
|
190
|
+
this._collection = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @returns {boolean} */
|
|
194
|
+
get available() {
|
|
195
|
+
return this._nucleiPath !== null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @returns {boolean} */
|
|
199
|
+
get templatesExist() {
|
|
200
|
+
if (!existsSync(this.templateDir)) return false;
|
|
201
|
+
// Check for at least one .yaml file (shallow check)
|
|
202
|
+
return _walkYaml(this.templateDir).length > 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pull or update nuclei templates from the official repository.
|
|
207
|
+
* @param {boolean} [force=false]
|
|
208
|
+
* @returns {object}
|
|
209
|
+
*/
|
|
210
|
+
pullTemplates(force = false) {
|
|
211
|
+
if (this.templatesExist && !force) {
|
|
212
|
+
return {
|
|
213
|
+
status: 'already_exists',
|
|
214
|
+
path: this.templateDir,
|
|
215
|
+
message: 'Templates already present. Use force=true to update.',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Try nuclei -update-templates first
|
|
220
|
+
if (this._nucleiPath) {
|
|
221
|
+
try {
|
|
222
|
+
execFileSync(this._nucleiPath, ['-update-templates', '-ud', this.templateDir], {
|
|
223
|
+
encoding: 'utf8',
|
|
224
|
+
timeout: 300000,
|
|
225
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
226
|
+
});
|
|
227
|
+
const count = _walkYaml(this.templateDir).length;
|
|
228
|
+
return {
|
|
229
|
+
status: 'updated',
|
|
230
|
+
path: this.templateDir,
|
|
231
|
+
template_count: count,
|
|
232
|
+
method: 'nuclei-update',
|
|
233
|
+
};
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// Fall through to git
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fallback: git clone
|
|
240
|
+
let gitPath;
|
|
241
|
+
try {
|
|
242
|
+
gitPath = execFileSync('which', ['git'], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
243
|
+
} catch {
|
|
244
|
+
gitPath = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (gitPath) {
|
|
248
|
+
try {
|
|
249
|
+
if (existsSync(join(this.templateDir, '.git'))) {
|
|
250
|
+
execFileSync(gitPath, ['-C', this.templateDir, 'pull', '--depth=1'], {
|
|
251
|
+
encoding: 'utf8',
|
|
252
|
+
timeout: 300000,
|
|
253
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
execFileSync(
|
|
257
|
+
gitPath,
|
|
258
|
+
[
|
|
259
|
+
'clone',
|
|
260
|
+
'--depth=1',
|
|
261
|
+
'https://github.com/projectdiscovery/nuclei-templates.git',
|
|
262
|
+
this.templateDir,
|
|
263
|
+
],
|
|
264
|
+
{ encoding: 'utf8', timeout: 300000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const count = _walkYaml(this.templateDir).length;
|
|
268
|
+
return {
|
|
269
|
+
status: 'cloned',
|
|
270
|
+
path: this.templateDir,
|
|
271
|
+
template_count: count,
|
|
272
|
+
method: 'git-clone',
|
|
273
|
+
};
|
|
274
|
+
} catch {
|
|
275
|
+
// Fall through
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
status: 'error',
|
|
281
|
+
message: 'Neither nuclei nor git available for template pull',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Scan template directory and build an indexed collection.
|
|
287
|
+
* @returns {TemplateCollection}
|
|
288
|
+
*/
|
|
289
|
+
indexTemplates() {
|
|
290
|
+
if (!this.templatesExist) {
|
|
291
|
+
return new TemplateCollection({ name: 'empty', version: '0.0.0' });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const templates = [];
|
|
295
|
+
const yamlFiles = _walkYaml(this.templateDir).sort();
|
|
296
|
+
for (const yamlFile of yamlFiles) {
|
|
297
|
+
const info = this._parseTemplateFile(yamlFile);
|
|
298
|
+
if (info) templates.push(info);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
this._collection = new TemplateCollection({
|
|
302
|
+
name: 'nuclei-templates',
|
|
303
|
+
version: this._detectVersion(),
|
|
304
|
+
templates,
|
|
305
|
+
source: this.templateDir,
|
|
306
|
+
});
|
|
307
|
+
return this._collection;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Search templates by ID, name, description, or tags.
|
|
312
|
+
* @param {string} query
|
|
313
|
+
* @returns {object[]}
|
|
314
|
+
*/
|
|
315
|
+
searchTemplates(query) {
|
|
316
|
+
if (!this._collection) this.indexTemplates();
|
|
317
|
+
if (!this._collection) return [];
|
|
318
|
+
return this._collection.search(query).slice(0, 50).map((t) => t.toDict());
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get template paths suitable for a scan type.
|
|
323
|
+
* @param {string} [scanType='pentest']
|
|
324
|
+
* @param {string} [severity]
|
|
325
|
+
* @param {string[]} [tags]
|
|
326
|
+
* @returns {string[]}
|
|
327
|
+
*/
|
|
328
|
+
getTemplatesForScan(scanType = 'pentest', severity, tags) {
|
|
329
|
+
const SCAN_TYPE_TAGS = {
|
|
330
|
+
pentest: ['cve', 'sqli', 'xss', 'rce', 'lfi', 'ssrf', 'injection'],
|
|
331
|
+
recon: ['tech', 'detect', 'fingerprint', 'exposure', 'panel'],
|
|
332
|
+
compliance: ['misconfig', 'default-login', 'exposure', 'unauth'],
|
|
333
|
+
fuzzing: ['fuzz', 'brute', 'spray'],
|
|
334
|
+
cve: ['cve'],
|
|
335
|
+
exposure: ['exposure', 'misconfig', 'default-login', 'unauth', 'panel'],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const targetTags = [...(SCAN_TYPE_TAGS[scanType] || ['cve'])];
|
|
339
|
+
if (tags) targetTags.push(...tags);
|
|
340
|
+
|
|
341
|
+
if (!this._collection) this.indexTemplates();
|
|
342
|
+
if (!this._collection) return [];
|
|
343
|
+
|
|
344
|
+
const matched = [];
|
|
345
|
+
for (const t of this._collection.templates) {
|
|
346
|
+
if (severity && t.severity !== severity) continue;
|
|
347
|
+
if (t.tags.some((tag) => targetTags.includes(tag))) {
|
|
348
|
+
if (t.filePath) matched.push(t.filePath);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return matched.slice(0, 500);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Generate a custom nuclei template from a security finding.
|
|
356
|
+
* @param {object} finding
|
|
357
|
+
* @returns {string}
|
|
358
|
+
*/
|
|
359
|
+
generateTemplate(finding) {
|
|
360
|
+
const title = (finding.title || 'custom-check').toLowerCase().replace(/\s+/g, '-');
|
|
361
|
+
const desc = finding.description || 'Custom security check';
|
|
362
|
+
const severity = finding.severity || 'medium';
|
|
363
|
+
const method = (finding.method || 'GET').toUpperCase();
|
|
364
|
+
const path = finding.path || '/';
|
|
365
|
+
const matchers = finding.matchers || [];
|
|
366
|
+
|
|
367
|
+
let matcherYaml;
|
|
368
|
+
if (matchers.length) {
|
|
369
|
+
matcherYaml = matchers
|
|
370
|
+
.map((m) => ` - type: word\n words:\n - "${m}"`)
|
|
371
|
+
.join('\n');
|
|
372
|
+
} else {
|
|
373
|
+
matcherYaml = ' - type: status\n status:\n - 200';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return `id: cipher-${title}
|
|
377
|
+
|
|
378
|
+
info:
|
|
379
|
+
name: ${finding.title || 'Custom Check'}
|
|
380
|
+
author: cipher-auto
|
|
381
|
+
severity: ${severity}
|
|
382
|
+
description: |
|
|
383
|
+
${desc}
|
|
384
|
+
tags: custom,cipher-generated
|
|
385
|
+
metadata:
|
|
386
|
+
generated-by: cipher-template-manager
|
|
387
|
+
generated-at: ${new Date().toISOString()}
|
|
388
|
+
|
|
389
|
+
http:
|
|
390
|
+
- method: ${method}
|
|
391
|
+
path:
|
|
392
|
+
- "{{BaseURL}}${path}"
|
|
393
|
+
matchers:
|
|
394
|
+
${matcherYaml}
|
|
395
|
+
`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Generate a nuclei workflow YAML that chains multiple templates.
|
|
400
|
+
* @param {string} name
|
|
401
|
+
* @param {string[]} templateIds
|
|
402
|
+
* @returns {string}
|
|
403
|
+
*/
|
|
404
|
+
generateWorkflow(name, templateIds) {
|
|
405
|
+
const subtemplates = templateIds
|
|
406
|
+
.map((tid) => ` - template: ${tid}`)
|
|
407
|
+
.join('\n');
|
|
408
|
+
return `id: cipher-workflow-${name}
|
|
409
|
+
|
|
410
|
+
info:
|
|
411
|
+
name: CIPHER Workflow - ${name}
|
|
412
|
+
author: cipher
|
|
413
|
+
severity: info
|
|
414
|
+
|
|
415
|
+
workflows:
|
|
416
|
+
- template: ${templateIds[0] || 'missing'}
|
|
417
|
+
subtemplates:
|
|
418
|
+
${subtemplates}
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Return template collection statistics.
|
|
424
|
+
* @returns {object}
|
|
425
|
+
*/
|
|
426
|
+
stats() {
|
|
427
|
+
if (!this._collection) this.indexTemplates();
|
|
428
|
+
if (!this._collection) {
|
|
429
|
+
return { status: 'no_templates', path: this.templateDir };
|
|
430
|
+
}
|
|
431
|
+
const protocols = {};
|
|
432
|
+
for (const p of [
|
|
433
|
+
'http', 'dns', 'network', 'file', 'headless', 'javascript', 'code', 'ssl',
|
|
434
|
+
]) {
|
|
435
|
+
protocols[p] = this._collection.byProtocol(p).length;
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
...this._collection.toDict(),
|
|
439
|
+
protocols,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// -- private helpers --
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Parse a nuclei template YAML file for metadata (fast string splitting).
|
|
447
|
+
* @param {string} filePath
|
|
448
|
+
* @returns {TemplateInfo|null}
|
|
449
|
+
*/
|
|
450
|
+
_parseTemplateFile(filePath) {
|
|
451
|
+
try {
|
|
452
|
+
const content = readFileSync(filePath, 'utf8');
|
|
453
|
+
const info = new TemplateInfo({ filePath });
|
|
454
|
+
|
|
455
|
+
const lines = content.split('\n').slice(0, 50);
|
|
456
|
+
for (const rawLine of lines) {
|
|
457
|
+
const line = rawLine.trim();
|
|
458
|
+
if (line.startsWith('id:')) {
|
|
459
|
+
info.id = line.split(':', 2)[1].trim();
|
|
460
|
+
} else if (line.startsWith('name:')) {
|
|
461
|
+
info.name = line.split(':', 2)[1].trim();
|
|
462
|
+
} else if (line.startsWith('severity:')) {
|
|
463
|
+
info.severity = line.split(':', 2)[1].trim();
|
|
464
|
+
} else if (line.startsWith('author:')) {
|
|
465
|
+
info.author = line.split(':', 2)[1].trim();
|
|
466
|
+
} else if (line.startsWith('description:')) {
|
|
467
|
+
info.description = line.split(':', 2)[1].trim();
|
|
468
|
+
} else if (line.startsWith('tags:')) {
|
|
469
|
+
info.tags = line
|
|
470
|
+
.split(':', 2)[1]
|
|
471
|
+
.split(',')
|
|
472
|
+
.map((t) => t.trim())
|
|
473
|
+
.filter(Boolean);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Detect protocol and category from path relative to template dir
|
|
478
|
+
try {
|
|
479
|
+
const rel = relative(this.templateDir, filePath);
|
|
480
|
+
const parts = rel.split('/');
|
|
481
|
+
if (parts.length > 0) {
|
|
482
|
+
info.category = parts[0];
|
|
483
|
+
const knownProtocols = [
|
|
484
|
+
'http', 'dns', 'network', 'file', 'headless',
|
|
485
|
+
'javascript', 'code', 'ssl', 'cloud',
|
|
486
|
+
];
|
|
487
|
+
info.protocol = knownProtocols.includes(parts[0]) ? parts[0] : 'http';
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
// relative path may fail if paths are on different roots — ignore
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (!info.id) return null;
|
|
494
|
+
return info;
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Detect template version from checksum or date.
|
|
502
|
+
* @returns {string}
|
|
503
|
+
*/
|
|
504
|
+
_detectVersion() {
|
|
505
|
+
const checksumFile = join(this.templateDir, 'cves.json-checksum.txt');
|
|
506
|
+
try {
|
|
507
|
+
if (existsSync(checksumFile)) {
|
|
508
|
+
return readFileSync(checksumFile, 'utf8').trim().slice(0, 12);
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
// ignore
|
|
512
|
+
}
|
|
513
|
+
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export {
|
|
518
|
+
// Constants
|
|
519
|
+
DEFAULT_TEMPLATE_DIR,
|
|
520
|
+
// Data classes
|
|
521
|
+
TemplateInfo,
|
|
522
|
+
TemplateCollection,
|
|
523
|
+
// Manager
|
|
524
|
+
NucleiTemplateManager,
|
|
525
|
+
};
|