forensicnomicon 1.9.0

The ForensicNomicon — comprehensive DFIR artifact catalog: UserAssist, Shimcache, Amcache, Prefetch, $MFT, ShellBags, EVTX, NTDS.dit, SAM, SRUM, LNK, Jump Lists + KAPE/Velociraptor/Sigma/MITRE. Zero deps.
Documentation
#!/usr/bin/env python3
"""Generate src/cloud_ranges/generated.rs from the published cloud-provider IP feeds.

Fetches the authoritative, publicly-published IPv4 range feeds for AWS, Google
Cloud, and Cloudflare, flattens every CIDR to an inclusive [start, end] u32
interval tagged with its provider, then MERGES all intervals globally into a
disjoint, sorted set (on overlap the earlier-listed provider wins — negligible in
practice, cross-provider overlaps are essentially nonexistent). The disjoint
sorted output lets `classify_ipv4` be a single binary search.

Azure is intentionally omitted: its ServiceTags feed requires an authenticated,
periodically-changing download URL, so it is not a stable open-data source. This
is documented as a coverage gap in cloud_ranges/mod.rs.

Re-run to refresh the snapshot (this command IS the provenance):

    python3 tools/gen_cloud_ranges.py > src/cloud_ranges/generated.rs

Sources:
- AWS:        https://ip-ranges.amazonaws.com/ip-ranges.json  (prefixes[].ip_prefix)
- GCP:        https://www.gstatic.com/ipranges/cloud.json      (prefixes[].ipv4Prefix)
- Cloudflare: https://www.cloudflare.com/ips-v4                (one CIDR per line)
"""
import ipaddress
import json
import sys
import urllib.request
from datetime import date

AWS = "https://ip-ranges.amazonaws.com/ip-ranges.json"
GCP = "https://www.gstatic.com/ipranges/cloud.json"
CF = "https://www.cloudflare.com/ips-v4"

# Provider order = overlap-resolution priority (earlier wins on a tie).
PROVIDERS = ["Aws", "Gcp", "Cloudflare"]


def _get(url: str) -> bytes:
    req = urllib.request.Request(url, headers={"User-Agent": "forensicnomicon-gen/1.0"})
    with urllib.request.urlopen(req, timeout=60) as r:
        return r.read()


def fetch_cidrs() -> list[tuple[int, int, int]]:
    """Return [(start_u32, end_u32, provider_index)] for every source CIDR."""
    out: list[tuple[int, int, int]] = []

    def add(cidr: str, pidx: int) -> None:
        net = ipaddress.ip_network(cidr.strip(), strict=False)
        if net.version != 4:
            return
        out.append((int(net.network_address), int(net.broadcast_address), pidx))

    aws = json.loads(_get(AWS))
    for p in aws.get("prefixes", []):
        add(p["ip_prefix"], PROVIDERS.index("Aws"))

    gcp = json.loads(_get(GCP))
    for p in gcp.get("prefixes", []):
        if "ipv4Prefix" in p:
            add(p["ipv4Prefix"], PROVIDERS.index("Gcp"))

    for line in _get(CF).decode().splitlines():
        if line.strip():
            add(line, PROVIDERS.index("Cloudflare"))

    return out


def merge(intervals: list[tuple[int, int, int]]) -> list[tuple[int, int, int]]:
    """Merge into globally disjoint, sorted [start,end,provider] intervals.

    Sort by (start, provider-priority). Sweep: extend the current interval while
    the next overlaps or is adjacent; the current interval keeps its provider
    (earlier priority, since it was sorted first at an equal start)."""
    if not intervals:
        return []
    intervals.sort(key=lambda t: (t[0], t[2]))
    merged: list[list[int]] = [list(intervals[0])]
    for s, e, p in intervals[1:]:
        cur = merged[-1]
        if s <= cur[1] + 1:  # overlap or adjacent → absorb, keep current provider
            if e > cur[1]:
                cur[1] = e
        else:
            merged.append([s, e, p])
    return [(s, e, p) for s, e, p in merged]


def main() -> None:
    merged = merge(fetch_cidrs())
    today = date.today().isoformat()
    lines = [
        "// @generated by tools/gen_cloud_ranges.py from the published AWS / GCP /",
        "// Cloudflare IPv4 feeds. DO NOT EDIT BY HAND. Re-run the tool to refresh.",
        f"// Snapshot: {today}. Azure omitted (no stable open-data feed). See mod.rs.",
        "use super::CloudProvider;",
        "",
        f"/// Disjoint, ascending IPv4 ranges (inclusive [start, end] as u32) tagged with",
        f"/// their cloud provider. {len(merged)} merged intervals; snapshot {today}.",
        "#[rustfmt::skip]",
        "pub const CLOUD_RANGES: &[(u32, u32, CloudProvider)] = &[",
    ]
    for s, e, p in merged:
        lines.append(f"    ({s}, {e}, CloudProvider::{PROVIDERS[p]}),")
    lines.append("];")
    sys.stdout.write("\n".join(lines) + "\n")


if __name__ == "__main__":
    main()