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,124 @@
|
|
|
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 Pipeline — Security scanning, analysis, and reporting.
|
|
7
|
+
*
|
|
8
|
+
* Public API surface for the pipeline module. All downstream consumers
|
|
9
|
+
* (gateway, API, MCP) import from this barrel.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Scanner — foundational data classes and subprocess runners
|
|
13
|
+
export {
|
|
14
|
+
ScanDomain,
|
|
15
|
+
PROFILE_CONFIGS,
|
|
16
|
+
EXT_TAG_MAP,
|
|
17
|
+
Finding,
|
|
18
|
+
ScanResult,
|
|
19
|
+
CrawlResult,
|
|
20
|
+
PipelineResult,
|
|
21
|
+
ScanProfile,
|
|
22
|
+
NucleiRunner,
|
|
23
|
+
KatanaRunner,
|
|
24
|
+
ScanPipeline,
|
|
25
|
+
whichSync,
|
|
26
|
+
} from './scanner.js';
|
|
27
|
+
|
|
28
|
+
// Async scanner — Promise-based wrappers with concurrency control
|
|
29
|
+
export {
|
|
30
|
+
AsyncScanStatus,
|
|
31
|
+
AsyncFinding,
|
|
32
|
+
AsyncScanResult,
|
|
33
|
+
AsyncNucleiRunner,
|
|
34
|
+
AsyncKatanaRunner,
|
|
35
|
+
AsyncScanManager,
|
|
36
|
+
Semaphore,
|
|
37
|
+
asyncScan,
|
|
38
|
+
severityForProfile,
|
|
39
|
+
} from './async-scanner.js';
|
|
40
|
+
|
|
41
|
+
// Binary analysis — ELF parsing, security features, ROP gadgets
|
|
42
|
+
export {
|
|
43
|
+
ELF_MAGIC,
|
|
44
|
+
SHF_WRITE,
|
|
45
|
+
SHF_ALLOC,
|
|
46
|
+
SHF_EXECINSTR,
|
|
47
|
+
readU16,
|
|
48
|
+
readU32,
|
|
49
|
+
readU64,
|
|
50
|
+
readStr,
|
|
51
|
+
ELFSection,
|
|
52
|
+
ELFSymbol,
|
|
53
|
+
SecurityFeatures,
|
|
54
|
+
ELFAnalysis,
|
|
55
|
+
ELFParser,
|
|
56
|
+
FormatStringFinding,
|
|
57
|
+
FormatStringScanner,
|
|
58
|
+
ROPGadget,
|
|
59
|
+
ROPScanner,
|
|
60
|
+
} from './binary-analysis.js';
|
|
61
|
+
|
|
62
|
+
// XSS scanner — static pattern detection
|
|
63
|
+
export {
|
|
64
|
+
SINK_PATTERNS,
|
|
65
|
+
SOURCE_PATTERNS,
|
|
66
|
+
TEMPLATE_SINKS,
|
|
67
|
+
SCANNABLE_EXTENSIONS,
|
|
68
|
+
XSSFinding,
|
|
69
|
+
XSSScanResult,
|
|
70
|
+
XSSScanner,
|
|
71
|
+
} from './xss-scanner.js';
|
|
72
|
+
|
|
73
|
+
// DOM XSS scanner — dynamic Playwright-based detection
|
|
74
|
+
export {
|
|
75
|
+
XSS_PAYLOADS,
|
|
76
|
+
URL_PAYLOADS,
|
|
77
|
+
DOMXSSFinding,
|
|
78
|
+
DOMXSSScanResult,
|
|
79
|
+
DOMXSSScanner,
|
|
80
|
+
} from './dom-xss-scanner.js';
|
|
81
|
+
|
|
82
|
+
// OSINT — intelligence gathering subprocess wrappers
|
|
83
|
+
export {
|
|
84
|
+
isPrivateIP,
|
|
85
|
+
OSINTResult,
|
|
86
|
+
DomainIntelligence,
|
|
87
|
+
IPIntelligence,
|
|
88
|
+
DocumentMetadata,
|
|
89
|
+
OSINTPipeline,
|
|
90
|
+
} from './osint.js';
|
|
91
|
+
|
|
92
|
+
// SARIF — Static Analysis Results Interchange Format
|
|
93
|
+
export {
|
|
94
|
+
SARIF_VERSION,
|
|
95
|
+
SARIF_SCHEMA,
|
|
96
|
+
severityToLevel,
|
|
97
|
+
severityScore,
|
|
98
|
+
SarifRule,
|
|
99
|
+
SarifResult,
|
|
100
|
+
SarifReport,
|
|
101
|
+
} from './sarif.js';
|
|
102
|
+
|
|
103
|
+
// GitHub Actions — diff analysis, formatting, workflow generation
|
|
104
|
+
export {
|
|
105
|
+
_getVersion,
|
|
106
|
+
RiskLevel,
|
|
107
|
+
SecretType,
|
|
108
|
+
SEV_EMOJI,
|
|
109
|
+
SecretFinding,
|
|
110
|
+
DiffAnalysis,
|
|
111
|
+
PRReviewResult,
|
|
112
|
+
SecurityDiffAnalyzer,
|
|
113
|
+
FindingFormatter,
|
|
114
|
+
PRSecurityReview,
|
|
115
|
+
WorkflowGenerator,
|
|
116
|
+
} from './github-actions.js';
|
|
117
|
+
|
|
118
|
+
// Template manager — Nuclei template management
|
|
119
|
+
export {
|
|
120
|
+
DEFAULT_TEMPLATE_DIR,
|
|
121
|
+
TemplateInfo,
|
|
122
|
+
TemplateCollection,
|
|
123
|
+
NucleiTemplateManager,
|
|
124
|
+
} from './template-manager.js';
|
|
@@ -0,0 +1,498 @@
|
|
|
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 OSINT Pipeline — structured OSINT workflows.
|
|
7
|
+
*
|
|
8
|
+
* - Domain intelligence and WHOIS enrichment
|
|
9
|
+
* - IP reputation and classification
|
|
10
|
+
* - Document metadata extraction (EXIF, PDF)
|
|
11
|
+
* - Investigation orchestration
|
|
12
|
+
*
|
|
13
|
+
* Ported from pipeline/osint.py (322 LOC Python).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import dns from 'node:dns';
|
|
18
|
+
import net from 'node:net';
|
|
19
|
+
import { promisify } from 'node:util';
|
|
20
|
+
|
|
21
|
+
const resolve4 = promisify(dns.resolve4);
|
|
22
|
+
const resolve6 = promisify(dns.resolve6);
|
|
23
|
+
const reversePromise = promisify(dns.reverse);
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// IP classification helper
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if an IP address is private (RFC 1918 / RFC 4193).
|
|
31
|
+
*
|
|
32
|
+
* IPv4: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8
|
|
33
|
+
* IPv6: ::1, fc00::/7, fe80::/10
|
|
34
|
+
*
|
|
35
|
+
* @param {string} ip
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
function isPrivateIP(ip) {
|
|
39
|
+
const version = net.isIP(ip);
|
|
40
|
+
if (version === 0) return false;
|
|
41
|
+
|
|
42
|
+
if (version === 4) {
|
|
43
|
+
const parts = ip.split('.').map(Number);
|
|
44
|
+
if (parts[0] === 10) return true; // 10.0.0.0/8
|
|
45
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 172.16.0.0/12
|
|
46
|
+
if (parts[0] === 192 && parts[1] === 168) return true; // 192.168.0.0/16
|
|
47
|
+
if (parts[0] === 127) return true; // 127.0.0.0/8
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// IPv6
|
|
52
|
+
const lower = ip.toLowerCase();
|
|
53
|
+
if (lower === '::1') return true;
|
|
54
|
+
|
|
55
|
+
// Expand the first 16 bits for fc00::/7 and fe80::/10 checks
|
|
56
|
+
// fc00::/7 covers fc00:: - fdff::
|
|
57
|
+
// fe80::/10 covers fe80:: - febf::
|
|
58
|
+
const expanded = _expandIPv6Prefix(lower);
|
|
59
|
+
if (expanded !== null) {
|
|
60
|
+
if (expanded >= 0xfc00 && expanded <= 0xfdff) return true; // fc00::/7
|
|
61
|
+
if (expanded >= 0xfe80 && expanded <= 0xfebf) return true; // fe80::/10
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract the first 16-bit group from an IPv6 address.
|
|
69
|
+
* @param {string} ip lowercased IPv6 address
|
|
70
|
+
* @returns {number|null}
|
|
71
|
+
*/
|
|
72
|
+
function _expandIPv6Prefix(ip) {
|
|
73
|
+
// Handle :: expansion — we only need the first group
|
|
74
|
+
const parts = ip.split(':');
|
|
75
|
+
if (!parts[0]) return 0; // starts with :: (e.g. ::1)
|
|
76
|
+
const val = parseInt(parts[0], 16);
|
|
77
|
+
return isNaN(val) ? null : val;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Data class
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
class OSINTResult {
|
|
85
|
+
/**
|
|
86
|
+
* @param {object} opts
|
|
87
|
+
* @param {string} opts.source
|
|
88
|
+
* @param {string} opts.query
|
|
89
|
+
* @param {object} [opts.data={}]
|
|
90
|
+
* @param {string} [opts.confidence='medium'] high | medium | low
|
|
91
|
+
* @param {string} [opts.timestamp]
|
|
92
|
+
* @param {string} [opts.collectionMethod='passive'] passive | active
|
|
93
|
+
*/
|
|
94
|
+
constructor(opts) {
|
|
95
|
+
this.source = opts.source;
|
|
96
|
+
this.query = opts.query;
|
|
97
|
+
this.data = opts.data ?? {};
|
|
98
|
+
this.confidence = opts.confidence ?? 'medium';
|
|
99
|
+
this.timestamp = opts.timestamp ?? new Date().toISOString();
|
|
100
|
+
this.collectionMethod = opts.collectionMethod ?? 'passive';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @type {boolean} Quick success check — true unless data contains an error. */
|
|
104
|
+
get success() {
|
|
105
|
+
return !this.data.error;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @type {string|null} Error message if present. */
|
|
109
|
+
get error() {
|
|
110
|
+
return this.data.error ?? null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
toDict() {
|
|
114
|
+
return {
|
|
115
|
+
source: this.source,
|
|
116
|
+
query: this.query,
|
|
117
|
+
data: this.data,
|
|
118
|
+
confidence: this.confidence,
|
|
119
|
+
timestamp: this.timestamp,
|
|
120
|
+
collection_method: this.collectionMethod,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Domain Intelligence
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
class DomainIntelligence {
|
|
130
|
+
/**
|
|
131
|
+
* Resolve DNS records for a domain (passive).
|
|
132
|
+
* Uses `dig` subprocess with `dns.resolve` fallback.
|
|
133
|
+
* @param {string} domain
|
|
134
|
+
* @returns {OSINTResult}
|
|
135
|
+
*/
|
|
136
|
+
static dnsLookup(domain) {
|
|
137
|
+
const data = { domain, records: {} };
|
|
138
|
+
const rtypes = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA'];
|
|
139
|
+
let hasDig = true;
|
|
140
|
+
|
|
141
|
+
for (const rtype of rtypes) {
|
|
142
|
+
if (hasDig) {
|
|
143
|
+
try {
|
|
144
|
+
const out = execFileSync('dig', ['+short', domain, rtype], {
|
|
145
|
+
encoding: 'utf-8',
|
|
146
|
+
timeout: 10000,
|
|
147
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
148
|
+
});
|
|
149
|
+
const records = out
|
|
150
|
+
.split('\n')
|
|
151
|
+
.map((r) => r.trim())
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
if (records.length > 0) {
|
|
154
|
+
data.records[rtype] = records;
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// If dig doesn't exist (ENOENT), fall through to socket-based fallback
|
|
158
|
+
if (err.code === 'ENOENT') {
|
|
159
|
+
hasDig = false;
|
|
160
|
+
// Fall through to fallback below
|
|
161
|
+
} else {
|
|
162
|
+
continue; // timeout or other error — skip this record type
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!hasDig) {
|
|
168
|
+
// Fallback: only A records via dns.resolve (sync not available, use socket)
|
|
169
|
+
if (rtype === 'A') {
|
|
170
|
+
try {
|
|
171
|
+
const { address } = dns.lookup
|
|
172
|
+
? (() => {
|
|
173
|
+
// Synchronous-ish: use execFileSync to call node
|
|
174
|
+
const out = execFileSync(
|
|
175
|
+
process.execPath,
|
|
176
|
+
['-e', `const dns=require('dns');dns.resolve4('${domain}',(e,a)=>console.log(JSON.stringify(a||[])))`],
|
|
177
|
+
{ encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
178
|
+
);
|
|
179
|
+
const ips = JSON.parse(out.trim());
|
|
180
|
+
if (ips.length > 0) data.records.A = ips;
|
|
181
|
+
return {};
|
|
182
|
+
})()
|
|
183
|
+
: {};
|
|
184
|
+
} catch {
|
|
185
|
+
// Can't resolve — skip
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
break; // No dig = only A records
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return new OSINTResult({ source: 'dns', query: domain, data, confidence: 'high' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* WHOIS lookup for domain registration info (passive).
|
|
197
|
+
* @param {string} domain
|
|
198
|
+
* @returns {OSINTResult}
|
|
199
|
+
*/
|
|
200
|
+
static whoisLookup(domain) {
|
|
201
|
+
try {
|
|
202
|
+
const out = execFileSync('whois', [domain], {
|
|
203
|
+
encoding: 'utf-8',
|
|
204
|
+
timeout: 15000,
|
|
205
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const data = { domain, raw_length: out.length };
|
|
209
|
+
|
|
210
|
+
const patterns = {
|
|
211
|
+
registrar: /Registrar:\s*(.+)/i,
|
|
212
|
+
creation_date: /Creat(?:ion|ed) Date:\s*(.+)/i,
|
|
213
|
+
expiration_date: /(?:Expir(?:ation|y) Date|Registry Expiry Date):\s*(.+)/i,
|
|
214
|
+
name_servers: /Name Server:\s*(.+)/ig,
|
|
215
|
+
status: /Status:\s*(.+)/ig,
|
|
216
|
+
registrant_org: /Registrant Organi[sz]ation:\s*(.+)/i,
|
|
217
|
+
registrant_country: /Registrant Country:\s*(.+)/i,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
for (const [key, pattern] of Object.entries(patterns)) {
|
|
221
|
+
// Use matchAll for patterns with /g, exec for single match
|
|
222
|
+
if (pattern.global) {
|
|
223
|
+
const matches = [...out.matchAll(pattern)].map((m) => m[1]);
|
|
224
|
+
if (matches.length > 0) data[key] = matches.length > 1 ? matches : matches[0];
|
|
225
|
+
} else {
|
|
226
|
+
const m = pattern.exec(out);
|
|
227
|
+
if (m) data[key] = m[1];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return new OSINTResult({ source: 'whois', query: domain, data, confidence: 'high' });
|
|
232
|
+
} catch (err) {
|
|
233
|
+
const errMsg = err.code === 'ENOENT' ? 'whois binary not found' : String(err.message || err);
|
|
234
|
+
return new OSINTResult({
|
|
235
|
+
source: 'whois',
|
|
236
|
+
query: domain,
|
|
237
|
+
data: { error: errMsg },
|
|
238
|
+
confidence: 'low',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// IP Intelligence
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
class IPIntelligence {
|
|
249
|
+
/**
|
|
250
|
+
* Reverse DNS lookup for an IP address.
|
|
251
|
+
* @param {string} ip
|
|
252
|
+
* @returns {OSINTResult}
|
|
253
|
+
*/
|
|
254
|
+
static reverseDns(ip) {
|
|
255
|
+
try {
|
|
256
|
+
// Synchronous approach: use execFileSync with node subprocess
|
|
257
|
+
const out = execFileSync(
|
|
258
|
+
process.execPath,
|
|
259
|
+
['-e', `const dns=require('dns');dns.reverse('${ip}',(e,h)=>console.log(JSON.stringify(e?null:h[0])))`],
|
|
260
|
+
{ encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
261
|
+
);
|
|
262
|
+
const hostname = JSON.parse(out.trim());
|
|
263
|
+
return new OSINTResult({
|
|
264
|
+
source: 'reverse_dns',
|
|
265
|
+
query: ip,
|
|
266
|
+
data: { ip, hostname },
|
|
267
|
+
confidence: hostname ? 'high' : 'low',
|
|
268
|
+
});
|
|
269
|
+
} catch {
|
|
270
|
+
return new OSINTResult({
|
|
271
|
+
source: 'reverse_dns',
|
|
272
|
+
query: ip,
|
|
273
|
+
data: { ip, hostname: null },
|
|
274
|
+
confidence: 'low',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Aggregate IP intelligence from available local tools.
|
|
281
|
+
* @param {string} ip
|
|
282
|
+
* @returns {OSINTResult}
|
|
283
|
+
*/
|
|
284
|
+
static ipInfo(ip) {
|
|
285
|
+
const data = { ip };
|
|
286
|
+
const version = net.isIP(ip);
|
|
287
|
+
|
|
288
|
+
if (version === 0) {
|
|
289
|
+
data.error = 'Invalid IP address';
|
|
290
|
+
return new OSINTResult({ source: 'ip_info', query: ip, data, confidence: 'low' });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
data.version = version;
|
|
294
|
+
data.is_private = isPrivateIP(ip);
|
|
295
|
+
data.is_loopback = (version === 4 && ip.startsWith('127.')) || ip === '::1';
|
|
296
|
+
data.is_multicast = _isMulticast(ip, version);
|
|
297
|
+
|
|
298
|
+
// Reverse DNS (best effort)
|
|
299
|
+
try {
|
|
300
|
+
const out = execFileSync(
|
|
301
|
+
process.execPath,
|
|
302
|
+
['-e', `const dns=require('dns');dns.reverse('${ip}',(e,h)=>console.log(JSON.stringify(e?null:h?h[0]:null)))`],
|
|
303
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
304
|
+
);
|
|
305
|
+
data.hostname = JSON.parse(out.trim());
|
|
306
|
+
} catch {
|
|
307
|
+
data.hostname = null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return new OSINTResult({ source: 'ip_info', query: ip, data, confidence: 'high' });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if IP is multicast.
|
|
316
|
+
* IPv4: 224.0.0.0 - 239.255.255.255
|
|
317
|
+
* IPv6: ff00::/8
|
|
318
|
+
* @param {string} ip
|
|
319
|
+
* @param {number} version
|
|
320
|
+
* @returns {boolean}
|
|
321
|
+
*/
|
|
322
|
+
function _isMulticast(ip, version) {
|
|
323
|
+
if (version === 4) {
|
|
324
|
+
const first = parseInt(ip.split('.')[0], 10);
|
|
325
|
+
return first >= 224 && first <= 239;
|
|
326
|
+
}
|
|
327
|
+
return ip.toLowerCase().startsWith('ff');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Document Metadata
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
class DocumentMetadata {
|
|
335
|
+
/**
|
|
336
|
+
* Extract EXIF metadata from an image file.
|
|
337
|
+
* @param {string} filepath
|
|
338
|
+
* @returns {OSINTResult}
|
|
339
|
+
*/
|
|
340
|
+
static extractExif(filepath) {
|
|
341
|
+
try {
|
|
342
|
+
const out = execFileSync('exiftool', ['-json', filepath], {
|
|
343
|
+
encoding: 'utf-8',
|
|
344
|
+
timeout: 15000,
|
|
345
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
346
|
+
});
|
|
347
|
+
const metadata = JSON.parse(out);
|
|
348
|
+
return new OSINTResult({
|
|
349
|
+
source: 'exif',
|
|
350
|
+
query: filepath,
|
|
351
|
+
data: { metadata: Array.isArray(metadata) ? metadata[0] : metadata },
|
|
352
|
+
confidence: 'high',
|
|
353
|
+
});
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const errMsg = err.code === 'ENOENT' ? 'exiftool not found' : 'Failed to extract EXIF';
|
|
356
|
+
return new OSINTResult({
|
|
357
|
+
source: 'exif',
|
|
358
|
+
query: filepath,
|
|
359
|
+
data: { error: errMsg },
|
|
360
|
+
confidence: 'low',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Extract metadata from a PDF file using pdfinfo.
|
|
367
|
+
* @param {string} filepath
|
|
368
|
+
* @returns {OSINTResult}
|
|
369
|
+
*/
|
|
370
|
+
static extractPdfMetadata(filepath) {
|
|
371
|
+
try {
|
|
372
|
+
const out = execFileSync('pdfinfo', [filepath], {
|
|
373
|
+
encoding: 'utf-8',
|
|
374
|
+
timeout: 15000,
|
|
375
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
376
|
+
});
|
|
377
|
+
const data = {};
|
|
378
|
+
for (const line of out.split('\n')) {
|
|
379
|
+
const idx = line.indexOf(':');
|
|
380
|
+
if (idx >= 0) {
|
|
381
|
+
data[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return new OSINTResult({
|
|
385
|
+
source: 'pdf_metadata',
|
|
386
|
+
query: filepath,
|
|
387
|
+
data,
|
|
388
|
+
confidence: 'high',
|
|
389
|
+
});
|
|
390
|
+
} catch (err) {
|
|
391
|
+
const errMsg = err.code === 'ENOENT' ? 'pdfinfo not found' : 'Failed to extract PDF metadata';
|
|
392
|
+
return new OSINTResult({
|
|
393
|
+
source: 'pdf_metadata',
|
|
394
|
+
query: filepath,
|
|
395
|
+
data: { error: errMsg },
|
|
396
|
+
confidence: 'low',
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// OSINT Pipeline — orchestrator
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
class OSINTPipeline {
|
|
407
|
+
constructor() {
|
|
408
|
+
this._results = [];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Run full domain investigation pipeline.
|
|
413
|
+
* @param {string} domain
|
|
414
|
+
* @returns {OSINTResult[]}
|
|
415
|
+
*/
|
|
416
|
+
investigateDomain(domain) {
|
|
417
|
+
const results = [];
|
|
418
|
+
results.push(DomainIntelligence.dnsLookup(domain));
|
|
419
|
+
results.push(DomainIntelligence.whoisLookup(domain));
|
|
420
|
+
|
|
421
|
+
// Pivot: resolve IPs and investigate them
|
|
422
|
+
const dnsResult = results[0];
|
|
423
|
+
const aRecords = dnsResult.data?.records?.A ?? [];
|
|
424
|
+
for (const ip of aRecords.slice(0, 5)) {
|
|
425
|
+
results.push(IPIntelligence.ipInfo(ip));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this._results.push(...results);
|
|
429
|
+
return results;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Run IP investigation pipeline.
|
|
434
|
+
* @param {string} ip
|
|
435
|
+
* @returns {OSINTResult[]}
|
|
436
|
+
*/
|
|
437
|
+
investigateIp(ip) {
|
|
438
|
+
const results = [IPIntelligence.ipInfo(ip), IPIntelligence.reverseDns(ip)];
|
|
439
|
+
this._results.push(...results);
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Extract metadata from a file.
|
|
445
|
+
* @param {string} filepath
|
|
446
|
+
* @returns {OSINTResult}
|
|
447
|
+
*/
|
|
448
|
+
extractMetadata(filepath) {
|
|
449
|
+
const result = filepath.toLowerCase().endsWith('.pdf')
|
|
450
|
+
? DocumentMetadata.extractPdfMetadata(filepath)
|
|
451
|
+
: DocumentMetadata.extractExif(filepath);
|
|
452
|
+
this._results.push(result);
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Return all investigation results.
|
|
458
|
+
* @returns {object[]}
|
|
459
|
+
*/
|
|
460
|
+
getAllResults() {
|
|
461
|
+
return this._results.map((r) => r.toDict());
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Investigation summary statistics.
|
|
466
|
+
* @returns {object}
|
|
467
|
+
*/
|
|
468
|
+
summary() {
|
|
469
|
+
const bySource = {};
|
|
470
|
+
const byConfidence = {};
|
|
471
|
+
for (const r of this._results) {
|
|
472
|
+
bySource[r.source] = (bySource[r.source] ?? 0) + 1;
|
|
473
|
+
byConfidence[r.confidence] = (byConfidence[r.confidence] ?? 0) + 1;
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
total_results: this._results.length,
|
|
477
|
+
by_source: bySource,
|
|
478
|
+
by_confidence: byConfidence,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// Exports
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
export {
|
|
488
|
+
// Helper
|
|
489
|
+
isPrivateIP,
|
|
490
|
+
// Data class
|
|
491
|
+
OSINTResult,
|
|
492
|
+
// Intelligence modules
|
|
493
|
+
DomainIntelligence,
|
|
494
|
+
IPIntelligence,
|
|
495
|
+
DocumentMetadata,
|
|
496
|
+
// Pipeline
|
|
497
|
+
OSINTPipeline,
|
|
498
|
+
};
|