import { type FilterState } from "src/store";
import { type HoneypotEvent } from "src/types";
import { formatTimestamp, ipToInt, maskIp } from "./formatting";
export interface ChartDataPoint {
id: number;
time: Date;
time_with_ms: string;
dst_port: number;
remote_addr: string;
remote_addr_int: number;
event: string;
country?: string;
city?: string;
latitude?: number;
longitude?: number;
}
export function isIpInSubnet(ip: string, subnet: string): boolean {
if (!subnet.includes("/")) return ip === subnet;
const [range, bits] = subnet.split("/");
if (!range || !bits) return false;
const maskBits = parseInt(bits);
if (isNaN(maskBits)) return ip === range;
return maskIp(ip, maskBits) === maskIp(range, maskBits);
}
export function transformToChartPoint(evt: HoneypotEvent): ChartDataPoint {
const time = new Date(evt.time);
const remoteAddr = evt.remote_addr!;
let dstPort = evt.dst_port ?? -1;
if (evt.event === "icmp_packet") {
dstPort = -1;
}
const event = evt.type + " " + evt.event;
return {
id: evt.id,
time: time,
time_with_ms: formatTimestamp(evt.time),
dst_port: dstPort,
remote_addr: remoteAddr,
remote_addr_int: ipToInt(remoteAddr),
event: event,
country: evt.country,
city: evt.city,
latitude: evt.latitude,
longitude: evt.longitude,
};
}
export function filterEvents(
events: HoneypotEvent[],
filterStore: FilterState,
): HoneypotEvent[] {
return events.filter((event) => {
// 1. Type filter
if (
filterStore.type &&
filterStore.type.length > 0 &&
!filterStore.type.includes(event.type)
) {
return false;
}
// 2. Event filter
if (
filterStore.event &&
filterStore.event.length > 0 &&
!filterStore.event.includes(event.event)
) {
return false;
}
// 3. Remote address filter (including ! exclusions)
const remote_addr = filterStore.remote_addr;
if (remote_addr && remote_addr.length > 0) {
const inclusions = remote_addr.filter((ip) => !ip.startsWith("!"));
const exclusions = remote_addr
.filter((ip) => ip.startsWith("!"))
.map((ip) => ip.substring(1));
if (event.remote_addr) {
if (exclusions.some((sub) => isIpInSubnet(event.remote_addr!, sub)))
return false;
if (
inclusions.length > 0 &&
!inclusions.some((sub) => isIpInSubnet(event.remote_addr!, sub))
)
return false;
} else if (inclusions.length > 0) {
return false;
}
}
// 4. Destination port filter (including ! exclusions)
if (filterStore.dst_port && filterStore.dst_port.length > 0) {
const ports = filterStore.dst_port;
const inclusions = ports.filter((p) => !p.startsWith("!"));
const exclusions = ports
.filter((p) => p.startsWith("!"))
.map((p) => p.substring(1));
const eventPort = event.dst_port ? String(event.dst_port) : undefined;
if (eventPort) {
if (exclusions.includes(eventPort)) return false;
if (inclusions.length > 0 && !inclusions.includes(eventPort))
return false;
} else if (inclusions.length > 0) {
return false;
}
}
// 5. Standard Field filters
const fieldFilters = [
"method",
"uri",
"user_agent",
"username",
"password",
"client_version",
"asn",
"country",
"city",
"domain",
"fqdn",
] as const;
for (const field of fieldFilters) {
const filterValues = (filterStore as any)[field] as string[];
if (filterValues && filterValues.length > 0) {
let eventValue: string | undefined;
if (
field === "user_agent" &&
event.type === "http" &&
event.fields?.headers
) {
const headers = event.fields.headers as Record<string, string>;
eventValue = (
headers["User-Agent"] || headers["user-agent"]
)?.toString();
} else {
// Check for top-level properties and fields map
eventValue = (
event.fields?.[field] ?? (event as any)[field]
)?.toString();
}
const inclusions = filterValues.filter((v) => !v.startsWith("!"));
const exclusions = filterValues
.filter((v) => v.startsWith("!"))
.map((v) => v.substring(1));
if (eventValue) {
if (exclusions.some((ex) => eventValue?.includes(ex))) return false;
if (
inclusions.length > 0 &&
!inclusions.some((inc) => eventValue?.includes(inc))
)
return false;
} else if (inclusions.length > 0) {
return false;
}
}
}
// 6. JSON fields filter (for custom filters)
if (filterStore.json_fields) {
for (const [field, filterValues] of Object.entries(
filterStore.json_fields,
)) {
if (filterValues && filterValues.length > 0) {
// Standardize value extraction
const eventValue = (
event.fields?.[field] ?? (event as any)[field]
)?.toString();
const inclusions = filterValues.filter((v) => !v.startsWith("!"));
const exclusions = filterValues
.filter((v) => v.startsWith("!"))
.map((v) => v.substring(1));
if (eventValue) {
if (exclusions.some((ex) => eventValue?.includes(ex))) return false;
if (
inclusions.length > 0 &&
!inclusions.some((inc) => eventValue?.includes(inc))
)
return false;
} else if (inclusions.length > 0) {
return false;
}
}
}
}
return true;
});
}
export function getSparkData(data: ChartDataPoint[]): {
x: number[];
y: number[];
} {
if (data.length === 0) return { x: [], y: [] };
const firstTime = data[0]!.time.getTime();
const lastTime = data[data.length - 1]!.time.getTime();
const points = 50;
const duration = lastTime - firstTime;
if (duration === 0) {
return { x: [firstTime], y: [data.length] };
}
const slotMs = duration / (points - 1);
const binEdges: number[] = [];
for (let i = 0; i < points; i++) {
binEdges.push(firstTime + i * slotMs);
}
const slotCounts = new Array(points).fill(0);
data.forEach((d) => {
const t = d.time.getTime();
let idx = Math.round((t - firstTime) / slotMs);
if (idx < 0) idx = 0;
if (idx >= points) idx = points - 1;
slotCounts[idx]++;
});
return { x: binEdges, y: slotCounts };
}