@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
JavaScript
// 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
};