admin / SAynapse-Horizon
publicSelf Hosted Cyber Threat Intelligence Hub
SAynapse-Horizon / Synapse-Horizon-v2 / backend / src / normalize.js
4217 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | // ----------------------------------------------------------------------------- // Shared normalization primitives. // // Every source fetcher converts its native schema into the uniform IOC object // defined by `makeIoc`. This is the ONLY shape the frontend consumes, so URLhaus // URLs, ThreatFox C2 IPs and CISA KEV CVEs all render through the same components. // ----------------------------------------------------------------------------- let SEQ = 0 export const uid = (prefix) => `${prefix}-${Date.now().toString(36)}-${(SEQ++).toString(36)}` export const SEVERITY = { CRITICAL: 'critical', HIGH: 'high', MEDIUM: 'medium', LOW: 'low' } // Minimal MITRE ATT&CK technique-name lookup for enrichment (OTX gives us IDs). const MITRE_NAMES = { T1071: 'Application Layer Protocol', 'T1071.001': 'Application Layer Protocol: Web Protocols', T1105: 'Ingress Tool Transfer', T1190: 'Exploit Public-Facing Application', T1204: 'User Execution', 'T1204.002': 'User Execution: Malicious File', T1566: 'Phishing', 'T1566.002': 'Phishing: Spearphishing Link', T1110: 'Brute Force', 'T1110.001': 'Brute Force: Password Guessing', T1583: 'Acquire Infrastructure', 'T1583.004': 'Acquire Infrastructure: Server', T1486: 'Data Encrypted for Impact', T1059: 'Command and Scripting Interpreter', T1041: 'Exfiltration Over C2 Channel', } export const mitreName = (id) => MITRE_NAMES[id] || 'ATT&CK Technique' /** Turn a list of technique IDs (strings or {id}) into [{id,name}]. */ export function mapMitre(ids = []) { return ids .map((t) => (typeof t === 'string' ? t : t?.id || t?.technique_id)) .filter(Boolean) .slice(0, 6) .map((id) => ({ id, name: mitreName(id) })) } /** Coerce assorted date inputs to an ISO string, defaulting to now. */ export function toIso(value) { if (!value) return new Date().toISOString() const d = new Date(value) return Number.isNaN(d.getTime()) ? new Date().toISOString() : d.toISOString() } /** Clamp a confidence-like value into 0–100. */ export function clampConfidence(n, fallback = 60) { const v = Number(n) if (Number.isNaN(v)) return fallback return Math.max(0, Math.min(100, Math.round(v))) } /** * Build one normalized IOC. Callers supply the semantic fields; id / ingestedAt * and safe defaults for optional structures are filled here. */ export function makeIoc(source, f) { return { id: uid(source), source, detectedAt: toIso(f.detectedAt), ingestedAt: new Date().toISOString(), severity: f.severity || SEVERITY.LOW, category: f.category || 'Indicator', indicator: String(f.indicator ?? '—'), indicatorType: f.indicatorType || 'unknown', target: f.target || f.indicator || '—', confidence: clampConfidence(f.confidence), mitre: f.mitre?.length ? f.mitre : [], headline: f.headline || `${f.category || 'Indicator'}: ${f.indicator}`, raw: f.raw ?? {}, stix: f.stix ?? { type: 'indicator', labels: ['malicious-activity'] }, } } // --- Live fetch helper ------------------------------------------------------- const DEFAULT_TIMEOUT = 12_000 const USER_AGENT = 'Synapse-Horizon/1.0 (+cti-dashboard)' /** * fetch() with a hard timeout and a descriptive error on non-2xx. * @returns {Promise<Response>} */ export async function fetchWithTimeout(url, opts = {}, timeout = DEFAULT_TIMEOUT) { const ctrl = new AbortController() const timer = setTimeout(() => ctrl.abort(), timeout) try { const res = await fetch(url, { ...opts, signal: ctrl.signal, headers: { 'User-Agent': USER_AGENT, ...(opts.headers || {}) }, }) if (!res.ok) { throw new Error(`HTTP ${res.status} ${res.statusText} for ${hostOf(url)}`) } return res } finally { clearTimeout(timer) } } export const hostOf = (url) => { try { return new URL(url).host } catch { return url } } /** Standard result envelope every fetcher returns. */ export const ok = (iocs, message) => ({ status: 'ok', count: iocs.length, iocs, message }) export const unconfigured = (message) => ({ status: 'unconfigured', count: 0, iocs: [], message }) export const failed = (message) => ({ status: 'error', count: 0, iocs: [], message }) |