Replaces LICENSE (GPLv3 -> AGPLv3) and prepends `SPDX-License-Identifier: AGPL-3.0-or-later` to every source file across decnet/, decnet_web/, tests/, scripts/, and tools/. Rationale: closes the GPLv3 ASP loophole so any party operating a modified DECNET as a network service must offer their modified source. Personal copyright (Samuel Paschuan) + inbound=outbound contributions make a future unilateral relicense infeasible. - LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt) - COPYRIGHT: project copyright notice - tools/add_spdx_headers.py: idempotent header injector (shebang- and PEP 263-aware) Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh). No behavior change; comments only.
154 lines
5.5 KiB
Python
154 lines
5.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Tests for the p0f v2 .fp parser (decnet/prober/osfp/p0f/format.py)."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from decnet.prober.osfp.p0f.format import P0fParseError, _parse_line, parse_p0f_v2
|
|
|
|
|
|
# ─── Line-parser unit tests ──────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_line_minimal_literal() -> None:
|
|
sig = _parse_line("5840:64:1:60:M1460,S,T,N,W7:.:Linux:2.6.x kernel")
|
|
assert sig.os == "Linux"
|
|
assert sig.flavor == "2.6.x kernel"
|
|
assert sig.ttl == 64
|
|
assert sig.df is True
|
|
assert sig.wss.kind == "literal" and sig.wss.value == 5840
|
|
assert sig.total_len.kind == "literal" and sig.total_len.value == 60
|
|
assert len(sig.options) == 5
|
|
# First option: MSS=1460
|
|
mss_opt = sig.options[0]
|
|
assert mss_opt.kind == "M"
|
|
assert mss_opt.value is not None and mss_opt.value.value == 1460
|
|
assert sig.quirks == frozenset()
|
|
assert not sig.is_userland
|
|
|
|
|
|
def test_parse_line_wildcard_window() -> None:
|
|
sig = _parse_line("*:128:1:*:M*,S,T,N,W*:.:Windows:XP SP1+")
|
|
assert sig.wss.kind == "any"
|
|
assert sig.total_len.kind == "any"
|
|
assert sig.options[0].kind == "M"
|
|
assert sig.options[0].value is not None and sig.options[0].value.kind == "any"
|
|
|
|
|
|
def test_parse_line_mss_multiple_window() -> None:
|
|
sig = _parse_line("S4:64:1:60:M*,S,T,N,W*:.:Linux:generic")
|
|
assert sig.wss.kind == "mss_mul" and sig.wss.value == 4
|
|
|
|
|
|
def test_parse_line_mtu_multiple_window() -> None:
|
|
sig = _parse_line("T3:64:1:60:M*,S,T,N,W*:.:Solaris:10")
|
|
assert sig.wss.kind == "mtu_mul" and sig.wss.value == 3
|
|
|
|
|
|
def test_parse_line_modulo_window() -> None:
|
|
sig = _parse_line("%8192:64:1:60:M*,S,T,N,W*:.:Linux:probe")
|
|
assert sig.wss.kind == "mod" and sig.wss.value == 8192
|
|
|
|
|
|
def test_parse_line_userland_prefix() -> None:
|
|
sig = _parse_line("5840:64:1:60:M*,S,T,N,W*:.:-nmap:syn stealth")
|
|
assert sig.is_userland is True
|
|
assert sig.os == "nmap"
|
|
|
|
|
|
def test_parse_line_combined_prefixes() -> None:
|
|
sig = _parse_line("5840:64:1:60:M*:.:-@Windows:fuzzy match")
|
|
assert sig.is_userland is True
|
|
assert sig.is_approximate is True
|
|
assert sig.os == "Windows"
|
|
|
|
|
|
def test_parse_line_quirks_non_empty() -> None:
|
|
sig = _parse_line("5840:64:1:60:M*,S,T,N,W*:PZ:Linux:with quirks")
|
|
assert sig.quirks == frozenset({"P", "Z"})
|
|
|
|
|
|
def test_parse_line_no_options_sentinel() -> None:
|
|
sig = _parse_line("5840:64:1:60:.:.:Linux:barebones")
|
|
assert len(sig.options) == 1
|
|
assert sig.options[0].kind == "."
|
|
|
|
|
|
def test_parse_line_t0_timestamp_distinct_from_t() -> None:
|
|
sig = _parse_line("5840:64:1:60:M*,T0:.:Linux:broken timestamps")
|
|
assert sig.options[1].kind == "T0"
|
|
|
|
|
|
def test_parse_line_unknown_option_number() -> None:
|
|
sig = _parse_line("5840:64:1:60:M*,?47:.:Weird:stack")
|
|
unknown = sig.options[1]
|
|
assert unknown.kind == "?"
|
|
assert unknown.value is not None and unknown.value.value == 47
|
|
|
|
|
|
def test_parse_line_rejects_too_few_fields() -> None:
|
|
with pytest.raises(P0fParseError):
|
|
_parse_line("5840:64:1:60")
|
|
|
|
|
|
def test_parse_line_rejects_bad_df() -> None:
|
|
with pytest.raises(P0fParseError):
|
|
_parse_line("5840:64:X:60:M*:.:Linux:bad")
|
|
|
|
|
|
def test_parse_line_rejects_bad_window_token() -> None:
|
|
with pytest.raises(P0fParseError):
|
|
_parse_line("Kfoo:64:1:60:M*:.:Linux:bad")
|
|
|
|
|
|
def test_parse_line_rejects_malformed_option() -> None:
|
|
with pytest.raises(P0fParseError):
|
|
_parse_line("5840:64:1:60:!!!wat:.:Linux:bad")
|
|
|
|
|
|
# ─── File-level tests ────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_file_skips_comments_blanks_bad_lines(tmp_path: Path) -> None:
|
|
fp = tmp_path / "test.fp"
|
|
fp.write_text(
|
|
"# comment\n"
|
|
"\n"
|
|
"5840:64:1:60:M1460,S,T,N,W7:.:Linux:2.6.x\n"
|
|
"# another comment\n"
|
|
"garbage line that should skip\n"
|
|
"8192:128:1:48:M1460,N,W0,N,N,S:.:Windows:XP\n"
|
|
)
|
|
sigs = parse_p0f_v2(fp)
|
|
assert len(sigs) == 2
|
|
assert {s.os for s in sigs} == {"Linux", "Windows"}
|
|
|
|
|
|
def test_parse_vendored_syn_db_fully_loads() -> None:
|
|
"""The full vendored p0f.fp MUST parse without losing signatures.
|
|
Upstream inventory: 262 SYN signatures. A regression that drops rows
|
|
would silently degrade OS-fingerprint coverage."""
|
|
data = Path(__file__).resolve().parents[3] / "decnet/prober/osfp/p0f/data/p0f.fp"
|
|
sigs = parse_p0f_v2(data)
|
|
assert len(sigs) == 262, f"expected 262 SYN sigs, parser returned {len(sigs)}"
|
|
|
|
|
|
def test_parse_vendored_all_four_dbs_fully_load() -> None:
|
|
"""Same invariant across all four vendored databases."""
|
|
base = Path(__file__).resolve().parents[3] / "decnet/prober/osfp/p0f/data"
|
|
expected = {"p0f.fp": 262, "p0fa.fp": 61, "p0fr.fp": 46, "p0fo.fp": 6}
|
|
for name, want in expected.items():
|
|
sigs = parse_p0f_v2(base / name)
|
|
assert len(sigs) == want, f"{name}: expected {want}, got {len(sigs)}"
|
|
|
|
|
|
def test_parse_vendored_specificity_in_range() -> None:
|
|
"""Every signature's computed specificity must land in [0, 1]."""
|
|
data = Path(__file__).resolve().parents[3] / "decnet/prober/osfp/p0f/data/p0f.fp"
|
|
for sig in parse_p0f_v2(data):
|
|
assert 0.0 <= sig.specificity <= 1.0, (
|
|
f"{sig.os}/{sig.flavor}: specificity out of range ({sig.specificity})"
|
|
)
|