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.
104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""E.2.7 — Static decoupling lint for ``decnet/ttp/``.
|
|
|
|
The "no SPOF" decoupling rule §2 of TTP_TAGGING.md: tagging code
|
|
must NEVER import an intel-provider module directly. Intel-derived
|
|
data flows through ``decnet.web.db.models`` (the ``AttackerIntel``
|
|
row), not through a function call into
|
|
``decnet.intel.{abuseipdb,greynoise,feodo,threatfox}``. A failed
|
|
provider produces an empty join, not a crash inside the tagger
|
|
worker.
|
|
|
|
The same property holds for biometrics: nothing under
|
|
``decnet/ttp/`` may reach into ``decnet.profiler.keystroke.*``.
|
|
Future biometric ingesters will land in their own subpackages with
|
|
the same DB-mediated bridge.
|
|
|
|
This is a pure AST walk — no module-level import side effects, no
|
|
runtime dependencies. The check runs on every TTP source file
|
|
(skipping ``__pycache__`` and dunder names) and surfaces a
|
|
``ImportFrom``/``Import`` violation with file path + line number on
|
|
failure.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import decnet.ttp as _ttp_pkg
|
|
|
|
|
|
_TTP_ROOT: Path = Path(_ttp_pkg.__file__).resolve().parent
|
|
|
|
# Exact module names the tagger MUST NOT import.
|
|
_FORBIDDEN_EXACT: frozenset[str] = frozenset({
|
|
"decnet.intel.abuseipdb",
|
|
"decnet.intel.greynoise",
|
|
"decnet.intel.feodo",
|
|
"decnet.intel.threatfox",
|
|
})
|
|
|
|
# Forbidden by-prefix: anything under these subpackages is off-limits.
|
|
_FORBIDDEN_PREFIXES: tuple[str, ...] = (
|
|
"decnet.profiler.keystroke",
|
|
)
|
|
|
|
|
|
def _is_forbidden(module_name: str) -> bool:
|
|
if module_name in _FORBIDDEN_EXACT:
|
|
return True
|
|
return any(
|
|
module_name == prefix or module_name.startswith(prefix + ".")
|
|
for prefix in _FORBIDDEN_PREFIXES
|
|
)
|
|
|
|
|
|
def _ttp_python_files() -> list[Path]:
|
|
files: list[Path] = []
|
|
for p in _TTP_ROOT.rglob("*.py"):
|
|
# Skip caches and any future stubs/shims that might slip in.
|
|
if "__pycache__" in p.parts:
|
|
continue
|
|
files.append(p)
|
|
return files
|
|
|
|
|
|
def test_at_least_one_ttp_file_visited() -> None:
|
|
"""Sanity guard: a future refactor that moves the package or
|
|
renames the import root must not silently neuter the lint by
|
|
walking zero files."""
|
|
files = _ttp_python_files()
|
|
assert files, f"no .py files under {_TTP_ROOT} — refactor regressed the lint"
|
|
# Spot-check that the lifters and the worker live under the root,
|
|
# otherwise the lint scope is wrong.
|
|
names = {p.name for p in files}
|
|
assert "worker.py" in names
|
|
assert "base.py" in names
|
|
|
|
|
|
@pytest.mark.parametrize("path", _ttp_python_files(), ids=lambda p: str(p.relative_to(_TTP_ROOT)))
|
|
def test_no_forbidden_imports(path: Path) -> None:
|
|
source = path.read_text(encoding="utf-8")
|
|
tree = ast.parse(source, filename=str(path))
|
|
violations: list[str] = []
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
if _is_forbidden(alias.name):
|
|
violations.append(
|
|
f"{path}:{node.lineno} import {alias.name}"
|
|
)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
mod = node.module or ""
|
|
if _is_forbidden(mod):
|
|
violations.append(
|
|
f"{path}:{node.lineno} from {mod} import …"
|
|
)
|
|
assert not violations, (
|
|
"decoupling rule §2 violated — TTP code must reach intel "
|
|
"data via decnet.web.db.models only:\n "
|
|
+ "\n ".join(violations)
|
|
)
|