UNPKG

@iocium/ioc-diff

Version:

A full-featured, ESM-compatible IOC diffing and normalization library + CLI for InfoSec tooling.

101 lines (99 loc) 3.1 kB
// src/utils.ts import validator from "validator"; function normalizeIOC(ioc) { return { ...ioc, value: ioc.value.trim().toLowerCase(), type: ioc.type?.toLowerCase() }; } function iocKey(ioc, matchBy) { const norm = normalizeIOC(ioc); return matchBy === "value+type" ? `${norm.value}|${norm.type}` : norm.value; } function isDifferent(a, b, compareTags, compareSeverity) { if (compareTags && JSON.stringify((a.tags || []).sort()) !== JSON.stringify((b.tags || []).sort())) return true; if (compareSeverity && a.severity !== b.severity) return true; return false; } function isValidDomain(value) { return validator.isFQDN(value); } function parsePlainIOCs(lines) { const seen = /* @__PURE__ */ new Set(); const iocs = []; for (const raw of lines) { const value = raw.trim(); if (!value || value.startsWith("#")) continue; const lower = value.toLowerCase(); let type; if (validator.isIP(lower)) type = "ip"; else if (validator.isHash(lower, "sha256")) type = "sha256"; else if (validator.isHash(lower, "md5")) type = "md5"; else if (validator.isURL(lower, { require_protocol: true })) type = "url"; else if (validator.isEmail(lower)) type = "email"; else if (isValidDomain(lower)) type = "domain"; else continue; const key = `${lower}|${type}`; if (seen.has(key)) continue; seen.add(key); iocs.push({ value, type }); } return iocs; } // src/diffIOCs.ts import stringSimilarity from "string-similarity"; function diffIOCs(oldIOCs, newIOCs, options = {}) { const matchBy = options.matchBy || "value"; const compareTags = options.compareTags || false; const compareSeverity = options.compareSeverity || false; const fuzzyMatch = options.fuzzyMatch || false; const fuzzyThreshold = options.fuzzyThreshold ?? 0.85; const oldMap = /* @__PURE__ */ new Map(); const newMap = /* @__PURE__ */ new Map(); for (const ioc of oldIOCs) { oldMap.set(iocKey(ioc, matchBy), ioc); } for (const ioc of newIOCs) { newMap.set(iocKey(ioc, matchBy), ioc); } const added = []; const removed = []; const changed = []; for (const [key, newIOC] of newMap.entries()) { if (!oldMap.has(key)) { let matched = false; if (fuzzyMatch) { for (const [oldKey, oldIOC] of oldMap.entries()) { const similarity = stringSimilarity.compareTwoStrings( iocKey(newIOC, matchBy), iocKey(oldIOC, matchBy) ); if (similarity >= fuzzyThreshold) { changed.push({ before: oldIOC, after: newIOC }); matched = true; break; } } } if (!matched) { added.push(newIOC); } } else { const oldIOC = oldMap.get(key); if (isDifferent(oldIOC, newIOC, compareTags, compareSeverity)) { changed.push({ before: oldIOC, after: newIOC }); } } } for (const [key, oldIOC] of oldMap.entries()) { if (!newMap.has(key)) { removed.push(oldIOC); } } return { added, removed, changed }; } export { diffIOCs, parsePlainIOCs };