2
Module Reference Web
anti edited this page 2026-04-18 06:20:17 -04:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Module Reference — Web

Every Python module under decnet/web/. See REST API Reference for the user-facing endpoint catalogue and Database Drivers for SQLite vs MySQL details.

Citation format: decnet/web/<path>::<symbol>.


decnet/web/api.py

FastAPI application factory. Wires the CORS middleware, the Pyinstrument profiling middleware (opt-in), the tiered validation-error exception handlers, and the lifespan that initialises the repository and optionally spawns the in-process ingestion / collector / profiler / sniffer background tasks. Defaults to running those workers as standalone daemons; the DECNET_EMBED_* flags are the escape hatch.

  • decnet/web/api.py::app — the FastAPI instance. Docs are hidden unless DECNET_DEVELOPER=true.
  • decnet/web/api.py::lifespan — async context manager that (1) warns on low ulimit -n, (2) retries DB init up to 5 times, (3) sets up OTEL tracing, (4) starts background tasks unless DECNET_CONTRACT_TEST=true, (5) cancels them on shutdown.
  • decnet/web/api.py::get_background_tasks — expose the four task handles to the health endpoint.
  • decnet/web/api.py::validation_exception_handler — tiered mapping for RequestValidationError: 400 for structural schema violations, 409 for semantic INI content violations (empty / syntax / missing sections), 422 otherwise.
  • decnet/web/api.py::pydantic_validation_exception_handler — catches manual model-instantiation ValidationError so bad DB rows never surface as 500s.
  • decnet/web/api.py::PyinstrumentMiddleware — mounted when DECNET_PROFILE_REQUESTS=true; writes per-request HTML flamegraphs to DECNET_PROFILE_DIR.
  • decnet/web/api.py::ingestion_task / collector_task / attacker_task / sniffer_task — module-level task handles used by the health endpoint.

decnet/web/auth.py

JWT + bcrypt primitives. HS256, 1440-minute (24 h) default expiry, bcrypt password length capped at 72 bytes to match the bcrypt spec, cost=12 work factor, and asyncio.to_thread wrappers so the ~250 ms bcrypt calls never block the event loop.

  • decnet/web/auth.py::SECRET_KEY — alias for DECNET_JWT_SECRET.
  • decnet/web/auth.py::ALGORITHM"HS256".
  • decnet/web/auth.py::ACCESS_TOKEN_EXPIRE_MINUTES1440.
  • decnet/web/auth.py::verify_password — sync bcrypt compare (72-byte truncation).
  • decnet/web/auth.py::get_password_hash — bcrypt hash with rounds=12.
  • decnet/web/auth.py::averify_password / ahash_passwordasyncio.to_thread wrappers.
  • decnet/web/auth.py::create_access_token — sign a dict with exp (default 15 minutes if no expires_delta) and iat claims.
def create_access_token(data, expires_delta=None):
    payload = data.copy()
    expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
    payload.update({"exp": expire, "iat": datetime.now(timezone.utc)})
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

decnet/web/dependencies.py

FastAPI dependency injection for the repository, auth decoding, and RBAC. Holds two in-memory caches (user-by-uuid with 10 s TTL, user-by-username with 5 s TTL) which collapse the "SELECT users WHERE uuid=?" that used to run once per authed request. Cache misses on username are intentionally not cached so that a just-created user can log in immediately.

  • decnet/web/dependencies.py::get_repo — returns the process-wide BaseRepository singleton (constructs it on first call via get_repository()).
  • decnet/web/dependencies.py::repo — module-level singleton convenience handle.
  • decnet/web/dependencies.py::oauth2_schemeOAuth2PasswordBearer(tokenUrl="/api/v1/auth/login").
  • decnet/web/dependencies.py::invalidate_user_cache — drop a single uuid (or everything); callers: password change, role change, user create/delete.
  • decnet/web/dependencies.py::get_user_by_username_cached — cached read for the login hot path.
  • decnet/web/dependencies.py::get_stream_user — SSE-only dependency that accepts Bearer header or ?token= query param (EventSource can't set headers).
  • decnet/web/dependencies.py::get_current_user — standard auth dependency that enforces must_change_password.
  • decnet/web/dependencies.py::get_current_user_unchecked — same decode but skips must_change_password; used by the change-password endpoint itself.
  • decnet/web/dependencies.py::require_role / require_stream_role — factories that return a dependency enforcing role membership and returning the full user dict. Inlines decode + lookup + must_change_password check to avoid a double DB hit.
  • decnet/web/dependencies.py::require_adminrequire_role("admin").
  • decnet/web/dependencies.py::require_viewerrequire_role("viewer", "admin").
  • decnet/web/dependencies.py::require_stream_viewer — SSE variant of require_viewer.
  • decnet/web/dependencies.py::_decode_token / _get_user_cached / _reset_user_cache — internal helpers (decode Bearer JWT, lookup with lock + TTL, test-only reset).

decnet/web/ingester.py

Background worker that tails <DECNET_INGEST_LOG_FILE>.json line-by-line, batches parsed records, and inserts them into the repository via repo.add_logs. After each batch, it calls _extract_bounty on every record to harvest credentials, HTTP user-agents, VNC client versions, SSH client banners, JA3/JA4/JA4L TLS fingerprints, TLS certificate details, and prober JARM / HASSH / TCP-FP hashes into the bounty table. File position is persisted via repo.set_state("ingest_worker_position", {...}) so restarts resume exactly where they stopped.

  • decnet/web/ingester.py::log_ingestion_worker — main loop. Detects truncation (current size < saved position → reset to 0), flushes when DECNET_BATCH_SIZE rows are buffered or DECNET_BATCH_MAX_WAIT_MS elapses, extracts OTEL parent context with extract_context(_log_data) so the ingester span chains off the collector's, and embeds the ingester span's trace/span IDs into the row.
  • decnet/web/ingester.py::_flush_batch — commit a batch and advance the saved position. Bails out early on cancellation so session teardown can't stall the worker; un-flushed lines stay uncommitted and are re-read on next start.
  • decnet/web/ingester.py::_extract_bounty — 11 bounty extractors keyed on service / event_type / field presence (credentials, http_useragent, vnc_client_version, ja3, ja4l, tls_resumption, tls_certificate, jarm, hassh_server, tcpfp).
  • decnet/web/ingester.py::_INGEST_STATE_KEY"ingest_worker_position".

decnet/web/db/factory.py

Selects the repository implementation based on DECNET_DB_TYPE and wraps it in the optional tracing proxy.

  • decnet/web/db/factory.py::get_repository — instantiate SQLiteRepository or MySQLRepository, wrap with decnet.telemetry.wrap_repository (no-op when tracing is off), return a BaseRepository.

decnet/web/db/models.py

SQLModel ORM tables and Pydantic request/response models. Long-text columns use a _BIG_TEXT variant that maps to MEDIUMTEXT on MySQL (16 MiB) and plain TEXT on SQLite.

  • decnet/web/db/models.py::_BIG_TEXTText().with_variant(MEDIUMTEXT(), "mysql").
  • decnet/web/db/models.py::NullableDatetime / NullableString — normalise the strings "null", "undefined", "" to None.
  • decnet/web/db/models.py::Useruuid, username, password_hash, role, must_change_password.
  • decnet/web/db/models.py::Logtimestamp, decky, service, event_type, attacker_ip, raw_line, fields (JSON text), msg, trace_id, span_id.
  • decnet/web/db/models.py::Bountydecky, service, attacker_ip, bounty_type, payload (JSON text).
  • decnet/web/db/models.py::State — key/value store. value is _BIG_TEXT because DecnetConfig blobs can be large.
  • decnet/web/db/models.py::Attacker — aggregated attacker profile row. All JSON blobs (services, deckies, fingerprints, commands) are _BIG_TEXT.
  • decnet/web/db/models.py::AttackerBehavior — separate table (FK to attackers.uuid) holding timing + behavioral profile: os_guess, hop_distance, tcp_fingerprint, retransmit_count, behavior_class, beacon_interval_s, beacon_jitter_pct, tool_guesses, timing_stats, phase_sequence.
  • decnet/web/db/models.py::Token / LoginRequest / ChangePasswordRequest — auth DTOs.
  • decnet/web/db/models.py::LogsResponse / BountyResponse / AttackersResponse / StatsResponse — paginated list envelopes.
  • decnet/web/db/models.py::MutateIntervalRequestr"^[1-9]\d*[mdMyY]$" — minutes/days/Months/years.
  • decnet/web/db/models.py::DeployIniRequest — wraps IniContent from decnet.models.
  • decnet/web/db/models.py::CreateUserRequest / UpdateUserRoleRequest / ResetUserPasswordRequest — user-management DTOs (password length 872, role literal).
  • decnet/web/db/models.py::DeploymentLimitRequest / GlobalMutationIntervalRequest — global config DTOs.
  • decnet/web/db/models.py::UserResponse / ConfigResponse / AdminConfigResponse — response DTOs.
  • decnet/web/db/models.py::ComponentHealth / HealthResponse/health envelope.

decnet/web/db/repository.py

Abstract base class. Every method is @abstractmethod so any repo must explicitly implement the whole contract; add_logs provides a default per-row fallback.

  • decnet/web/db/repository.py::BaseRepository — contract for storage: initialize, add_log / add_logs, get_logs / get_total_logs / get_max_log_id / get_logs_after_id, get_stats_summary, get_deckies, user CRUD (get_user_by_username / get_user_by_uuid / create_user / update_user_password / list_users / delete_user / update_user_role), purge_logs_and_bounties, bounty CRUD + get_all_bounties_by_ip / get_bounties_for_ips, get_state / set_state, attacker upsert + queries (upsert_attacker, upsert_attacker_behavior, get_attacker_behavior, get_behaviors_for_ips, get_attacker_by_uuid, get_attackers, get_total_attackers, get_attacker_commands, get_attacker_artifacts).

decnet/web/db/sqlmodel_repo.py

Concrete portable SQLModel/SQLAlchemy-async implementation. Subclasses only override __init__, _migrate_attackers_table, _json_field_equals, and get_log_histogram. Everything else (filters, bounty dedup, attacker upsert, JSON blob deserialisation) is shared.

  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository — the portable base.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.initialize — migrate legacy attackers, create all tables, seed the admin user.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.reinitialize — re-create schema without dropping. Used by the DELETE /config/reinit flow.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository._ensure_admin_user — seed admin from env; self-heal password drift when must_change_password is still true.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository._apply_filters — shared where builder: start_time / end_time, decky:xxx / service:xxx / event:xxx / attacker:xxx shortcuts, arbitrary key:value that translates to JSON_EXTRACT(fields, '$.key') = :val, plus a %search% OR across raw_line / decky / service / attacker_ip.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository._json_field_equals — SQL builder for JSON-field equality; overridden per dialect (SQLite json_extract, MySQL JSON_UNQUOTE(JSON_EXTRACT(...))).
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.add_log / add_logs — single and bulk insert; add_logs is one session, one commit.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.get_logs / get_logs_after_id / get_total_logs / get_max_log_id — paginated read paths; get_logs_after_id drives the SSE stream.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.get_stats_summary — counts (logs, unique IPs) joined with the in-memory load_state() deckies.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.get_deckies — returns the deckies from the state file (not the DB — the deploy state is the source of truth).
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.add_bounty — deduplicates on (bounty_type, attacker_ip, payload) before insert.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.get_all_bounties_by_ip / get_bounties_for_ips — grouped reads used by the profiler.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.upsert_attacker / upsert_attacker_behavior / get_attacker_behavior / get_behaviors_for_ips / get_attacker_by_uuid / get_attackers / get_total_attackers / get_attacker_commands / get_attacker_artifacts — attacker-table operations.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository.purge_logs_and_bounties — wipes logs, bounty, attacker_behavior, attackers; returns row counts.
  • decnet/web/db/sqlmodel_repo.py::SQLModelRepository._deserialize_attacker / _deserialize_behavior — decode JSON-text columns into Python dicts/lists.
  • decnet/web/db/sqlmodel_repo.py::_safe_session — cancellation-safe session context manager. Success path inline-closes so the caller observes cleanup; exception path hands close() off to a fresh task so a client disconnect can't orphan a MySQL connection.
  • decnet/web/db/sqlmodel_repo.py::_detach_close — fire-and-forget session close on a brand-new task; invalidates the connection if close fails.

decnet/web/db/sqlite/database.py

Async + sync engine factories for SQLite (aiosqlite). On every connection, sets PRAGMA journal_mode=WAL, PRAGMA synchronous=NORMAL, and PRAGMA busy_timeout=30000.

  • decnet/web/db/sqlite/database.py::get_async_enginecreate_async_engine("sqlite+aiosqlite:///<path>") with pool knobs from env + the pragma listener.
  • decnet/web/db/sqlite/database.py::get_sync_engine — sync twin used for DDL.
  • decnet/web/db/sqlite/database.py::init_db — synchronously create all tables. Forces WAL first.
  • decnet/web/db/sqlite/database.py::get_session — async-generator session dependency.

decnet/web/db/sqlite/repository.py

SQLite concrete subclass. Default DB path is <repo root>/decnet.db.

  • decnet/web/db/sqlite/repository.py::SQLiteRepository — inherits SQLModelRepository.
  • decnet/web/db/sqlite/repository.py::SQLiteRepository.__init__ — build async engine and session factory.
  • decnet/web/db/sqlite/repository.py::SQLiteRepository._migrate_attackers_table — via PRAGMA table_info(attackers); drops the pre-UUID schema.
  • decnet/web/db/sqlite/repository.py::SQLiteRepository._json_field_equalsjson_extract(fields, '$.key') = :val.
  • decnet/web/db/sqlite/repository.py::SQLiteRepository.get_log_histogramdatetime((strftime('%s', timestamp) / N) * N, 'unixepoch') for per-interval_minutes bucketing.

decnet/web/db/mysql/database.py

MySQL async engine factory (driver: asyncmy). Precedence: explicit url argument > DECNET_DB_URL > component vars. Password is URL-encoded; empty passwords are allowed only under pytest.

  • decnet/web/db/mysql/database.py::build_mysql_url — compose mysql+asyncmy://user:pass@host:port/db.
  • decnet/web/db/mysql/database.py::resolve_url — pick URL source per the above precedence.
  • decnet/web/db/mysql/database.py::get_async_enginecreate_async_engine with pool_size, max_overflow, hourly pool_recycle (idle-reaper sidestep), and pool_pre_ping=true by default.
  • decnet/web/db/mysql/database.py::DEFAULT_POOL_SIZE / DEFAULT_MAX_OVERFLOW / DEFAULT_POOL_RECYCLE / DEFAULT_POOL_PRE_PING — env-driven defaults.

decnet/web/db/mysql/repository.py

MySQL concrete subclass.

  • decnet/web/db/mysql/repository.py::MySQLRepository.__init__ — build async engine and session factory from a resolved URL.
  • decnet/web/db/mysql/repository.py::MySQLRepository.initialize — acquires the GET_LOCK('decnet_schema_init', 30) advisory lock so concurrent uvicorn workers don't race on DDL, then runs _migrate_attackers_table, _migrate_column_types, SQLModel.metadata.create_all, _ensure_admin_user, releases lock.
  • decnet/web/db/mysql/repository.py::MySQLRepository._migrate_attackers_table — uses information_schema.COLUMNS to drop pre-UUID attackers.
  • decnet/web/db/mysql/repository.py::MySQLRepository._migrate_column_typesALTER TABLE … MODIFY COLUMN … to widen TEXT → MEDIUMTEXT on columns that pre-date _BIG_TEXT (attackers.commands / fingerprints / services / deckies, state.value).
  • decnet/web/db/mysql/repository.py::MySQLRepository._json_field_equalsJSON_UNQUOTE(JSON_EXTRACT(fields, '$.key')) = :val.
  • decnet/web/db/mysql/repository.py::MySQLRepository.get_log_histogramFROM_UNIXTIME((UNIX_TIMESTAMP(timestamp) DIV N) * N); normalises the returned datetime to an ISO string for parity with SQLite.

decnet/web/router/__init__.py

Aggregates every sub-router (auth, logs, bounty, stats, fleet, attackers, config, health, stream, artifacts) into a single api_router which the app mounts under /api/v1.

  • decnet/web/router/__init__.py::api_router — the combined APIRouter.

decnet/web/router/auth/api_login.py

  • decnet/web/router/auth/api_login.py::loginPOST /auth/login. Verifies the bcrypt hash via averify_password, issues a 24-hour JWT, returns Token.

decnet/web/router/auth/api_change_pass.py

  • decnet/web/router/auth/api_change_pass.py::change_passwordPOST /auth/change-password. Uses get_current_user_unchecked so a forced-change user can still call it; verifies the old password, hashes and stores the new one, clears must_change_password, invalidates the auth cache.

decnet/web/router/logs/api_get_logs.py

  • decnet/web/router/logs/api_get_logs.pyGET /logs (require_viewer). Paginated log reads with the shared _apply_filters search syntax.

decnet/web/router/logs/api_get_histogram.py

  • decnet/web/router/logs/api_get_histogram.pyGET /logs/histogram (require_viewer). Dialect-aware bucketed count series.

decnet/web/router/bounty/api_get_bounties.py

  • decnet/web/router/bounty/api_get_bounties.py::get_bountiesGET /bounty (require_viewer). Cached default (no filters) via _get_bounty_default_cached.

decnet/web/router/stats/api_get_stats.py

  • decnet/web/router/stats/api_get_stats.py::get_statsGET /stats (require_viewer). Cached via _get_stats_cached.

decnet/web/router/fleet/api_get_deckies.py

  • decnet/web/router/fleet/api_get_deckies.py::get_deckiesGET /deckies (require_viewer). Cached via _get_deckies_cached; reads the state file.

decnet/web/router/fleet/api_mutate_decky.py

  • decnet/web/router/fleet/api_mutate_decky.py::api_mutate_deckyPOST /deckies/{decky_name}/mutate (require_admin). Triggers decnet.mutator.mutate_decky immediately.

decnet/web/router/fleet/api_mutate_interval.py

  • decnet/web/router/fleet/api_mutate_interval.py::api_update_mutate_intervalPUT /deckies/{decky_name}/mutate-interval (require_admin). Expects <n>[mdMyY].

decnet/web/router/fleet/api_deploy_deckies.py

  • decnet/web/router/fleet/api_deploy_deckies.py::api_deploy_deckiesPOST /deckies/deploy (require_admin). Accepts ini_content (validated by IniContent), delegates to decnet.engine.deploy.

decnet/web/router/stream/api_stream_events.py

  • decnet/web/router/stream/api_stream_events.py::stream_eventsGET /stream (require_stream_viewer). Server-Sent-Events loop that polls get_logs_after_id and flushes new rows as data: frames. Honours Last-Event-ID and ?token=.

decnet/web/router/attackers/*

  • decnet/web/router/attackers/api_get_attackers.py::get_attackersGET /attackers (require_viewer). Filtering by service / search, sort by recent / active / traversals. Cached default via _get_total_attackers_cached.
  • decnet/web/router/attackers/api_get_attacker_detail.py::get_attacker_detailGET /attackers/{uuid} (require_viewer). Joins the behavior row.
  • decnet/web/router/attackers/api_get_attacker_commands.py::get_attacker_commandsGET /attackers/{uuid}/commands (require_viewer). Paginated per-service slicing over the JSON commands column.
  • decnet/web/router/attackers/api_get_attacker_artifacts.py::get_attacker_artifactsGET /attackers/{uuid}/artifacts (require_viewer). file_captured log rows, newest first.

decnet/web/router/config/*

  • decnet/web/router/config/api_get_config.py::api_get_configGET /config (require_viewer). Returns the caller's role, the deployment limit, the global mutation interval, and — for admins — the full user list. Caches user list + state via _get_list_users_cached / _get_state_cached.
  • decnet/web/router/config/api_update_config.py::api_update_deployment_limitPUT /config/deployment-limit (require_admin).
  • decnet/web/router/config/api_update_config.py::api_update_global_mutation_intervalPUT /config/global-mutation-interval (require_admin).
  • decnet/web/router/config/api_manage_users.py::api_create_userPOST /config/users (require_admin).
  • decnet/web/router/config/api_manage_users.py::api_delete_userDELETE /config/users/{user_uuid} (require_admin). Invalidates auth cache.
  • decnet/web/router/config/api_manage_users.py::api_update_user_rolePUT /config/users/{user_uuid}/role (require_admin).
  • decnet/web/router/config/api_manage_users.py::api_reset_user_passwordPUT /config/users/{user_uuid}/reset-password (require_admin). Sets must_change_password=True.
  • decnet/web/router/config/api_reinit.py::api_reinitDELETE /config/reinit (require_admin). Calls purge_logs_and_bounties then reinitialize.

decnet/web/router/health/api_get_health.py

  • decnet/web/router/health/api_get_health.py::get_healthGET /health (require_viewer). Returns HealthResponse (healthy|degraded|unhealthy) aggregated from: database roundtrip (cached via _check_database_cached), each background task's .done() status.

decnet/web/router/artifacts/api_get_artifact.py

  • decnet/web/router/artifacts/api_get_artifact.py::get_artifactGET /artifacts/{decky}/{stored_as} (require_admin). Streams back a previously-captured attacker file drop; resolves the path inside the decky's artifact directory and refuses traversal attempts.