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,127 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse freeform CLI task text into structured taskInput for each autonomous mode.
|
|
6
|
+
*
|
|
7
|
+
* Each mode's system prompt template expects specific variables:
|
|
8
|
+
* BLUE: {ttp_id}, {ttp_description}
|
|
9
|
+
* PURPLE: {ttp_id}, {ttp_description}
|
|
10
|
+
* INCIDENT: {pcap_path}, {pcap_description}
|
|
11
|
+
* RECON: {target_domain}, {target_description}
|
|
12
|
+
* PRIVACY: {codebase_description}, {file_listing}
|
|
13
|
+
* ARCHITECT: {system_description}
|
|
14
|
+
*
|
|
15
|
+
* The parser extracts what it can from the text and fills defaults for the rest.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ATT&CK technique ID pattern
|
|
19
|
+
const TTP_RE = /\b(T\d{4}(?:\.\d{3})?)\b/i;
|
|
20
|
+
|
|
21
|
+
// Domain/hostname pattern
|
|
22
|
+
const DOMAIN_RE = /\b([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+)\b/;
|
|
23
|
+
|
|
24
|
+
// File path pattern
|
|
25
|
+
const FILE_PATH_RE = /(?:^|\s)((?:\/|\.\/|~\/)[^\s]+|[a-zA-Z]:\\[^\s]+)/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse freeform task text into structured input for a given mode.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} mode - Uppercase mode name (BLUE, INCIDENT, etc.)
|
|
31
|
+
* @param {string} taskText - Freeform user input
|
|
32
|
+
* @returns {object} Structured taskInput with mode-specific fields + user_message
|
|
33
|
+
*/
|
|
34
|
+
export function parseTaskInput(mode, taskText) {
|
|
35
|
+
const base = { user_message: taskText };
|
|
36
|
+
|
|
37
|
+
switch (mode) {
|
|
38
|
+
case 'BLUE':
|
|
39
|
+
case 'PURPLE':
|
|
40
|
+
return { ...parseTtpInput(taskText), ...base };
|
|
41
|
+
|
|
42
|
+
case 'INCIDENT':
|
|
43
|
+
return { ...parseIncidentInput(taskText), ...base };
|
|
44
|
+
|
|
45
|
+
case 'RECON':
|
|
46
|
+
return { ...parseReconInput(taskText), ...base };
|
|
47
|
+
|
|
48
|
+
case 'PRIVACY':
|
|
49
|
+
return { ...parsePrivacyInput(taskText), ...base };
|
|
50
|
+
|
|
51
|
+
case 'ARCHITECT':
|
|
52
|
+
return { system_description: taskText, ...base };
|
|
53
|
+
|
|
54
|
+
case 'RED':
|
|
55
|
+
return { task: taskText, ...base };
|
|
56
|
+
|
|
57
|
+
default:
|
|
58
|
+
return { task: taskText, ...base };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseTtpInput(text) {
|
|
63
|
+
const match = text.match(TTP_RE);
|
|
64
|
+
const ttpId = match ? match[1].toUpperCase() : '';
|
|
65
|
+
|
|
66
|
+
// Remove the TTP ID from description
|
|
67
|
+
let description = text;
|
|
68
|
+
if (ttpId) {
|
|
69
|
+
description = text.replace(TTP_RE, '').trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If no description left, use the TTP ID as description
|
|
73
|
+
if (!description) {
|
|
74
|
+
description = `ATT&CK technique ${ttpId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ttp_id: ttpId || 'UNSPECIFIED',
|
|
79
|
+
ttp_description: description,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseIncidentInput(text) {
|
|
84
|
+
const pathMatch = text.match(FILE_PATH_RE);
|
|
85
|
+
const pcapPath = pathMatch ? pathMatch[1] : '';
|
|
86
|
+
|
|
87
|
+
let description = text;
|
|
88
|
+
if (pcapPath) {
|
|
89
|
+
description = text.replace(pcapPath, '').trim();
|
|
90
|
+
}
|
|
91
|
+
if (!description) {
|
|
92
|
+
description = pcapPath ? `Analyze packet capture: ${pcapPath}` : 'Triage the provided evidence';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
pcap_path: pcapPath || 'provided via tool input',
|
|
97
|
+
pcap_description: description,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseReconInput(text) {
|
|
102
|
+
const domainMatch = text.match(DOMAIN_RE);
|
|
103
|
+
const domain = domainMatch ? domainMatch[1] : '';
|
|
104
|
+
|
|
105
|
+
let description = text;
|
|
106
|
+
if (domain) {
|
|
107
|
+
description = text.replace(domain, '').trim();
|
|
108
|
+
}
|
|
109
|
+
if (!description) {
|
|
110
|
+
description = domain ? `Reconnaissance on ${domain}` : 'Perform OSINT reconnaissance';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
target_domain: domain || 'UNSPECIFIED',
|
|
115
|
+
target_description: description,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parsePrivacyInput(text) {
|
|
120
|
+
const pathMatch = text.match(FILE_PATH_RE);
|
|
121
|
+
const fileListing = pathMatch ? pathMatch[1] : '';
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
codebase_description: text,
|
|
125
|
+
file_listing: fileListing || 'Analyze the described system',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
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
|
+
* Forensic report validator for INCIDENT mode agent output.
|
|
7
|
+
*
|
|
8
|
+
* Ported from autonomous/validators/forensic.py.
|
|
9
|
+
* Validates LLM-generated JSON forensic reports for structural correctness:
|
|
10
|
+
* required top-level sections, CVE-ID format, non-empty findings and services.
|
|
11
|
+
*
|
|
12
|
+
* @module autonomous/validators/forensic
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ValidationResult } from '../framework.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const REQUIRED_SECTIONS = [
|
|
22
|
+
'summary', 'services', 'connections', 'findings', 'recommendations',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const RECOMMENDED_FINDING_FIELDS = ['severity', 'description', 'evidence'];
|
|
26
|
+
|
|
27
|
+
const RECOMMENDED_TOP_LEVEL_FIELDS = ['pcap_file', 'analysis_timestamp'];
|
|
28
|
+
|
|
29
|
+
// Matches well-formed CVE IDs: CVE-YYYY-NNNNN (4-7 digit suffix)
|
|
30
|
+
const CVE_VALID_RE = /CVE-\d{4}-\d{4,7}/;
|
|
31
|
+
|
|
32
|
+
// Matches anything that looks like a CVE reference (broader, to catch malformed)
|
|
33
|
+
const CVE_LIKE_RE = /CVE-[A-Za-z0-9]+-[A-Za-z0-9]+/g;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract JSON content from markdown code fences, or return text as-is.
|
|
41
|
+
* Uses findall+join pattern.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} text
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function stripCodeFences(text) {
|
|
47
|
+
const matches = [...text.matchAll(/```(?:json)?\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
48
|
+
if (matches.length > 0) {
|
|
49
|
+
return matches.join('\n');
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse a JSON forensic report from text.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} text
|
|
58
|
+
* @returns {Object}
|
|
59
|
+
* @throws {Error}
|
|
60
|
+
*/
|
|
61
|
+
function parseJsonReport(text) {
|
|
62
|
+
const stripped = stripCodeFences(text);
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(stripped);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new Error(`JSON parse error: ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Recursively collect all string values from a nested structure.
|
|
72
|
+
*
|
|
73
|
+
* @param {*} obj
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
function collectStringValues(obj) {
|
|
77
|
+
const strings = [];
|
|
78
|
+
if (typeof obj === 'string') {
|
|
79
|
+
strings.push(obj);
|
|
80
|
+
} else if (Array.isArray(obj)) {
|
|
81
|
+
for (const item of obj) {
|
|
82
|
+
strings.push(...collectStringValues(item));
|
|
83
|
+
}
|
|
84
|
+
} else if (obj && typeof obj === 'object') {
|
|
85
|
+
for (const v of Object.values(obj)) {
|
|
86
|
+
strings.push(...collectStringValues(v));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return strings;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a forensic report dict for structural correctness.
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} report
|
|
96
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
97
|
+
*/
|
|
98
|
+
function validateReport(report) {
|
|
99
|
+
const errors = [];
|
|
100
|
+
const warnings = [];
|
|
101
|
+
|
|
102
|
+
// Required top-level sections
|
|
103
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
104
|
+
if (!(section in report)) {
|
|
105
|
+
errors.push(`Missing required section: ${section}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// findings must be a non-empty list
|
|
110
|
+
const findings = report.findings;
|
|
111
|
+
if (findings !== undefined) {
|
|
112
|
+
if (!Array.isArray(findings)) {
|
|
113
|
+
errors.push(`findings must be a list (got ${typeof findings})`);
|
|
114
|
+
} else if (findings.length === 0) {
|
|
115
|
+
errors.push('findings list must not be empty');
|
|
116
|
+
} else {
|
|
117
|
+
for (let i = 0; i < findings.length; i++) {
|
|
118
|
+
if (findings[i] && typeof findings[i] === 'object' && !Array.isArray(findings[i])) {
|
|
119
|
+
for (const fieldName of RECOMMENDED_FINDING_FIELDS) {
|
|
120
|
+
if (!(fieldName in findings[i])) {
|
|
121
|
+
warnings.push(`Finding ${i + 1}: missing recommended field '${fieldName}'`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// services must be a non-empty list
|
|
130
|
+
const services = report.services;
|
|
131
|
+
if (services !== undefined) {
|
|
132
|
+
if (!Array.isArray(services)) {
|
|
133
|
+
errors.push(`services must be a list (got ${typeof services})`);
|
|
134
|
+
} else if (services.length === 0) {
|
|
135
|
+
errors.push('services list must not be empty');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// CVE format validation in findings
|
|
140
|
+
if (Array.isArray(findings)) {
|
|
141
|
+
const allStrings = collectStringValues(findings);
|
|
142
|
+
for (const s of allStrings) {
|
|
143
|
+
// Reset global regex
|
|
144
|
+
const cveLikeRe = /CVE-[A-Za-z0-9]+-[A-Za-z0-9]+/g;
|
|
145
|
+
let match;
|
|
146
|
+
while ((match = cveLikeRe.exec(s)) !== null) {
|
|
147
|
+
const cveRef = match[0];
|
|
148
|
+
if (!CVE_VALID_RE.test(cveRef)) {
|
|
149
|
+
errors.push(
|
|
150
|
+
`Malformed CVE ID: ${cveRef} (must match CVE-YYYY-NNNN[N{0,3}])`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Recommended top-level fields
|
|
158
|
+
for (const fieldName of RECOMMENDED_TOP_LEVEL_FIELDS) {
|
|
159
|
+
if (!(fieldName in report)) {
|
|
160
|
+
warnings.push(`Missing recommended top-level field: ${fieldName}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { errors, warnings };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// ForensicValidator
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Deterministic validator for JSON forensic reports.
|
|
173
|
+
*
|
|
174
|
+
* Scoring:
|
|
175
|
+
* - 1.0: no errors and no warnings (clean)
|
|
176
|
+
* - 0.5: warnings only (valid but imperfect)
|
|
177
|
+
* - 0.0: any errors (invalid)
|
|
178
|
+
*/
|
|
179
|
+
export class ForensicValidator {
|
|
180
|
+
/**
|
|
181
|
+
* @param {import('../framework.js').ModeAgentResult} result
|
|
182
|
+
* @returns {import('../framework.js').ValidationResult}
|
|
183
|
+
*/
|
|
184
|
+
validate(result) {
|
|
185
|
+
const text = result.outputText || '';
|
|
186
|
+
const emptyMeta = { findings_count: 0, services_count: 0, cve_count: 0 };
|
|
187
|
+
|
|
188
|
+
// Empty output
|
|
189
|
+
if (!text.trim()) {
|
|
190
|
+
return new ValidationResult({
|
|
191
|
+
valid: false,
|
|
192
|
+
errors: ['No forensic report found in output (empty)'],
|
|
193
|
+
warnings: [],
|
|
194
|
+
score: 0.0,
|
|
195
|
+
metadata: { ...emptyMeta },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Parse JSON
|
|
200
|
+
let parsed;
|
|
201
|
+
try {
|
|
202
|
+
parsed = parseJsonReport(text);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return new ValidationResult({
|
|
205
|
+
valid: false,
|
|
206
|
+
errors: [e.message],
|
|
207
|
+
warnings: [],
|
|
208
|
+
score: 0.0,
|
|
209
|
+
metadata: { ...emptyMeta },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Must be an object
|
|
214
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
215
|
+
return new ValidationResult({
|
|
216
|
+
valid: false,
|
|
217
|
+
errors: [`Forensic report must be a JSON object (got ${Array.isArray(parsed) ? 'Array' : typeof parsed})`],
|
|
218
|
+
warnings: [],
|
|
219
|
+
score: 0.0,
|
|
220
|
+
metadata: { ...emptyMeta },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Structural validation
|
|
225
|
+
const { errors, warnings } = validateReport(parsed);
|
|
226
|
+
|
|
227
|
+
// Compute metadata
|
|
228
|
+
const findings = parsed.findings || [];
|
|
229
|
+
const services = parsed.services || [];
|
|
230
|
+
const findingsCount = Array.isArray(findings) ? findings.length : 0;
|
|
231
|
+
const servicesCount = Array.isArray(services) ? services.length : 0;
|
|
232
|
+
|
|
233
|
+
// Count valid CVE references
|
|
234
|
+
let cveCount = 0;
|
|
235
|
+
if (Array.isArray(findings)) {
|
|
236
|
+
const allStrings = collectStringValues(findings);
|
|
237
|
+
for (const s of allStrings) {
|
|
238
|
+
const cveValidRe = /CVE-\d{4}-\d{4,7}/g;
|
|
239
|
+
const matches = s.match(cveValidRe);
|
|
240
|
+
if (matches) cveCount += matches.length;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Scoring
|
|
245
|
+
let score;
|
|
246
|
+
if (errors.length > 0) {
|
|
247
|
+
score = 0.0;
|
|
248
|
+
} else if (warnings.length > 0) {
|
|
249
|
+
score = 0.5;
|
|
250
|
+
} else {
|
|
251
|
+
score = 1.0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return new ValidationResult({
|
|
255
|
+
valid: errors.length === 0,
|
|
256
|
+
errors,
|
|
257
|
+
warnings,
|
|
258
|
+
score,
|
|
259
|
+
metadata: {
|
|
260
|
+
findings_count: findingsCount,
|
|
261
|
+
services_count: servicesCount,
|
|
262
|
+
cve_count: cveCount,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
* OSINT intelligence report validator for RECON mode agent output.
|
|
7
|
+
*
|
|
8
|
+
* Ported from autonomous/validators/osint.py.
|
|
9
|
+
* Validates LLM-generated JSON intelligence reports for structural correctness:
|
|
10
|
+
* required sections, non-empty data, and finding structure validation.
|
|
11
|
+
*
|
|
12
|
+
* @module autonomous/validators/osint
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { ValidationResult } from '../framework.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const REQUIRED_SECTIONS = [
|
|
22
|
+
'summary', 'target', 'dns_records', 'whois_data', 'technologies', 'findings',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const RECOMMENDED_TOP_LEVEL_FIELDS = [
|
|
26
|
+
'subdomains', 'infrastructure', 'collection_metadata',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract JSON content from markdown code fences, or return text as-is.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} text
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function stripCodeFences(text) {
|
|
40
|
+
// Try explicit json-tagged fences first
|
|
41
|
+
let matches = [...text.matchAll(/```json\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
42
|
+
if (matches.length > 0) return matches.join('\n');
|
|
43
|
+
|
|
44
|
+
// Fall back to bare fences
|
|
45
|
+
matches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
|
|
46
|
+
if (matches.length > 0) return matches.join('\n');
|
|
47
|
+
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a JSON OSINT report from text.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} text
|
|
55
|
+
* @returns {Object}
|
|
56
|
+
* @throws {Error}
|
|
57
|
+
*/
|
|
58
|
+
function parseJsonReport(text) {
|
|
59
|
+
const stripped = stripCodeFences(text);
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(stripped);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw new Error(`JSON parse error: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate an OSINT report dict for structural correctness.
|
|
69
|
+
*
|
|
70
|
+
* @param {Object} report
|
|
71
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
72
|
+
*/
|
|
73
|
+
function validateReport(report) {
|
|
74
|
+
const errors = [];
|
|
75
|
+
const warnings = [];
|
|
76
|
+
|
|
77
|
+
// Required top-level sections
|
|
78
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
79
|
+
if (!(section in report)) {
|
|
80
|
+
errors.push(`Missing required section: ${section}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// findings must be a non-empty list
|
|
85
|
+
const findings = report.findings;
|
|
86
|
+
if (findings !== undefined) {
|
|
87
|
+
if (!Array.isArray(findings)) {
|
|
88
|
+
errors.push(`findings must be a list (got ${typeof findings})`);
|
|
89
|
+
} else if (findings.length === 0) {
|
|
90
|
+
errors.push('findings list must not be empty');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// dns_records must be a non-empty dict or list
|
|
95
|
+
const dnsRecords = report.dns_records;
|
|
96
|
+
if (dnsRecords !== undefined) {
|
|
97
|
+
if (typeof dnsRecords === 'object' && dnsRecords !== null && !Array.isArray(dnsRecords)) {
|
|
98
|
+
if (Object.keys(dnsRecords).length === 0) {
|
|
99
|
+
errors.push('dns_records must not be empty');
|
|
100
|
+
}
|
|
101
|
+
} else if (Array.isArray(dnsRecords)) {
|
|
102
|
+
if (dnsRecords.length === 0) {
|
|
103
|
+
errors.push('dns_records must not be empty');
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
errors.push(`dns_records must be a dict or list (got ${typeof dnsRecords})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Recommended top-level fields
|
|
111
|
+
for (const fieldName of RECOMMENDED_TOP_LEVEL_FIELDS) {
|
|
112
|
+
if (!(fieldName in report)) {
|
|
113
|
+
warnings.push(`Missing recommended field: ${fieldName}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { errors, warnings };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// OSINTValidator
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Deterministic validator for JSON OSINT intelligence reports.
|
|
126
|
+
*
|
|
127
|
+
* Scoring:
|
|
128
|
+
* - 1.0: no errors and no warnings (clean)
|
|
129
|
+
* - 0.5: warnings only (valid but imperfect)
|
|
130
|
+
* - 0.0: any errors (invalid)
|
|
131
|
+
*/
|
|
132
|
+
export class OSINTValidator {
|
|
133
|
+
/**
|
|
134
|
+
* @param {import('../framework.js').ModeAgentResult} result
|
|
135
|
+
* @returns {import('../framework.js').ValidationResult}
|
|
136
|
+
*/
|
|
137
|
+
validate(result) {
|
|
138
|
+
const text = result.outputText || '';
|
|
139
|
+
const emptyMeta = { findings_count: 0, technologies_count: 0, dns_record_types: [] };
|
|
140
|
+
|
|
141
|
+
// Empty output
|
|
142
|
+
if (!text.trim()) {
|
|
143
|
+
return new ValidationResult({
|
|
144
|
+
valid: false,
|
|
145
|
+
errors: ['No OSINT report found in output (empty)'],
|
|
146
|
+
warnings: [],
|
|
147
|
+
score: 0.0,
|
|
148
|
+
metadata: { ...emptyMeta },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Parse JSON
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = parseJsonReport(text);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return new ValidationResult({
|
|
158
|
+
valid: false,
|
|
159
|
+
errors: [e.message],
|
|
160
|
+
warnings: [],
|
|
161
|
+
score: 0.0,
|
|
162
|
+
metadata: { ...emptyMeta },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Must be an object
|
|
167
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
168
|
+
return new ValidationResult({
|
|
169
|
+
valid: false,
|
|
170
|
+
errors: [`OSINT report must be a JSON object (got ${Array.isArray(parsed) ? 'Array' : typeof parsed})`],
|
|
171
|
+
warnings: [],
|
|
172
|
+
score: 0.0,
|
|
173
|
+
metadata: { ...emptyMeta },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Structural validation
|
|
178
|
+
const { errors, warnings } = validateReport(parsed);
|
|
179
|
+
|
|
180
|
+
// Compute metadata
|
|
181
|
+
const findings = parsed.findings || [];
|
|
182
|
+
const technologies = parsed.technologies || [];
|
|
183
|
+
const dnsRecords = parsed.dns_records || {};
|
|
184
|
+
|
|
185
|
+
const findingsCount = Array.isArray(findings) ? findings.length : 0;
|
|
186
|
+
const technologiesCount = Array.isArray(technologies) ? technologies.length
|
|
187
|
+
: (typeof technologies === 'object' && technologies !== null ? Object.keys(technologies).length : 0);
|
|
188
|
+
|
|
189
|
+
let dnsRecordTypes = [];
|
|
190
|
+
if (typeof dnsRecords === 'object' && dnsRecords !== null && !Array.isArray(dnsRecords)) {
|
|
191
|
+
dnsRecordTypes = Object.keys(dnsRecords).sort();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Scoring
|
|
195
|
+
let score;
|
|
196
|
+
if (errors.length > 0) {
|
|
197
|
+
score = 0.0;
|
|
198
|
+
} else if (warnings.length > 0) {
|
|
199
|
+
score = 0.5;
|
|
200
|
+
} else {
|
|
201
|
+
score = 1.0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return new ValidationResult({
|
|
205
|
+
valid: errors.length === 0,
|
|
206
|
+
errors,
|
|
207
|
+
warnings,
|
|
208
|
+
score,
|
|
209
|
+
metadata: {
|
|
210
|
+
findings_count: findingsCount,
|
|
211
|
+
technologies_count: technologiesCount,
|
|
212
|
+
dns_record_types: dnsRecordTypes,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|