Catalyst / admin/SAynapse-Horizon 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / SAynapse-Horizon

public

Self Hosted Cyber Threat Intelligence Hub

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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 })