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.
Files changed (75) hide show
  1. package/bin/cipher.js +465 -0
  2. package/lib/api/billing.js +321 -0
  3. package/lib/api/compliance.js +693 -0
  4. package/lib/api/controls.js +1401 -0
  5. package/lib/api/index.js +49 -0
  6. package/lib/api/marketplace.js +467 -0
  7. package/lib/api/openai-proxy.js +383 -0
  8. package/lib/api/server.js +685 -0
  9. package/lib/autonomous/feedback-loop.js +554 -0
  10. package/lib/autonomous/framework.js +512 -0
  11. package/lib/autonomous/index.js +97 -0
  12. package/lib/autonomous/leaderboard.js +594 -0
  13. package/lib/autonomous/modes/architect.js +412 -0
  14. package/lib/autonomous/modes/blue.js +386 -0
  15. package/lib/autonomous/modes/incident.js +684 -0
  16. package/lib/autonomous/modes/privacy.js +369 -0
  17. package/lib/autonomous/modes/purple.js +294 -0
  18. package/lib/autonomous/modes/recon.js +250 -0
  19. package/lib/autonomous/parallel.js +587 -0
  20. package/lib/autonomous/researcher.js +583 -0
  21. package/lib/autonomous/runner.js +955 -0
  22. package/lib/autonomous/scheduler.js +615 -0
  23. package/lib/autonomous/task-parser.js +127 -0
  24. package/lib/autonomous/validators/forensic.js +266 -0
  25. package/lib/autonomous/validators/osint.js +216 -0
  26. package/lib/autonomous/validators/privacy.js +296 -0
  27. package/lib/autonomous/validators/purple.js +298 -0
  28. package/lib/autonomous/validators/sigma.js +248 -0
  29. package/lib/autonomous/validators/threat-model.js +363 -0
  30. package/lib/benchmark/agent.js +119 -0
  31. package/lib/benchmark/baselines.js +43 -0
  32. package/lib/benchmark/builder.js +143 -0
  33. package/lib/benchmark/config.js +35 -0
  34. package/lib/benchmark/coordinator.js +91 -0
  35. package/lib/benchmark/index.js +20 -0
  36. package/lib/benchmark/llm.js +58 -0
  37. package/lib/benchmark/models.js +137 -0
  38. package/lib/benchmark/reporter.js +103 -0
  39. package/lib/benchmark/runner.js +103 -0
  40. package/lib/benchmark/sandbox.js +96 -0
  41. package/lib/benchmark/scorer.js +32 -0
  42. package/lib/benchmark/solver.js +166 -0
  43. package/lib/benchmark/tools.js +62 -0
  44. package/lib/bot/bot.js +130 -0
  45. package/lib/commands.js +99 -0
  46. package/lib/complexity.js +377 -0
  47. package/lib/config.js +213 -0
  48. package/lib/gateway/client.js +309 -0
  49. package/lib/gateway/commands.js +830 -0
  50. package/lib/gateway/config-validate.js +109 -0
  51. package/lib/gateway/gateway.js +367 -0
  52. package/lib/gateway/index.js +62 -0
  53. package/lib/gateway/mode.js +309 -0
  54. package/lib/gateway/plugins.js +222 -0
  55. package/lib/gateway/prompt.js +214 -0
  56. package/lib/mcp/server.js +262 -0
  57. package/lib/memory/compressor.js +425 -0
  58. package/lib/memory/engine.js +763 -0
  59. package/lib/memory/evolution.js +668 -0
  60. package/lib/memory/index.js +58 -0
  61. package/lib/memory/orchestrator.js +506 -0
  62. package/lib/memory/retriever.js +515 -0
  63. package/lib/memory/synthesizer.js +333 -0
  64. package/lib/pipeline/async-scanner.js +510 -0
  65. package/lib/pipeline/binary-analysis.js +1043 -0
  66. package/lib/pipeline/dom-xss-scanner.js +435 -0
  67. package/lib/pipeline/github-actions.js +792 -0
  68. package/lib/pipeline/index.js +124 -0
  69. package/lib/pipeline/osint.js +498 -0
  70. package/lib/pipeline/sarif.js +373 -0
  71. package/lib/pipeline/scanner.js +880 -0
  72. package/lib/pipeline/template-manager.js +525 -0
  73. package/lib/pipeline/xss-scanner.js +353 -0
  74. package/lib/setup-wizard.js +229 -0
  75. package/package.json +30 -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
+ }