diff --git a/CHANGELOG.md b/CHANGELOG.md index a58376e..f5766e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to `since`. Format loosely follows Keep a Changelog. +## [0.4.10] — 2026-07-27 + +Found by reading the trial machine's own digest, minutes after shipping 0.4.9. Suite 692 → **696**. + +**Severity now follows reachability for a gained port, not just a new listener.** 0.4.8 taught the +tool that a loopback-only binding is a different claim from `*:4444` — but wired that rule into the +`added` path only. So an ssh local port-forward showed up as `listener ssh now ALSO on port(s) +127.0.0.1:8080` at ORANGE, above the notify threshold, while the identical binding on a brand new +process was YELLOW. Same binding, same reachability, two different answers depending on whether +the process was already in the baseline. A gained port that is loopback-only is now YELLOW; one +public port among loopback ones keeps the whole finding ORANGE; and a bare port from a pre-0.4.8 +snapshot is still treated as unknown, therefore loud. + ## [0.4.9] — 2026-07-27 Day 3 of the trial, and both fixes are things only a multi-day run on real machines could have diff --git a/since.py b/since.py index 20b6772..55e1f76 100755 --- a/since.py +++ b/since.py @@ -59,7 +59,7 @@ from datetime import datetime, timedelta from pathlib import Path -__version__ = "0.4.9" +__version__ = "0.4.10" SCHEMA_VERSION = 5 # 4: snap['tools'] (tool identity); 5: snap['blob_flags'] if sys.version_info < (3, 9): # uses PEP 585 generics in annotations + os.replace @@ -2046,7 +2046,17 @@ def _eph(bindings): for b in bindings if b) if added_ports and removed_ports and _eph(added_ports) and _eph(removed_ports): continue - level = ORANGE if added_ports else YELLOW + # Locality decides severity for a GAINED port exactly as it does for a brand + # new listener — v0.4.8 applied that rule only to `added`, so an ssh local + # port-forward (`ssh now ALSO on 127.0.0.1:8080`) still fired ORANGE and + # notified, while the identical binding on a new process was YELLOW. A port + # reachable only from this machine is the same claim either way. Bare ports + # from a pre-v0.4.8 snapshot are not local by `binding_is_local`, so an + # unknown binding stays loud. + if added_ports and not all(binding_is_local(b) for b in added_ports): + level = ORANGE + else: + level = YELLOW if added_ports: extra["added_ports"] = added_ports if removed_ports: diff --git a/tests/test_since.py b/tests/test_since.py index 6a5d274..4072afb 100644 --- a/tests/test_since.py +++ b/tests/test_since.py @@ -2188,3 +2188,34 @@ def test_exposure_change_is_reported_once_both_sides_are_addressed(): snap(collectors={"listening": {"x": "*:5000"}})) if x["category"] == "listening"] assert f, "a service becoming publicly reachable must be reported" + + +# The trial's own digest reported `listener ssh now ALSO on 127.0.0.1:8080` at ORANGE — a local +# port-forward, notifying. v0.4.8 taught severity to follow reachability but only on the `added` +# path, so the same binding was YELLOW as a new listener and ORANGE as a gained port. +def _listen_change(old, new): + return [x for x in since.build_findings(snap(collectors={"listening": {"ssh": old}}), + snap(collectors={"listening": {"ssh": new}})) + if x["category"] == "listening"] + + +def test_gained_loopback_port_does_not_notify(): + f = _listen_change("*:22", "*:22,127.0.0.1:8080") + assert len(f) == 1 and f[0]["level"] == since.YELLOW + + +def test_gained_public_port_still_notifies(): + f = _listen_change("*:22", "*:22,*:8080") + assert len(f) == 1 and f[0]["level"] >= since.ORANGE + + +def test_mixed_gain_is_loud(): + """One publicly-bound port among loopback ones must not be averaged away.""" + f = _listen_change("*:22", "*:22,127.0.0.1:8080,*:9090") + assert len(f) == 1 and f[0]["level"] >= since.ORANGE + + +def test_gained_port_of_unknown_locality_stays_loud(): + """Bare port from a pre-v0.4.8 snapshot: unknown binding is never assumed safe.""" + f = _listen_change("22", "22,8080") + assert len(f) == 1 and f[0]["level"] >= since.ORANGE