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.
133 lines
4.9 KiB
Python
133 lines
4.9 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""MITRE ATT&CK Terms of Use compliance: LICENSE.txt is fetched, verified, and required.
|
|
|
|
Bundle and license live side-by-side in the cache dir. The bundle is
|
|
fail-closed on hash mismatch (drift = mistagging risk); the license
|
|
is logged-and-refreshed on hash mismatch (drift = MITRE updated the
|
|
text, not a security event), but its *presence* is mandatory.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from decnet.ttp import attack_stix
|
|
from decnet.ttp.attack_version import (
|
|
ATTACK_LICENSE_FILENAME,
|
|
ATTACK_LICENSE_SHA256,
|
|
)
|
|
|
|
_REPO_BUNDLE = Path(__file__).resolve().parents[2] / "enterprise-attack-19.0.json"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_loader_state() -> None:
|
|
attack_stix._data = None
|
|
attack_stix._loaded_path = None
|
|
attack_stix._attack_pattern_by_id.cache_clear()
|
|
attack_stix._tactic_by_id.cache_clear()
|
|
attack_stix._tactic_by_short_name.cache_clear()
|
|
|
|
|
|
def _write_dummy_license(path: Path) -> str:
|
|
text = "placeholder license content for tests"
|
|
path.write_text(text, encoding="utf-8")
|
|
return hashlib.sha256(text.encode()).hexdigest()
|
|
|
|
|
|
def test_license_filename_constant() -> None:
|
|
assert ATTACK_LICENSE_FILENAME == "LICENSE.txt"
|
|
|
|
|
|
def test_resolve_bundle_path_with_override_and_sibling_license(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Operator points DECNET_ATTACK_BUNDLE at a file with LICENSE.txt next to it — happy path."""
|
|
bundle = tmp_path / "enterprise-attack-19.0.json"
|
|
bundle.write_bytes(_REPO_BUNDLE.read_bytes())
|
|
license_path = tmp_path / ATTACK_LICENSE_FILENAME
|
|
_write_dummy_license(license_path)
|
|
|
|
monkeypatch.setenv("DECNET_ATTACK_BUNDLE", str(bundle))
|
|
monkeypatch.delenv("DECNET_ATTACK_LICENSE", raising=False)
|
|
# Empty cache dir so override-mode resolves license from sibling.
|
|
monkeypatch.setenv("DECNET_ATTACK_CACHE_DIR", str(tmp_path / "cache"))
|
|
|
|
resolved = attack_stix.resolve_bundle_path()
|
|
assert resolved == bundle
|
|
assert attack_stix.loaded_license_path() == license_path
|
|
|
|
|
|
def test_resolve_bundle_path_via_decnet_attack_license_env(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""DECNET_ATTACK_LICENSE points to an arbitrary path — accepted."""
|
|
bundle = tmp_path / "bundle" / "enterprise-attack-19.0.json"
|
|
bundle.parent.mkdir()
|
|
bundle.write_bytes(_REPO_BUNDLE.read_bytes())
|
|
explicit_license = tmp_path / "license_elsewhere.txt"
|
|
_write_dummy_license(explicit_license)
|
|
|
|
monkeypatch.setenv("DECNET_ATTACK_BUNDLE", str(bundle))
|
|
monkeypatch.setenv("DECNET_ATTACK_LICENSE", str(explicit_license))
|
|
monkeypatch.setenv("DECNET_ATTACK_CACHE_DIR", str(tmp_path / "cache"))
|
|
|
|
resolved = attack_stix.resolve_bundle_path()
|
|
assert resolved == bundle
|
|
assert attack_stix.loaded_license_path() == explicit_license
|
|
|
|
|
|
def test_decnet_attack_license_pointing_to_missing_file_raises(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
bundle = tmp_path / "enterprise-attack-19.0.json"
|
|
bundle.write_bytes(_REPO_BUNDLE.read_bytes())
|
|
monkeypatch.setenv("DECNET_ATTACK_BUNDLE", str(bundle))
|
|
monkeypatch.setenv("DECNET_ATTACK_LICENSE", str(tmp_path / "nope.txt"))
|
|
monkeypatch.setenv("DECNET_ATTACK_CACHE_DIR", str(tmp_path / "cache"))
|
|
|
|
with pytest.raises(attack_stix.AttackBundleError) as exc:
|
|
attack_stix.resolve_bundle_path()
|
|
assert "Terms of Use" in str(exc.value)
|
|
|
|
|
|
def test_loaded_license_path_returns_none_when_absent(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.delenv("DECNET_ATTACK_BUNDLE", raising=False)
|
|
monkeypatch.delenv("DECNET_ATTACK_LICENSE", raising=False)
|
|
monkeypatch.setenv("DECNET_ATTACK_CACHE_DIR", str(tmp_path))
|
|
assert attack_stix.loaded_license_path() is None
|
|
|
|
|
|
def test_pinned_license_sha_matches_repo_committed_text() -> None:
|
|
"""The pinned hash in attack_version.py is a 64-char lowercase hex sha256."""
|
|
assert len(ATTACK_LICENSE_SHA256) == 64
|
|
assert all(c in "0123456789abcdef" for c in ATTACK_LICENSE_SHA256)
|
|
|
|
|
|
def test_cli_license_subcommand_prints_cached_license(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
license_path = tmp_path / ATTACK_LICENSE_FILENAME
|
|
license_path.write_text("MITRE Corporation grants you a license\n", encoding="utf-8")
|
|
monkeypatch.setenv("DECNET_ATTACK_LICENSE", str(license_path))
|
|
|
|
rc = attack_stix.main(["license"])
|
|
assert rc == 0
|
|
out = capsys.readouterr().out
|
|
assert "MITRE Corporation" in out
|
|
|
|
|
|
def test_cli_license_returns_nonzero_when_not_present(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.delenv("DECNET_ATTACK_LICENSE", raising=False)
|
|
monkeypatch.delenv("DECNET_ATTACK_BUNDLE", raising=False)
|
|
monkeypatch.setenv("DECNET_ATTACK_CACHE_DIR", str(tmp_path / "empty"))
|
|
|
|
rc = attack_stix.main(["license"])
|
|
assert rc == 1
|