mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 15:21:30 +02:00
An open port with no name is a dead end. The IANA Service Name and Transport Protocol Port Number Registry (public domain, no key) fills the ~5,800 TCP ports our curated map does not cover, committed as a generated data file because it is small and slow-moving; scripts/fetch_port_registry.py rebuilds it. IANA is the fallback vocabulary, never the authority. It still calls 9200 wap-wsp and 5601 esmagent while the internet runs Elasticsearch and Kibana there, so WELL_KNOWN wins and gained ASM-framed descriptions of its own — "Docker API without TLS, equivalent to root on the host" beats "Docker REST API (plain text)". 80, 443, kibana, nessus, consul, kubelet and rabbitmq are now named rather than falling through to a generic "http". Provenance is carried, not hidden: describe() returns (text, from_registry) and a registered name is presented as "IANA registration for port N. This scan did not confirm it." — because IANA says port 12345 is a chat system. A name the UI shows is always a name `service:<name>` can find, so the registry writes through service_for_port into the stored column and a migration backfills rows written before it existed. Services also get an identity mark: the vendored Wappalyzer logo when the service is a product, the class glyph when it is a protocol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnC2xMMzV6WQnpDp5zcXoU
87 lines
2.9 KiB
Python
Executable File
87 lines
2.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Regenerate shared/definitions/data/port_registry.json from the IANA port registry.
|
|
|
|
The registry is public domain and slow-moving, so the generated file is committed
|
|
rather than fetched at build time. Run this when IANA publishes an update:
|
|
|
|
python3 scripts/fetch_port_registry.py
|
|
|
|
IANA is the fallback vocabulary only. shared/definitions/ports.py:WELL_KNOWN stays
|
|
authoritative, because IANA still calls 9200 "wap-wsp" and 5601 "esmagent" while the
|
|
internet uses those ports for Elasticsearch and Kibana.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import io
|
|
import json
|
|
import re
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
SOURCE = (
|
|
"https://www.iana.org/assignments/service-names-port-numbers/"
|
|
"service-names-port-numbers.csv"
|
|
)
|
|
TARGET = (
|
|
Path(__file__).resolve().parents[1] / "shared/definitions/data/port_registry.json"
|
|
)
|
|
PROTOCOLS = ("tcp",)
|
|
MAX_DESCRIPTION = 120
|
|
UNASSIGNED = {"", "reserved", "unassigned", "de-registered", "deregistered"}
|
|
|
|
_ALNUM = re.compile(r"[^a-z0-9]")
|
|
# IANA appends its own registration housekeeping to 94 descriptions; it says nothing
|
|
# about the service
|
|
_HOUSEKEEPING = re.compile(
|
|
r"\s*IANA assigned this well-formed service name as a replacement for.*$", re.I
|
|
)
|
|
|
|
|
|
def _same(a: str, b: str) -> bool:
|
|
return _ALNUM.sub("", a.lower()) == _ALNUM.sub("", b.lower())
|
|
|
|
|
|
def build(raw: str) -> dict[str, list[str]]:
|
|
out: dict[str, list[str]] = {}
|
|
for row in csv.DictReader(io.StringIO(raw)):
|
|
if (row["Transport Protocol"] or "").strip().lower() not in PROTOCOLS:
|
|
continue
|
|
number = (row["Port Number"] or "").strip()
|
|
name = (row["Service Name"] or "").strip().lower()
|
|
description = _HOUSEKEEPING.sub(
|
|
"", " ".join((row["Description"] or "").split())
|
|
)
|
|
if not number.isdigit() or not name or number in out:
|
|
continue
|
|
if description.lower() in UNASSIGNED or len(description) > MAX_DESCRIPTION:
|
|
continue
|
|
# a description that only restates the name carries nothing the UI cannot infer
|
|
out[number] = [name] if _same(description, name) else [name, description]
|
|
return out
|
|
|
|
|
|
def main() -> int:
|
|
with urllib.request.urlopen(SOURCE, timeout=60) as response: # noqa: S310
|
|
raw = response.read().decode("utf-8")
|
|
registry = build(raw)
|
|
if len(registry) < 4000: # noqa: PLR2004
|
|
print(
|
|
f"refusing to write a suspiciously small registry ({len(registry)} ports)"
|
|
)
|
|
return 1
|
|
TARGET.parent.mkdir(parents=True, exist_ok=True)
|
|
TARGET.write_text(
|
|
json.dumps(registry, separators=(",", ":"), sort_keys=True) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
described = sum(1 for entry in registry.values() if len(entry) > 1)
|
|
print(f"{TARGET}: {len(registry)} ports, {described} with a description")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|