Table of Contents
- Module Reference — Web
- decnet/web/api.py
- decnet/web/auth.py
- decnet/web/dependencies.py
- decnet/web/ingester.py
- decnet/web/db/factory.py
- decnet/web/db/models.py
- decnet/web/db/repository.py
- decnet/web/db/sqlmodel_repo.py
- decnet/web/db/sqlite/database.py
- decnet/web/db/sqlite/repository.py
- decnet/web/db/mysql/database.py
- decnet/web/db/mysql/repository.py
- decnet/web/router/__init__.py
- decnet/web/router/auth/api_login.py
- decnet/web/router/auth/api_change_pass.py
- decnet/web/router/logs/api_get_logs.py
- decnet/web/router/logs/api_get_histogram.py
- decnet/web/router/bounty/api_get_bounties.py
- decnet/web/router/stats/api_get_stats.py
- decnet/web/router/fleet/api_get_deckies.py
- decnet/web/router/fleet/api_mutate_decky.py
- decnet/web/router/fleet/api_mutate_interval.py
- decnet/web/router/fleet/api_deploy_deckies.py
- decnet/web/router/stream/api_stream_events.py
- decnet/web/router/attackers/*
- decnet/web/router/config/*
- decnet/web/router/health/api_get_health.py
- decnet/web/router/artifacts/api_get_artifact.py
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— theFastAPIinstance. Docs are hidden unlessDECNET_DEVELOPER=true.decnet/web/api.py::lifespan— async context manager that (1) warns on lowulimit -n, (2) retries DB init up to 5 times, (3) sets up OTEL tracing, (4) starts background tasks unlessDECNET_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 forRequestValidationError: 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-instantiationValidationErrorso bad DB rows never surface as 500s.decnet/web/api.py::PyinstrumentMiddleware— mounted whenDECNET_PROFILE_REQUESTS=true; writes per-request HTML flamegraphs toDECNET_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 forDECNET_JWT_SECRET.decnet/web/auth.py::ALGORITHM—"HS256".decnet/web/auth.py::ACCESS_TOKEN_EXPIRE_MINUTES—1440.decnet/web/auth.py::verify_password— sync bcrypt compare (72-byte truncation).decnet/web/auth.py::get_password_hash— bcrypt hash withrounds=12.decnet/web/auth.py::averify_password/ahash_password—asyncio.to_threadwrappers.decnet/web/auth.py::create_access_token— sign a dict withexp(default 15 minutes if noexpires_delta) andiatclaims.
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-wideBaseRepositorysingleton (constructs it on first call viaget_repository()).decnet/web/dependencies.py::repo— module-level singleton convenience handle.decnet/web/dependencies.py::oauth2_scheme—OAuth2PasswordBearer(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 enforcesmust_change_password.decnet/web/dependencies.py::get_current_user_unchecked— same decode but skipsmust_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_passwordcheck to avoid a double DB hit.decnet/web/dependencies.py::require_admin—require_role("admin").decnet/web/dependencies.py::require_viewer—require_role("viewer", "admin").decnet/web/dependencies.py::require_stream_viewer— SSE variant ofrequire_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 whenDECNET_BATCH_SIZErows are buffered orDECNET_BATCH_MAX_WAIT_MSelapses, extracts OTEL parent context withextract_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 onservice/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— instantiateSQLiteRepositoryorMySQLRepository, wrap withdecnet.telemetry.wrap_repository(no-op when tracing is off), return aBaseRepository.
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_TEXT—Text().with_variant(MEDIUMTEXT(), "mysql").decnet/web/db/models.py::NullableDatetime/NullableString— normalise the strings"null","undefined",""toNone.decnet/web/db/models.py::User—uuid,username,password_hash,role,must_change_password.decnet/web/db/models.py::Log—timestamp,decky,service,event_type,attacker_ip,raw_line,fields(JSON text),msg,trace_id,span_id.decnet/web/db/models.py::Bounty—decky,service,attacker_ip,bounty_type,payload(JSON text).decnet/web/db/models.py::State— key/value store.valueis_BIG_TEXTbecause 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 toattackers.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::MutateIntervalRequest—r"^[1-9]\d*[mdMyY]$"— minutes/days/Months/years.decnet/web/db/models.py::DeployIniRequest— wrapsIniContentfromdecnet.models.decnet/web/db/models.py::CreateUserRequest/UpdateUserRoleRequest/ResetUserPasswordRequest— user-management DTOs (password length 8–72, 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—/healthenvelope.
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 legacyattackers, create all tables, seed the admin user.decnet/web/db/sqlmodel_repo.py::SQLModelRepository.reinitialize— re-create schema without dropping. Used by theDELETE /config/reinitflow.decnet/web/db/sqlmodel_repo.py::SQLModelRepository._ensure_admin_user— seed admin from env; self-heal password drift whenmust_change_passwordis still true.decnet/web/db/sqlmodel_repo.py::SQLModelRepository._apply_filters— sharedwherebuilder:start_time/end_time,decky:xxx/service:xxx/event:xxx/attacker:xxxshortcuts, arbitrarykey:valuethat translates toJSON_EXTRACT(fields, '$.key') = :val, plus a%search%ORacrossraw_line/decky/service/attacker_ip.decnet/web/db/sqlmodel_repo.py::SQLModelRepository._json_field_equals— SQL builder for JSON-field equality; overridden per dialect (SQLitejson_extract, MySQLJSON_UNQUOTE(JSON_EXTRACT(...))).decnet/web/db/sqlmodel_repo.py::SQLModelRepository.add_log/add_logs— single and bulk insert;add_logsis 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_iddrives the SSE stream.decnet/web/db/sqlmodel_repo.py::SQLModelRepository.get_stats_summary— counts (logs, unique IPs) joined with the in-memoryload_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 handsclose()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_engine—create_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— inheritsSQLModelRepository.decnet/web/db/sqlite/repository.py::SQLiteRepository.__init__— build async engine and session factory.decnet/web/db/sqlite/repository.py::SQLiteRepository._migrate_attackers_table— viaPRAGMA table_info(attackers); drops the pre-UUID schema.decnet/web/db/sqlite/repository.py::SQLiteRepository._json_field_equals—json_extract(fields, '$.key') = :val.decnet/web/db/sqlite/repository.py::SQLiteRepository.get_log_histogram—datetime((strftime('%s', timestamp) / N) * N, 'unixepoch')for per-interval_minutesbucketing.
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— composemysql+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_engine—create_async_enginewithpool_size,max_overflow, hourlypool_recycle(idle-reaper sidestep), andpool_pre_ping=trueby 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 theGET_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— usesinformation_schema.COLUMNSto drop pre-UUIDattackers.decnet/web/db/mysql/repository.py::MySQLRepository._migrate_column_types—ALTER TABLE … MODIFY COLUMN …to widenTEXT → MEDIUMTEXTon columns that pre-date_BIG_TEXT(attackers.commands/fingerprints/services/deckies,state.value).decnet/web/db/mysql/repository.py::MySQLRepository._json_field_equals—JSON_UNQUOTE(JSON_EXTRACT(fields, '$.key')) = :val.decnet/web/db/mysql/repository.py::MySQLRepository.get_log_histogram—FROM_UNIXTIME((UNIX_TIMESTAMP(timestamp) DIV N) * N); normalises the returneddatetimeto 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 combinedAPIRouter.
decnet/web/router/auth/api_login.py
decnet/web/router/auth/api_login.py::login—POST /auth/login. Verifies the bcrypt hash viaaverify_password, issues a 24-hour JWT, returnsToken.
decnet/web/router/auth/api_change_pass.py
decnet/web/router/auth/api_change_pass.py::change_password—POST /auth/change-password. Usesget_current_user_uncheckedso a forced-change user can still call it; verifies the old password, hashes and stores the new one, clearsmust_change_password, invalidates the auth cache.
decnet/web/router/logs/api_get_logs.py
decnet/web/router/logs/api_get_logs.py—GET /logs(require_viewer). Paginated log reads with the shared_apply_filterssearch syntax.
decnet/web/router/logs/api_get_histogram.py
decnet/web/router/logs/api_get_histogram.py—GET /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_bounties—GET /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_stats—GET /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_deckies—GET /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_decky—POST /deckies/{decky_name}/mutate(require_admin). Triggersdecnet.mutator.mutate_deckyimmediately.
decnet/web/router/fleet/api_mutate_interval.py
decnet/web/router/fleet/api_mutate_interval.py::api_update_mutate_interval—PUT /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_deckies—POST /deckies/deploy(require_admin). Acceptsini_content(validated byIniContent), delegates todecnet.engine.deploy.
decnet/web/router/stream/api_stream_events.py
decnet/web/router/stream/api_stream_events.py::stream_events—GET /stream(require_stream_viewer). Server-Sent-Events loop that pollsget_logs_after_idand flushes new rows asdata:frames. HonoursLast-Event-IDand?token=.
decnet/web/router/attackers/*
decnet/web/router/attackers/api_get_attackers.py::get_attackers—GET /attackers(require_viewer). Filtering by service / search, sort byrecent/active/traversals. Cached default via_get_total_attackers_cached.decnet/web/router/attackers/api_get_attacker_detail.py::get_attacker_detail—GET /attackers/{uuid}(require_viewer). Joins the behavior row.decnet/web/router/attackers/api_get_attacker_commands.py::get_attacker_commands—GET /attackers/{uuid}/commands(require_viewer). Paginated per-service slicing over the JSONcommandscolumn.decnet/web/router/attackers/api_get_attacker_artifacts.py::get_attacker_artifacts—GET /attackers/{uuid}/artifacts(require_viewer).file_capturedlog rows, newest first.
decnet/web/router/config/*
decnet/web/router/config/api_get_config.py::api_get_config—GET /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_limit—PUT /config/deployment-limit(require_admin).decnet/web/router/config/api_update_config.py::api_update_global_mutation_interval—PUT /config/global-mutation-interval(require_admin).decnet/web/router/config/api_manage_users.py::api_create_user—POST /config/users(require_admin).decnet/web/router/config/api_manage_users.py::api_delete_user—DELETE /config/users/{user_uuid}(require_admin). Invalidates auth cache.decnet/web/router/config/api_manage_users.py::api_update_user_role—PUT /config/users/{user_uuid}/role(require_admin).decnet/web/router/config/api_manage_users.py::api_reset_user_password—PUT /config/users/{user_uuid}/reset-password(require_admin). Setsmust_change_password=True.decnet/web/router/config/api_reinit.py::api_reinit—DELETE /config/reinit(require_admin). Callspurge_logs_and_bountiesthenreinitialize.
decnet/web/router/health/api_get_health.py
decnet/web/router/health/api_get_health.py::get_health—GET /health(require_viewer). ReturnsHealthResponse(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_artifact—GET /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.
DECNET
User docs
- Quick-Start
- Installation
- Requirements-and-Python-Versions
- CLI-Reference
- INI-Config-Format
- Custom-Services
- Services-Catalog
- Service-Personas
- Archetypes
- Distro-Profiles
- OS-Fingerprint-Spoofing
- Networking-MACVLAN-IPVLAN
- Deployment-Modes
- SWARM-Mode
- MazeNET
- Remote-Updates
- Environment-Variables
- Teardown-and-State
- Database-Drivers
- Systemd-Setup
- Logging-and-Syslog
- Service-Bus
- Web-Dashboard
- REST-API-Reference
- Mutation-and-Randomization
- Troubleshooting
Developer docs
DECNET — honeypot deception-network framework. Pre-1.0, active development — use with caution. See Sponsors to support the project. Contact: samuel@securejump.cl