diff --git a/decnet/bus/topics.py b/decnet/bus/topics.py index c8d83b83..64344360 100644 --- a/decnet/bus/topics.py +++ b/decnet/bus/topics.py @@ -427,7 +427,7 @@ def attacker_observation(primitive: str) -> str: ``cognitive.feedback_loop_engagement``, ``motor.shell_mastery.tab_completion``). Dotted primitives are permitted — this matches the format - ``decnet_behave_shell.spec.event_adapter.event_topic_for`` produces + ``behave_shell.spec.event_adapter.event_topic_for`` produces upstream, and DECNET's bus admits the dotted leaf the same way :func:`attacker` does for ``session.started``. diff --git a/decnet/correlation/attribution_worker.py b/decnet/correlation/attribution_worker.py index 2bba4c0a..c9c03b81 100644 --- a/decnet/correlation/attribution_worker.py +++ b/decnet/correlation/attribution_worker.py @@ -36,7 +36,7 @@ from decnet.logging import get_logger from decnet.web.db.repository import BaseRepository try: - from decnet_behave_shell.spec import ( + from behave_shell.spec import ( PRIMITIVE_REGISTRY, ValueKind, ) diff --git a/decnet/profiler/behave_shell/_features/__init__.py b/decnet/profiler/behave_shell/_features/__init__.py index b0231ef2..7e6a6583 100644 --- a/decnet/profiler/behave_shell/_features/__init__.py +++ b/decnet/profiler/behave_shell/_features/__init__.py @@ -8,7 +8,7 @@ from __future__ import annotations from typing import Callable, Iterable -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext from decnet.profiler.behave_shell._features.cognitive import ( diff --git a/decnet/profiler/behave_shell/_features/_emit.py b/decnet/profiler/behave_shell/_features/_emit.py index 33c5fa51..60581ce5 100644 --- a/decnet/profiler/behave_shell/_features/_emit.py +++ b/decnet/profiler/behave_shell/_features/_emit.py @@ -9,7 +9,7 @@ from __future__ import annotations from typing import Any -from decnet_behave_core.spec.envelope import Observation, Window +from behave_core.spec.envelope import Observation, Window from decnet.profiler.behave_shell._ctx import SessionContext diff --git a/decnet/profiler/behave_shell/_features/cognitive.py b/decnet/profiler/behave_shell/_features/cognitive.py index 9d077eac..978330ea 100644 --- a/decnet/profiler/behave_shell/_features/cognitive.py +++ b/decnet/profiler/behave_shell/_features/cognitive.py @@ -11,7 +11,7 @@ from __future__ import annotations import statistics from typing import Iterator -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext from decnet.profiler.behave_shell._features._emit import make_observation diff --git a/decnet/profiler/behave_shell/_features/emotional_valence.py b/decnet/profiler/behave_shell/_features/emotional_valence.py index c2c715fc..9e587cf4 100644 --- a/decnet/profiler/behave_shell/_features/emotional_valence.py +++ b/decnet/profiler/behave_shell/_features/emotional_valence.py @@ -15,7 +15,7 @@ from __future__ import annotations import statistics from typing import Iterator -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext from decnet.profiler.behave_shell._features._emit import make_observation diff --git a/decnet/profiler/behave_shell/_features/environmental.py b/decnet/profiler/behave_shell/_features/environmental.py index dcd3c147..5de41a07 100644 --- a/decnet/profiler/behave_shell/_features/environmental.py +++ b/decnet/profiler/behave_shell/_features/environmental.py @@ -17,7 +17,7 @@ import collections import re from typing import Iterator -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext from decnet.profiler.behave_shell._features._emit import make_observation diff --git a/decnet/profiler/behave_shell/_features/motor.py b/decnet/profiler/behave_shell/_features/motor.py index 9b6ac36e..f2b7634a 100644 --- a/decnet/profiler/behave_shell/_features/motor.py +++ b/decnet/profiler/behave_shell/_features/motor.py @@ -10,7 +10,7 @@ import statistics from itertools import chain from typing import Iterator -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext from decnet.profiler.behave_shell._features._emit import make_observation diff --git a/decnet/profiler/behave_shell/_features/operational.py b/decnet/profiler/behave_shell/_features/operational.py index 40ac6858..0ac54401 100644 --- a/decnet/profiler/behave_shell/_features/operational.py +++ b/decnet/profiler/behave_shell/_features/operational.py @@ -11,7 +11,7 @@ import collections import statistics from typing import Iterator -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext from decnet.profiler.behave_shell._features._emit import make_observation diff --git a/decnet/profiler/behave_shell/_features/temporal.py b/decnet/profiler/behave_shell/_features/temporal.py index 70be329f..0f428a56 100644 --- a/decnet/profiler/behave_shell/_features/temporal.py +++ b/decnet/profiler/behave_shell/_features/temporal.py @@ -16,7 +16,7 @@ import math import statistics from typing import Iterator -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext from decnet.profiler.behave_shell._features._emit import make_observation diff --git a/decnet/profiler/behave_shell/_handler.py b/decnet/profiler/behave_shell/_handler.py index f42d59b3..f003d27a 100644 --- a/decnet/profiler/behave_shell/_handler.py +++ b/decnet/profiler/behave_shell/_handler.py @@ -17,8 +17,8 @@ import json from pathlib import Path from typing import Any, Callable, Iterable, Optional -from decnet_behave_core.spec.envelope import Observation -from decnet_behave_shell.spec.event_adapter import event_topic_for, to_event_payload +from behave_core.spec.envelope import Observation +from behave_shell.spec.event_adapter import event_topic_for, to_event_payload from decnet.logging import get_logger from decnet.profiler.behave_shell import extract_session diff --git a/decnet/profiler/behave_shell/extract.py b/decnet/profiler/behave_shell/extract.py index c02fa6ee..163dd74b 100644 --- a/decnet/profiler/behave_shell/extract.py +++ b/decnet/profiler/behave_shell/extract.py @@ -9,7 +9,7 @@ from __future__ import annotations from typing import Iterable, Iterator -from decnet_behave_core.spec.envelope import Observation +from behave_core.spec.envelope import Observation from decnet.profiler.behave_shell._ctx import SessionContext, build_session_context from decnet.profiler.behave_shell._features import FEATURES diff --git a/decnet/ttp/attack_stix.py b/decnet/ttp/attack_stix.py index c852fca0..7591f27d 100644 --- a/decnet/ttp/attack_stix.py +++ b/decnet/ttp/attack_stix.py @@ -38,9 +38,10 @@ from dataclasses import dataclass from functools import lru_cache from pathlib import Path from threading import Lock -from typing import Final +from typing import TYPE_CHECKING, Final -from mitreattack.stix20 import MitreAttackData +if TYPE_CHECKING: + from mitreattack.stix20 import MitreAttackData from decnet.ttp.attack_version import ( ATTACK_BUNDLE_SHA256, @@ -231,6 +232,7 @@ def loaded_license_path() -> Path | None: def _load() -> MitreAttackData: + from mitreattack.stix20 import MitreAttackData # heavy — lazy on first call global _data, _loaded_path with _data_lock: if _data is not None: diff --git a/decnet/ttp/stix_custom.py b/decnet/ttp/stix_custom.py index 70a492bd..e2652ce9 100644 --- a/decnet/ttp/stix_custom.py +++ b/decnet/ttp/stix_custom.py @@ -77,7 +77,7 @@ class XDecnetBehaveProfile: evidence_ref, identity_ref (optional). ``schema_version`` matches ``OBSERVATION_SCHEMA_VERSION`` from - decnet_behave_shell.spec.envelope — bump when the envelope schema changes. + behave_shell.spec.envelope — bump when the envelope schema changes. ``kd_digraph_simhash`` is the 8-byte digraph SimHash from AttackerIdentity, hex-encoded. Null when identity has not been clustered. diff --git a/decnet/ttp/stix_export.py b/decnet/ttp/stix_export.py index 8b17e847..041bfa21 100644 --- a/decnet/ttp/stix_export.py +++ b/decnet/ttp/stix_export.py @@ -209,7 +209,7 @@ def _threat_actor( obs_list = observations or [] if obs_list or kd_hash is not None: - from decnet_behave_shell.spec.envelope import OBSERVATION_SCHEMA_VERSION + from behave_shell.spec.envelope import OBSERVATION_SCHEMA_VERSION profile_id = ( f"x-decnet-behave-profile--{_uuid.uuid5(_NS, attacker['uuid'])}" ) diff --git a/decnet/web/db/models/observations.py b/decnet/web/db/models/observations.py index 147c9ef6..8dff5521 100644 --- a/decnet/web/db/models/observations.py +++ b/decnet/web/db/models/observations.py @@ -2,7 +2,7 @@ emitted Observation envelope. Mirrors the BEHAVE-SHELL ``Observation`` Pydantic envelope -(``decnet_behave_core.spec.envelope.Observation``) field-for-field, plus +(``behave_core.spec.envelope.Observation``) field-for-field, plus one DECNET-side denormalisation (``attacker_uuid``) for cheap joins. The class is named ``ObservationRow`` to avoid colliding with the BEHAVE Pydantic class when both are imported into the same module — diff --git a/decnet/web/db/sqlmodel_repo/observations.py b/decnet/web/db/sqlmodel_repo/observations.py index ddd7bceb..e3aed4f7 100644 --- a/decnet/web/db/sqlmodel_repo/observations.py +++ b/decnet/web/db/sqlmodel_repo/observations.py @@ -13,7 +13,7 @@ Composed onto :class:`SQLModelRepository`. Three public methods: drift charts. PII discipline is the BEHAVE envelope's job -(``core/decnet_behave_core/spec/envelope.py:3-19``); this mixin does +(``core/behave_core/spec/envelope.py:3-19``); this mixin does not validate values — that happens at construction time by the BEHAVE ``Observation`` subclass before the dict reaches us. """ diff --git a/decnet/web/db/sqlmodel_repo/ttp.py b/decnet/web/db/sqlmodel_repo/ttp.py index 20255ed5..03d18a2e 100644 --- a/decnet/web/db/sqlmodel_repo/ttp.py +++ b/decnet/web/db/sqlmodel_repo/ttp.py @@ -20,8 +20,6 @@ from typing import Any from sqlalchemy import func, select from sqlmodel import col -from decnet.ttp.attack_catalog import technique_name as _technique_name -from decnet.ttp.attack_stix import mitre_url_for as _mitre_url_for from decnet.web.db.models import ( Attacker, AttackerIdentity, @@ -34,6 +32,16 @@ from decnet.web.db.models.canary import CanaryTrigger from decnet.web.db.sqlmodel_repo._helpers import _MixinBase +def _technique_name(tid: str | None) -> str | None: + from decnet.ttp.attack_catalog import technique_name # heavy — lazy on first call + return technique_name(tid) + + +def _mitre_url_for(tid: str | None) -> str | None: + from decnet.ttp.attack_stix import mitre_url_for # heavy — lazy on first call + return mitre_url_for(tid) + + # Confidence floor: tags computed below this value are silently dropped # at insert time. Pinned by tests/ttp/test_confidence.py. _CONFIDENCE_FLOOR: float = 0.3 diff --git a/decnet/web/router/attackers/api_export_attacker_misp.py b/decnet/web/router/attackers/api_export_attacker_misp.py index 7697986a..a6380f48 100644 --- a/decnet/web/router/attackers/api_export_attacker_misp.py +++ b/decnet/web/router/attackers/api_export_attacker_misp.py @@ -14,7 +14,6 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import Response from decnet.telemetry import traced as _traced -from decnet.ttp.misp_export import build_attacker_misp_event from decnet.web.dependencies import require_viewer, repo router = APIRouter() @@ -79,6 +78,7 @@ async def api_export_attacker_misp( observations = cast(list[dict[str, Any]], results[8]) fingerprint_bounties = cast(list[dict[str, Any]], results[9]) + from decnet.ttp.misp_export import build_attacker_misp_event # heavy — lazy on first call event = build_attacker_misp_event( attacker=attacker, behavior=behavior, diff --git a/decnet/web/router/attackers/api_export_attackers_misp.py b/decnet/web/router/attackers/api_export_attackers_misp.py index 22df69f2..f73b3428 100644 --- a/decnet/web/router/attackers/api_export_attackers_misp.py +++ b/decnet/web/router/attackers/api_export_attackers_misp.py @@ -19,7 +19,6 @@ from fastapi import APIRouter, Depends from fastapi.responses import Response from decnet.telemetry import traced as _traced -from decnet.ttp.misp_export import build_fleet_misp_collection from decnet.web.dependencies import require_viewer, repo router = APIRouter() @@ -49,6 +48,7 @@ async def api_export_attackers_misp( repo.get_all_observations_for_export(), repo.get_all_fingerprint_bounties_for_export(), ) + from decnet.ttp.misp_export import build_fleet_misp_collection # heavy — lazy on first call collection = build_fleet_misp_collection( rows=rows, ttp_by_attacker=ttp_by_attacker, diff --git a/pyproject.toml b/pyproject.toml index 373082f2..551a6104 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,7 +117,19 @@ decnet = "decnet.cli:app" asyncio_mode = "auto" asyncio_debug = "true" asyncio_default_fixture_loop_scope = "module" -addopts = "-m 'not fuzz and not live and not stress and not bench and not docker' -v -q -x -n logical --dist loadscope" +addopts = "-v -q -x -n logical --dist load" +norecursedirs = [ + "tests/live", + "tests/stress", + "tests/service_testing", + "tests/docker", + "tests/perf", + "__pycache__", + ".git", + "node_modules", + ".venv", + ".311", +] markers = [ "fuzz: hypothesis-based fuzz tests (slow, run with -m fuzz or -m '' for all)", "live: live subprocess service tests (run with -m live)", diff --git a/tests/api/conftest.py b/tests/api/conftest.py index 0d349c62..8a52beee 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -14,12 +14,13 @@ import os as _os def pytest_ignore_collect(collection_path, config): - """Skip test_schemathesis.py unless fuzz marker is selected. + """Skip all test_schemathesis*.py files unless fuzz marker is selected. - Its module-level code starts a subprocess server and mutates - decnet.web.auth.SECRET_KEY, which poisons other test suites. + These files start a subprocess server at module-import time and mutate + decnet.web.auth.SECRET_KEY, which poisons other test suites and + inflates collection time by 20+ seconds. """ - if collection_path.name == "test_schemathesis.py": + if collection_path.name.startswith("test_schemathesis"): markexpr = config.getoption("markexpr", default="") if "fuzz" not in markexpr: return True @@ -28,7 +29,6 @@ def pytest_ignore_collect(collection_path, config): os.environ["DECNET_JWT_SECRET"] = "test-secret-key-at-least-32-chars-long!!" os.environ["DECNET_ADMIN_PASSWORD"] = "test-password-123" -from decnet.web.api import app from decnet.web.dependencies import repo from decnet.web.db.models import User from decnet.web.auth import get_password_hash @@ -115,6 +115,7 @@ async def setup_db(monkeypatch) -> AsyncGenerator[None, None]: @pytest.fixture async def client() -> AsyncGenerator[httpx.AsyncClient, None]: + from decnet.web.api import app # heavy — deferred so collection pays no import cost async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as ac: yield ac diff --git a/tests/profiler/behave_shell/test_pin_smoke.py b/tests/profiler/behave_shell/test_pin_smoke.py index 3de76955..fb4c74f3 100644 --- a/tests/profiler/behave_shell/test_pin_smoke.py +++ b/tests/profiler/behave_shell/test_pin_smoke.py @@ -8,7 +8,7 @@ from __future__ import annotations def test_envelope_imports_cleanly() -> None: - from decnet_behave_core.spec.envelope import Observation, Window + from behave_core.spec.envelope import Observation, Window # construction smoke — registry-agnostic envelope obs = Observation( primitive="motor.input_modality", @@ -22,14 +22,14 @@ def test_envelope_imports_cleanly() -> None: def test_registry_imports_and_is_non_empty() -> None: - from decnet_behave_shell.spec.primitives import PRIMITIVE_REGISTRY + from behave_shell.spec.primitives import PRIMITIVE_REGISTRY assert len(PRIMITIVE_REGISTRY) > 0 # spot-check a primitive every Tier-A engine emits assert "motor.input_modality" in PRIMITIVE_REGISTRY def test_event_adapter_topic_shape() -> None: - from decnet_behave_shell.spec.event_adapter import event_topic_for + from behave_shell.spec.event_adapter import event_topic_for assert ( event_topic_for("motor.input_modality") == "attacker.observation.motor.input_modality" @@ -40,8 +40,8 @@ def test_to_event_payload_excludes_envelope_meta_fields() -> None: """The adapter excludes id/ts/v from payload (they ride at the DECNET Event envelope level). The profiler worker re-merges them in per BEHAVE-INTEGRATION.md §339-366.""" - from decnet_behave_core.spec.envelope import Observation, Window - from decnet_behave_shell.spec.event_adapter import to_event_payload + from behave_core.spec.envelope import Observation, Window + from behave_shell.spec.event_adapter import to_event_payload obs = Observation( primitive="motor.input_modality", value="typed", diff --git a/tests/profiler/behave_shell/test_registry_coverage.py b/tests/profiler/behave_shell/test_registry_coverage.py index 280dc028..f3bf8d2d 100644 --- a/tests/profiler/behave_shell/test_registry_coverage.py +++ b/tests/profiler/behave_shell/test_registry_coverage.py @@ -1,7 +1,7 @@ """Step H.1: registry-coverage test. Static assertion that every Tier-A primitive in -``decnet_behave_shell.spec.primitives.PRIMITIVE_REGISTRY`` has a slot +``behave_shell.spec.primitives.PRIMITIVE_REGISTRY`` has a slot in the calibration-grid binding sets — either the per-shard hard gate (``PHASE_ABCDEFG_PRIMITIVES``) or one of the conditional sets (``PHASE_D_CONDITIONAL_PRIMITIVES`` / ``PHASE_F_CONDITIONAL_PRIMITIVES`` @@ -24,7 +24,7 @@ Tier exclusion (mirrors ``BEHAVE-EXTRACTOR.md:180-223``): """ from __future__ import annotations -from decnet_behave_shell.spec.primitives import PRIMITIVE_REGISTRY +from behave_shell.spec.primitives import PRIMITIVE_REGISTRY from tests.profiler.behave_shell.test_calibration_grid import ( PHASE_ABCDEFG_PRIMITIVES,