Files
DECNET/decnet/prober/osfp/base.py
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
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.
2026-05-22 21:04:16 -04:00

61 lines
2.1 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""OS-fingerprint provider protocol + OsMatch result shape.
Each concrete provider (p0f v2 today; nmap-osdb / DECNET-observed DB
later) implements `Provider`. Callers go through
:func:`decnet.prober.osfp.factory.get_provider` or
:func:`decnet.prober.osfp.factory.get_all_providers` — direct imports
of a concrete class are forbidden, mirroring the convention in
``decnet/geoip`` and ``decnet/bus``.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Optional
@dataclass(frozen=True)
class OsMatch:
"""The result of matching an observation against a provider's DB.
Consumers should prefer higher ``confidence``. Providers compute
confidence as the fraction of signature fields that matched exactly
(vs. wildcard / modulo / "any" predicates) — a signature with every
field constrained scoring 1.0, one with every field wildcarded
approaching 0.0. This is explicit so the profiler can pick the
most-specific match when multiple providers fire.
"""
os: str
flavor: str
confidence: float
provider: str
is_userland: bool = False
def __str__(self) -> str:
tag = "userland" if self.is_userland else self.os
return f"{tag} {self.flavor} ({self.confidence:.2f} via {self.provider})"
class Provider(ABC):
"""Abstract OS-fingerprint source.
Providers consume a dict of observed TCP/IP quirks (``window``,
``wscale``, ``mss``, ``options_sig``, ``ttl``, ``df``,
``total_len``, ``quirks`` — not all fields required) and return a
best-match :class:`OsMatch` or ``None`` when nothing matches.
Providers MUST NOT raise on malformed or partial input — the
upstream caller (`profiler/fingerprint.py::sniffer_rollup`) runs
on data that may be missing any or all fields depending on the
event mix, and a raising provider would wedge every attacker
profile rebuild. Return ``None`` instead.
"""
name: str
@abstractmethod
def match(self, obs: dict[str, Any]) -> Optional[OsMatch]:
"""Return best-match OsMatch for *obs*, or None."""