Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions since.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_since.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading