"""Extract observables, not verdicts. Original values remain suitable for exact matching.""" import ipaddress import re from typing import Any from urllib.parse import urlsplit def defang(value: str, indicator_type: str) -> str: if indicator_type in {"url", "domain", "ipv4", "ipv6", "ip"}: value = re.sub(r"^https:", "hxxps:", value, flags=re.I) value = re.sub(r"^http:", "hxxp:", value, flags=re.I) value = value.replace(".", "[.]").replace(":", "[:]") return value def extract_indicators(text: str) -> list[dict[str, Any]]: if len(text) > 16777216: raise ValueError("indicator input exceeds 16 MiB; process log lines individually") found: dict[tuple[str, str], dict[str, Any]] = {} def add(kind: str, value: str, role: str = "observable", start: int | None = None) -> None: key = (kind, value) if key not in found: found[key] = dict( type=kind, value=value, defanged=defang(value, kind), role=role, observation="Text occurrence; not evidence of a connection or execution.", start=start, ) urls = list(re.finditer(r"\bhttps?://[^\s<>\"'`]+", text, re.I)) for match in urls: value = match.group().rstrip("),;}") add("url", value, "url_in_text", match.start()) try: hostname = urlsplit(value).hostname if hostname: try: parsed = ipaddress.ip_address(hostname) add( "ipv4" if parsed.version == 4 else "ipv6", hostname, "url_host", match.start(), ) except ValueError: if "." in hostname and re.fullmatch(r"[A-Za-z0-9.-]+", hostname): add("domain", hostname, "url_host", match.start()) except ValueError: pass for match in re.finditer( r"(?|;{}()]+", text): while url_index < len(urls) and urls[url_index].end() <= match.start(): url_index += 1 if ( url_index < len(urls) and urls[url_index].start() <= match.start() < urls[url_index].end() ): continue add("path", match.group(), "path_in_text", match.start()) return list(found.values())