[pull] master from kevoreilly:master - #508
Merged
Merged
Conversation
…w tenancy imports
…d, hunt() dropped, guac 3b + central coexist)
…wer_scope to coverage-gate markers
An adversarial security review of the multitenancy feature found 10 verified cross-tenant leaks, all concentrated in the mongo-side scoping (the core predicate and the SQL/per-task read gates were already fail-closed). Fixes, each written test-first; 162 MT tests pass in the real CAPE env, ruff clean. Root causes: - #1 modules/reporting/mongodb.py: stamp_tenant_info fails CLOSED to private on an unresolved task (was public); run() resolves tenant context by the LOCAL task id, not the distributed-rewritten main_task_id, which misses on a worker and stamped every distributed analysis world-visible. - #2 lib/cuckoo/common/tenancy.py + web/dashboard/views.py: drop the mode-not-locked guard from viewer_scope_match, viewer_scope_es_filter and entitled_scopes so scoping is mode-independent, mirroring can_read and the SQL list filter; shared mode (the default) no longer degrades to see-all. - #6 multitenancy_config validates and normalizes mode, failing closed to locked on an unknown/typo value. - #9 stats aggregates auto-scope via the fixed entitled_scopes (stale comment corrected in web/apiv2/views.py). Per-record backstops and the rest: - #3 web/analysis/views.py: capeyarazipall gates records by owning-task can_view_task BEFORE resolving paths, covering content-addressed storage/binaries samples the old analyses-path regex missed. - #4 web/compare/views.py: the mongo md5-pivot gets the per-record can_view_task backstop the ES branch already had. - #5 lib/cuckoo/core/data/tasking.py: config-dedup re-verifies the deduped task against the authoritative SQL row before surfacing its id, closing the cross-tenant task-id / config-exists oracle. - #7 web/guac/consumers.py: gate the WebSocket socket user unconditionally (viewer_for resolves anonymous to public-only) so a replayed guac_session token cannot tunnel into another tenant's live VM. - #8 utils/db_migration/mongo_backfill_tenant.py: orphaned docs backfill to private, not public. - #10 set_task_visibility retries the mongo sync and raises on persistent failure (only when mongo is the enabled report store) instead of swallowing it, so a stale public stamp after a private toggle is surfaced.
An adversarial re-verification of the first fix commit found two fixes that did not actually hold, plus a HIGH the review series had missed. This commit closes the ones with clear fixes; 164 MT tests pass, ruff clean. - #10 (was reopened): mongo_update_one is wrapped by graceful_auto_reconnect, which swallows AutoReconnect/ServerSelectionTimeoutError and RETURNS None with no re-raise on persistent mongo-down, so the earlier except-based raise never fired. Detect the swallowed failure by the None return instead; the apiv2 caller now returns 503 rather than a raw 500. The regression test models the real None-return path (the prior RuntimeError test never exercised the bug). - rtid wrong-object (new HIGH): _resolve_task_id and 14 hand-inlined apiv2 artifact endpoints ran _deny_if_hidden on the ORIGINAL task_id, then followed a user-influenced Recovery_<N> pointer to a DIFFERENT task and served its report/memory/pcap/dropped/config bytes with no re-gate. Re-run _deny_if_hidden on the resolved id at every pivot; add negative + positive regression tests. - hygiene: corrected stale "no-op in shared" comments on the perform_search scope gate in web_utils.py (the gate is mode-independent after the #2 fix). Tracked as follow-ups (see docs/superpowers/mt-upstream-adversarial-review): the distributed-worker stamp (#1) needs tenant propagation through utils/dist.py, plus the ES-store visibility-toggle sync and the shared-mode stats API shape.
Per the support-boundary decision — multitenancy is generalized for the modes we actually run (MongoDB report store + single-node / broker-central / worker / Guacamole), not for the rarely-used Elasticsearch or legacy utils/dist.py distributed paths. Unsupported modes fail closed (safe but limited) rather than leak, and the boundary is documented. - #1 distributed-worker stamp: fail CLOSED to private when a legacy-distributed worker processes a task (options.main_task_id set) — the worker-local task carries no submitter tenancy, so stamp it invisible rather than world-visible. Our central/broker path keys by job_id and never sets main_task_id, so it is unaffected. - direct VNC console (task_id=0, caller-chosen host:port, no tenant scoping): restrict to superusers, in addition to the existing config gate. - docs/MULTITENANCY-SUPPORT.md: the supported matrix (mongo + our modes), the fail-closed unsupported modes (ES, dist.py), the shared-mode stats API shape change, and the direct-VNC admin gate. ES report-store visibility sync and full utils/dist.py tenant propagation are documented as not-yet-supported (fail-closed), tracked for later. 164 MT tests pass; ruff clean.
…ct-VNC, hunt doc)
A final adversarial re-verification confirmed the round-2/3 fixes hold, but found
two gaps and a doc overstatement:
- rtid wrong-object (16th pivot): tasks_view has its OWN inline Recovery_<N>
re.match pivot (separate from the 15 check.get("rtid") sites fixed earlier); it
re-fetched the pivoted task and served its data/sample/mongo doc without a
re-gate. Re-run _deny_if_hidden on the resolved id + a regression test that
exercises the actual view.
- direct-VNC (was gated on only 1 of 8 endpoints): the 7 sibling task-less
operator endpoints (direct_vnc_vm + start/shutdown/route/snapshots-list/
snapshot-create/snapshot-delete) mint task_id=0 sessions that bypass the
per-task can_view_task tunnel gate and control arbitrary live VMs by name. Gate
ALL of them on viewer_for(request.user).is_local_admin (break-glass admin;
usable on MT-disabled/no-auth installs), and upgrade the host:port endpoint from
the raw is_superuser check to the same break-glass gate. Add a regression test.
- hunt(): its facet task_ids rely solely on the viewer_scope $match (a $facet
count can't be per-id SQL-backstopped) — documented as safe given the tenant
stamp is now written fail-closed on every path.
- docs/MULTITENANCY-SUPPORT.md: corrected the direct-VNC note (all endpoints, not
just host:port) and documented the hunt() facet scoping.
166 MT tests pass; ruff clean. Independently corroborated by a Codex security scan
(F-003) run separately.
…atomicity, stamp coverage) Addresses the non-blocking open items from the final adversarial re-verification: - rtid re-gate now runs BEFORE the TLP:RED check at all 14 pivot sites (_resolve_task_id + 13 inline apiv2 endpoints), so a hidden resolved task returns a generic 404 instead of a distinguishable "TLP of RED" message — closing a narrow existence oracle (no bytes were served either way). - set_task_visibility now rolls the SQL change back if the mongo stamp sync fails, so the two stores can never diverge (previously a mongo-down toggle left SQL private + mongo stale-public until retry). The regression test asserts the rollback. - extracted the run() distributed-vs-local stamp decision into _stamp_report_for_task and unit-tested both branches (distributed main_task_id -> fail-closed private; local -> stamp from the local task), closing the coverage gap the reviewer noted. - (the cosmetic hunt() comment was already corrected in the prior commit.) 168 MT tests pass; ruff clean.
The final re-verification (clean) flagged three non-leak accuracy nits introduced by the round-5 changes: - tasks_set_visibility now reports the change was ABORTED and rolled back (round-5 makes set_task_visibility roll SQL back on a mongo-down sync failure), not "updated in DB" — so an operator retries instead of assuming a public->private toggle applied. - direct_vnc_host_port comment: the gate is viewer_for().is_local_admin (break-glass admin, config-aware), not raw is_superuser. - set_task_visibility comment: retries live in graceful_auto_reconnect; on a persistent failure we roll back + raise (the old 3-attempt loop is gone). Comment/message-only; no logic change.
…g + sample oracle Gemini's review of the fork-internal PR flagged 7 valid items; all fixed. Security: - top_detections shared-cache: bypass the 10-minute cache when EITHER scope_match OR viewer is set (the ES branch scopes via viewer, not scope_match) — keying only on scope_match served/poisoned the global cache with tenant-scoped data. - sample_path_by_hash(task_id=...): gate the SPECIFIC task_id (it must itself be visible) before resolving its sample, instead of "any visible task for the sample" — the latter leaks whether another tenant's task_id analyzed a shared sample (existence oracle). Performance (N+1 -> one query each): - compare left()/hash() mongo md5-pivot, capeyarazipall record filter, and search() now batch the visibility check via a single list_tasks(task_ids=..., visible_to=...) instead of a view_task per record. Behaviour-equivalent (the existing visibility tests still pass; the capeyara regression test now mocks list_tasks). 168 MT tests pass; ruff clean.
…h guard, toggle UX Codex + Copilot reviews on the PR flagged 5 valid MT items; all fixed + tested. - (Codex P1) API PCAP submit: the tasks/create/file pcap branch called add_pcap() directly, bypassing the details tenancy dict, so it used defaults (user_id=0, tenant_id=None, visibility=private) — a tenant user's own PCAP task was invisible to them in locked mode. Pass the submitter's user/tenant/visibility. - (Codex + Copilot P2) OIDC login tenant guard checked the raw extra_data, but the openid_connect provider nests claims under extra["userinfo"], so the groups claim read as absent and reconcile_tenant() was skipped — SSO users logged in with no tenant/tenant-admin membership. Guard on _claims(extra). (+ regression test) - (Codex P2) tasks_status POST status=finish was gated only by read visibility, so a same-tenant read-only user could stop another user's running analysis. Require can_manage_task on the mutation. - (Copilot) reconcile_tenant could TypeError on non-string JSONField group entries and wrote UserProfile on every login. String-filter the groups + save only when the tenant/admin values actually change. (+ regression test) - (Copilot) report.html visibility dropdown never reverted the FIRST failed update (no initial data-prev; onchange fires after the value changed). Seed data-prev from the server-rendered visibility. 170 MT tests pass; ruff clean.
Copilot re-review of 6116094: - tasks_set_visibility now parses task_id to int once up front, so view_task() and set_task_visibility() get a consistent value and a non-numeric id fails as the same generic 404 instead of relying on implicit SQLAlchemy coercion. - set_task_visibility now FLUSHes the SQL change, syncs the mongo stamp, then commits on success / rolls back on failure — removing the transient window where SQL reads "private" while the mongo aggregate/search/stats surfaces still read "public" (and the commit-then-rollback churn when mongo is down).
Codex: in SSO/OIDC deployments settings.py drops SessionAuthentication from the DRF default chain, so the report-page visibility toggle (browser session + CSRF, no API token) was rejected — owners/admins couldn't change visibility from the UI. Opt tasks_set_visibility into @authentication_classes([SessionAuthentication, ApiKeyAuthentication]) (the per-view pattern settings.py documents) so both the in-browser control and scripted API-token clients authenticate.
…ce on failure Follow-up to the Copilot atomicity fix: session.rollback() discarded more than the visibility change under the test's transaction model (broke the mongo-down regression test). Instead update the mongo stamp first while the SQL change is still uncommitted, then commit; on a persistent mongo failure revert the in-memory visibility to the previous value and commit that, then raise. Result: no committed SQL-private / mongo-public window, and no session.rollback() discarding unrelated pending work.
…js, ES stats note) - submission index(): an invalid `visibility` now returns HTTP 400 (was 200) so the client-input failure is machine-detectable, per the submission_scope() contract. - report.html: escapejs the CSRF token interpolated into the JS string. - web_utils.top_detections: corrected the ES-branch comment — it applies the viewer's entitled UNION (not per-scope like the mongo branch), so ES-backed per-scope panels show the union; it never exposes another tenant's data (the viewer's own union) and ES + multitenancy is a documented not-yet-supported combination (mongo is the supported store).
…t fail-closed Codex re-review of 6bf4ac6: - web/guac/consumers.py: the socket-side replay re-check only ran for task-backed sessions (guac_task_id > 0). Direct-VNC sessions (task_id=0) fell through with no check, so a replayed break-glass-admin guac_session cookie from a tenant/anon socket could open the raw VM/host console on token possession. The direct branch now re-validates viewer_for(ws_user).is_local_admin before resolving VNC. - web/users/tenancy.py: viewer_for kept trusting prof.tenant_id/is_tenant_admin after a Tenant was marked inactive, so deactivated-tenant members kept tenant-scoped read/submit access until the next SSO login reconciled. viewer_for now drops the tenant when its Tenant row is inactive (fail closed), matching reconcile_tenant's active=True filter. (+ regression test)
…orators + drop dead config key Copilot re-review of f4db397: - web/analysis/views.py: require_task_manage / require_task_visibility forwarded a URL-supplied task id straight to db.view_task() without coercion. On the \w+ analysis routes (filereport, full_memory_dump_file/_strings) a non-numeric id (e.g. /full_memory/abc/) reached the postgres task DB as "abc" and raised a DataError -> an uncaught 500 that also leaked a task-vs-no-task signal, breaking the decorators' "hidden and missing are indistinguishable" contract. Added a shared _coerce_task_id() helper used by both decorators: a non-numeric id now fails closed with the same generic 403, before any DB hit. (+ regression test) - conf/default/cuckoo.conf.default: removed the dead `tenant_claim_source` key — it had zero code references; tenant membership derives from the OIDC groups claim (OIDC_CFG['groups_claim']). Leaving it in was misleading to operators. Not changed (verified against actual code): - reconcile_tenant O(n) tenant scan: the suggested idp_groups__contains pushdown is unsupported on the sqlite auth DB (supports_json_field_contains=False -> NotSupportedError); the Python-side filter is the correct portable approach. Documented the constraint inline. - submission/views.py resubmit int-coercion: the str(task_id)/find_sample lines flagged are pre-existing CAPE code and the route is \d+ (always numeric), so the new authz gate already behaves correctly; no MT-scoped change needed.
…tenant Copilot re-review of 93c69ed (web/users/tenancy.py:137): submission_scope could return visibility='tenant' with tenant_id=None — a tenant-less user in locked mode, or an anonymous submitter (WEB_AUTHENTICATION off + MT on), or an explicit visibility=tenant from a tenant-less user. can_read's _same_tenant requires a non-None job tenant, so such a job is readable only by its owner (via _is_owner) and, for an anonymous submitter whose viewer has user_id=None, by nobody but break-glass — a self-lockout / orphaned job. Fix at the source: submission_scope refuses an explicit 'tenant' from a tenant-less viewer (ValueError -> the caller returns 400) and downgrades a per-mode default that resolves to 'tenant' to PRIVATE (owner still reads via _is_owner; fail closed rather than world-exposing an anon job to PUBLIC under the most-restrictive locked mode). Preserves the (tenant_id, visibility) 2-tuple + ValueError-on-invalid contract, so no caller/test changes. (+ regression test)
…P1s) Codex re-review of 93c69ed flagged two P1s: minting a Guacamole session (guac/views.py) and streaming a file off the running guest (apiv2 tasks_file_stream) were gated on can_view_task. Those are task ACTIONS — live keyboard/mouse/framebuffer control and arbitrary live-VM file pull — not passive report viewing, so a read-only viewer of a public/tenant task could hijack another user's/tenant's running VM. They must follow the same owner/tenant-admin/break-glass boundary (can_manage_task) as other task mutations. Upgraded the ENTIRE live-VM path to the manage boundary (MT-off = is_local_admin, so the single-analyst console is unchanged): - web/guac/views.py index(): mint gated on can_manage_task. - web/guac/consumers.py: websocket re-check now can_manage_task (defense-in-depth). - web/submission/views.py: remote_session() gated on can_manage_task; status() still readable (can_view_task) but emits the guac session_data only to a manager. - web/apiv2/views.py tasks_file_stream(): now _deny_manage (was _deny_if_hidden). Tests: guac mint denies a read-only PUBLIC-task viewer; websocket-recheck gate now asserts can_manage_task; remote_session denies a read-only viewer.
…uracy (Copilot) Copilot re-review of 38ef08e (all low-severity, verified against the code): - web/submission/views.py: the submit form offered the 'tenant' visibility option based on mode == "shared" (hidden in shared), but submission_scope honors an explicit 'tenant' for any tenant MEMBER in both modes (and rejects it for a tenant-less user). Key the option on viewer_for(request.user).tenant_id instead, so the UI matches the API/predicate: tenant members see 'tenant', tenant-less users don't. (+ regression test) - docs/MULTITENANCY-SUPPORT.md: the Guacamole bullet still said the tunnel is gated by can_view_task; it's now can_manage_task (owner/tenant-admin/break-glass) after 30ce167e. Updated the doc to the stricter boundary. - web/apiv2/views.py: tasks_set_visibility docstring said "superuser"; the actual authz is can_toggle (break-glass = viewer.is_local_admin, a cuckoo.conf-gated superuser, not any superuser). Clarified the wording.
…_filename (Codex P2s) Codex re-review of 38ef08e: - web/compare/views.py: the md5-pivot result `_raw` (a PyMongo cursor) is iterated twice — first to collect info.ids for the batched visibility check, then to build `records`. A cursor is single-pass, so the first loop exhausted it and Mongo-backed compare always returned empty `records`. Materialize with list() (both the both() and single-compare paths). Introduced by the earlier N+1 batching change. (+ regression test: mongo_find mocked as a single-pass iterator.) - web/submission/views.py: the resubmit original_filename recovery ran for the by-HASH branch too, where the route task_id is NOT can_view_task-gated (only the task_id branch gates it). A by-hash resubmit carrying another tenant's task_id in the URL (/submission/resubmit/<task_id>/<hash>/) copied that hidden task's target basename into the new submission — a cross-tenant filename leak. Require can_view_task before trusting the route task_id; otherwise fall back to the hash.
…c (Copilot) Copilot re-review of c36a4b1 (lib/cuckoo/core/data/tasking.py): set_task_visibility syncs the mongo stamp FIRST, then commits SQL — which closes the mongo-fails window, but not the complementary one. If the final self.session.commit() fails AFTER a successful mongo sync (transient DB error), mongo already reflects the new (possibly more-permissive) visibility while SQL rolls back to the old value — a fail-open window where the aggregate/search/stats surfaces read more permissively than SQL authorizes. Wrap the SQL commit: on failure after a successful mongo sync, best-effort revert the mongo stamp to the previous value, roll back the poisoned session, and raise CuckooOperationalError (the caller retries / returns 503). Now BOTH stores fail closed regardless of which write fails. (+ regression test)
…he limit (Codex) Codex re-review of c36a4b1 (lib/cuckoo/common/web_utils.py:1595): the tags_tasks / options / user_tasks searches build the id set with db.list_tasks(..., limit= search_limit) WITHOUT visible_to=viewer, and the tenant scope is only applied later at the mongo layer. In a multi-tenant deployment other tenants' matching tasks fill search_limit before the scope is applied, so the (scoped) mongo query returns no results even when the viewer has older visible matches (fail-closed, but the viewer loses their own results). Thread visible_to=viewer into those SQL prequeries so the limit applies to the viewer's VISIBLE matches. visible_to=None is a no-op (multitenancy disabled / break-glass), preserving legacy behavior. (+ regression test)
…Codex P2s) Codex re-review of 6f9de56, both fallout from the earlier UI/API-alignment change: - web/submission/views.py: in locked mode default_visibility resolves to 'tenant', but a tenant-less user isn't offered 'tenant', so the template selected nothing and the browser fell back to the FIRST option (public) and submitted it EXPLICITLY — bypassing submission_scope's fail-closed private downgrade, so an unchanged form created a PUBLIC job. Compute the form default with the same tenantless downgrade (tenant -> private) so the preselected value is always offered and matches what submission_scope persists. (+ test) - web/templates/analysis/report.html + web/analysis/views.py + web/apiv2/views.py: the report visibility toggle offered 'tenant' for every toggleable task, incl. tenant_id=NULL (legacy/tenantless) tasks; selecting it makes the task readable by nobody but owner/break-glass (can_read's tenant branch needs a non-null tenant). Hide the 'tenant' option unless the task has a tenant (task_has_tenant context), AND reject a tenant transition for a tenantless task in tasks_set_visibility (400) as the authoritative guard. (+ test)
…ups (Codex P1 + Copilot) Codex P1 (lib/cuckoo/core/data/tasking.py:882): set_task_visibility synced the mongo stamp BEFORE the SQL commit for ALL directions. That closes the restrictive leak but OPENS the permissive one: a private->public toggle publishes mongo=public before SQL is durable, so a crash or a concurrent aggregate in the window sees mongo more permissive than SQL (cross-tenant leak), and a crash means it never reverts. Fix: order the two writes by the DIRECTION of the change so the invariant "mongo is never more permissive than SQL" always holds: - MORE permissive (rank public>tenant>private): commit SQL FIRST, then publish the mongo stamp; a mongo lag is fail-closed (mongo less permissive) and reconciles on reprocess, so it logs rather than rolling back a durable authorized change. - MORE restrictive / unchanged: mongo-first (existing behavior) — restrictive value reaches the mongo surfaces before SQL drops the restriction; revert+raise on mongo failure, and best-effort mongo revert if the SQL commit then fails. Updated the two existing mongo-fail/commit-fail tests to the restrictive direction (that's the mongo-first path now) + added two permissive-direction regression tests. Copilot (low-severity): - web/cuckoo/common/web_utils.py top_asn(): annotation/docstring said `-> dict` but it returns a list of rows or False; corrected to `list | bool` (+ documented scope_match). - web/apiv2/views.py: hoisted the mid-module `from rest_framework.response import Response` to the top DRF imports (the new MT helpers use it before that line).
…p parsing (Copilot) Copilot re-review of 35cccb1: - lib/cuckoo/core/data/tasking.py: on the RESTRICTIVE mongo-sync-fail path the setter reverted task.visibility then called self.session.commit(). On a shared/scoped session that could persist UNRELATED pending work. No SQL was committed on this path, so it needs neither a commit nor a rollback (rollback would DISCARD unrelated pending work): just revert the in-memory change and raise. (The SQL-commit-fail path still rolls back, because there a failed commit must be recovered.) - web/web/allauth_adapters.py: _g() did `{g for g in (vals or [])}`, so a JSONField idp_groups accidentally stored as a bare STRING ("acme-soc") iterated as characters and could char-match the wrong tenant. Normalize: a string is ONE group, a list/tuple/set is filtered to strings, any other type fails closed to empty. (+ test)
Codex re-review of 35cccb1 (web/apiv2/views.py:114): with [multitenancy] enabled=no (the default/legacy mode) viewer_for marks every principal is_local_admin, so can_toggle_task authorized ANY allowed API caller (or anonymous under DRF AllowAny) to PATCH a task's visibility. The value is ignored while MT is off, but it writes SQL and (mongo store) info.visibility; if MT is later enabled the backfill skips already-stamped docs, so these user-written values could hide/expose legacy analyses. Gate the write surfaces on multitenancy_config().enabled: - web/apiv2/views.py tasks_set_visibility: reject with 400 when MT is disabled (before any parse/lookup/write). (+ regression test) - web/analysis/views.py report(): can_toggle_visibility is now False when MT is disabled, so the report page doesn't render the (meaningless) toggle control.
Copilot re-review of 82ad865 (robustness against corrupt/partial docs): - utils/db_migration/mongo_backfill_tenant.py: backfill_doc int()-cast info.id unconditionally, so a doc with a missing/non-numeric id would raise and abort the whole one-shot backfill mid-run. Now wraps the coercion: a bad id fails CLOSED to private (orphan stamp) instead of crashing, without ever calling view_task with a value that would raise. (+ regression test) - web/analysis/views.py report(): the existent_tasks loop passed rid straight from a mongo/ES record to db.view_task(rid); a non-numeric rid would raise in SQLAlchemy parameter binding (-> 500). Coerce rid to int and skip on failure (also covers a missing/None id), then compare against the current task id.
…er_for fan-out) Fork-internal review #16 [LOW]: pending()'s per-task can_delete_task(request.user, task) rebuilt viewer_for(request.user) on every row; for a break-glass-off superuser viewer_for runs an un-memoized user.socialaccount_set.exists() -> one EXISTS query PER pending row (an O(N) fan-out on the queue page). Fix: new can_delete_job(viewer, task) web wrapper + import-optional shim (same fail-closed contract as can_delete_task); pending() resolves the viewer ONCE and reuses it for both the read scope (list_tasks visible_to) and the per-row deletability annotation. Test: viewer_for resolved exactly once for N tasks. Box-validated both import modes: 104 append + 223 importlib; ruff clean.
…x the cleaner failure paths Fork-internal review #16 (dist.py / freeze-gate cluster from the 63ac5bc review): - [HIGH] Revert the [taskdelete] freeze on tasks_delete_many. It keyed on request.user.is_authenticated, which is wrong in both shipped configs: token_auth=no -> AllowAny/AnonymousUser -> never fires (while tasks_delete blocks everyone), and token_auth=yes -> the dist/go-fetcher workers authenticate with a node Token (non-staff) -> 403 on EVERY cleanup sweep -> silent unbounded worker-disk fill. delete_many is the MACHINE cleanup path and carries no freeze; the legal-hold freeze stays on the HUMAN endpoint tasks_delete. - [MED] utils/dist.py _delete_many now RETURNS success/failure and never rolls back the shared session itself (a session-wide rollback in a multi-node sweep reverted the healthy nodes' progress). It also inspects the 200-body: delete_many answers HTTP 200 even on a per-id failure (error=true/partial_error; NOT for an idempotent 'not exists'), so that is surfaced instead of silently committing deleted=True. - [MED] cleaner(): group task objects per node and set deleted=True ONLY for a node whose delete succeeded (one bad node no longer wipes the others' flags). CleanerThread: re-queue a failed node's ids (they were consumed off the queue with .get() and this session has no writes to roll back, so they were being lost). - [MED] go-fetcher DeleteFromWorker now checks resp.StatusCode + surfaces a partial_error body (was: read nothing, log "success"). gofmt-clean. Tests: replaced the two freeze tests with test_tasks_delete_many_never_frozen_machine_cleanup_path (authed non-staff AND anon both proceed under [taskdelete]=disabled -- RED if the freeze is re-added). dist.py/ go-fetcher aren't in the pytest harness (dist.py instantiates Database() at import) -> validated via py_compile + gofmt. Box-validated both import modes: 104 append + 222 importlib; ruff clean.
…(2 HIGH availability bugs) Fork-internal review #16 (2 HIGH, both pre-existing, both in the path the R18 cleaner rewrite now sits on; the rewrite's resilience goal depends on #1): - [HIGH] _delete_many's requests.post had NO timeout=. A frozen/half-open worker (accepts the connection, never responds) blocks it forever -> `return False` never fires -> the re-queue/retry path never runs -> one wedged node stalls disk-reclamation for EVERY node (cron_cleaner hangs; the Retriever cleaner thread stalls before its next drain). Added a bounded timeout=(10, 120): a frozen node now raises requests.exceptions.Timeout -> caught -> return False -> the resilience path engages. - [HIGH] cron_cleaner created /tmp/dist_cleaner.pid and removed it with the whole body (DB query, the now-hangable _delete_many loop, commit) in between, OUTSIDE any try/finally. Any exception/kill left the pidfile on disk -> the presence check at the top (sys.exit "we running") permanently disabled the cleaner until a manual rm -> unbounded worker analyses/ leak. Wrapped the body in try/finally so the pidfile (and the DB session) is always released. - Minor (review): CleanerThread re-queues failed ids as int now (details holds numeric strings; the queue + t_is_none bookkeeping use int), so re-queued ids aren't silently skipped there. dist.py isn't in the pytest harness (instantiates Database() at import) -> validated via py_compile + ruff; MT regression suite still green. Left as-is per review (non-blocking): deleted_orphan_report treated as retryable (converges next sweep, no leak); the presence-only pidfile lock (PID-write + liveness = follow-up).
…death-safe Fork-internal review #16 — 2 residuals the R18/R19 resilience path newly exposed (not regressions): - [MED] The CleanerThread re-queue retried a failed (node_id, task_id) every ~20s forever. Worker task ids are node-local autoincrements and worker re-provisioning reuses them, so a stale re-queued delete could land on a FRESHLY-submitted task after a node resets (delete its row + analyses/ before the report is fetched). Bound it: a per-id attempt counter (self.cleaner_retries) drops the id with a log after CLEANER_MAX_RETRIES (=10, ~a few minutes); counters clear on a node's successful sweep. - [LOW] The R19 try/finally removes the pidfile on exceptions + normal exit, but a signal death (SIGTERM/SIGKILL/OOM, e.g. a `timeout` cron wrapper or systemd stop) skips it, leaving a stale /tmp/dist_cleaner.pid that disabled the cleaner forever. Replace the presence-only check with a PID lock: write the real PID, and on startup treat a pidfile whose PID is not alive (os.kill(pid, 0)) as stale -> remove + proceed. (PermissionError => alive-but-other-user.) dist.py isn't in the pytest harness (Database() at import) -> validated via py_compile + ruff (clean); MT regression suite green.
…ycled-PID/race class) Fork-internal review #16 [LOW]: the R20 PID-liveness check is identity-blind -- os.kill(pid, 0) proves only that SOME process owns the pid, so a recycled pid (esp. root-owned -> the PermissionError=alive arm) re- disables the cleaner; and the check->delete->create sequence is non-atomic (FileNotFoundError race). Rather than patch the heuristic again, replace it with an advisory flock (the critic's own suggestion): fcntl.flock(LOCK_EX | LOCK_NB) held on an open fd for the process lifetime. The kernel releases it AUTOMATICALLY when the process dies by ANY means (exit, exception, SIGTERM/SIGKILL, OOM), which eliminates the whole stale-pidfile / recycled-PID / check-then-create-race class at once -- the failure modes the presence-only then PID-liveness heuristics kept re-exposing. Open a+ (create, no truncate) so a losing racer can't clobber the holder's recorded pid; write our pid only after the lock is ours (informational -- flock, not the file content, is authoritative). finally releases the lock, closes the fd, and unlinks best-effort (a concurrent/absent unlink can't raise). dist.py isn't in the pytest harness (Database() at import) -> validated via py_compile + ruff (clean) + a real-flock smoke on the box (second acquirer blocked; acquirable after release); MT regression green.
…two-cleaner seam) + narrow catch Fork-internal review #16 on the flock lock: - [MEDIUM] The finally unlinked /tmp/dist_cleaner.pid. flock locks the INODE but exclusion is checked via the PATH: unlinking orphans the locked inode, so a starter that opened before the unlink holds a lock on a dead inode while a later start creates + locks a FRESH inode at the same path -> two cleaners. Fix: do NOT unlink -- a persistent lockfile is harmless (its pid content is informational; the flock is authoritative). The finally now only LOCK_UN + close. - [LOW] The acquire caught bare OSError and logged "already running", masking environmental failures (ENOLCK/EIO) as contention -> silent disable with a misleading log. Split: BlockingIOError -> already running (exit); other OSError -> log the real cause + skip this run (next cron retries), not disable. - [hardening] Open the lockfile with os.open(O_RDWR|O_CREAT|O_NOFOLLOW, 0o600) so a symlink planted at the /tmp path can't redirect the open where fs.protected_symlinks is off. dist.py isn't in the pytest harness (Database() at import) -> validated via py_compile + ruff (clean) + a real-flock smoke on the box: 2nd acquire -> BlockingIOError while held; lockfile persists after release (no unlink); re-acquires the same inode; O_NOFOLLOW refuses a symlink (ELOOP).
… forged-job_id cross-tenant report read) Fork-internal review #16 [HIGH]: in central_analysis_query the BRIDGE-REQUIRED (central+MT, the production config) own-not-yet-reconciled arm was {info.job_id: jid, info.tenant_id: None}. job_id there just repeats the outer {info.job_id: jid} predicate, so the arm reduced to `info.tenant_id IS NULL`; and jid is submitter- FORGEABLE (central_job_id_for_task reads it from the RDS `custom` the submitter set, e.g. custom= 'job_id=ui-<victim>'). So a caller could forge their own task's custom to the victim's job_id and read the victim's UNSTAMPED analysis doc (the insert->reconcile window, or a reconcile-skip stranded doc) cross-tenant. Fix: pin the arm to the AUTHORIZED task's info.id -> {info.job_id: jid, info.tenant_id: None, info.id: int(task_id)}. A forged jid resolves the victim's doc (info.id != our task_id) -> no match; the owner's own not-yet-reconciled doc (centralstore re-keys info.id to the central id == task_id) still resolves; keeping the unique jid also blocks a foreign non-bridged doc that merely collides on info.id. The non-bridge arm already pinned info.id; this removes the divergence. Root cause it survived: the forged-jobid + owner tests only exercised the non-bridge (else) branch, and a separate test PINNED the vulnerable jid-only shape for the bridge-required branch. Retargeted that test to the info.id-pinned shape (keeping its foreign-collision assertion) and added two tests that drive the bridge-required branch directly (central_bridge_required()=True): forged job_id blocked, owner resolves. Box-validated importlib: web/analysis/test_central_scope.py 18 passed; full web MT suite 251 passed; append 104. ruff clean.
…(not exit) on contention + docstring Fork-internal review #16 (the 18:41 flock-hardening review, deferred while F2 was fixed): - [MEDIUM] The lockfile lived in world-writable /tmp. In sticky /tmp with Ubuntu's default fs.protected_regular=2 the kernel refuses to open a not-owned regular file (and DAC refuses a foreign 0600 file), so a lockfile ever created there by another user -- e.g. a one-off `sudo utils/dist.py -ec` -> root:root 0600 -- wedged EVERY subsequent cape run (open -> skip-this-run) with no self-heal. Move the lock to the SERVICE-OWNED CUCKOO_ROOT/log dir (created + owned by the cape service, same dir as dist.log), which removes the world-writable/sticky trap. On the narrow residual (EACCES/EPERM) log the file's owner and the exact remediation (`remove <path> to recover`) instead of a bare errno. - [LOW] The BlockingIOError (contention) arm called sys.exit(). With -ec, __main__ continues into StatusThread/Retriever/app.run after cron_cleaner, so exiting there took down the whole dist server for a startup-contention window. Return instead -> the server starts and merely skips that one sweep. - [LOW] The docstring still described the removed presence-based PID protocol ("looking for a PID file", "Deletes the PID file"). Rewrote steps to the flock lifecycle + state the lockfile is PERSISTENT BY DESIGN and must not be rm'd (deleting it re-opens the fresh-inode two-cleaner seam). dist.py isn't in the pytest harness (Database() at import) -> validated via py_compile + ruff (clean) + a lock smoke on the box (service-owned-dir acquire at 0600; O_NOFOLLOW; EACCES owner-hint resolves to 'cape'); MT regression green.
…s absent On EACCES/EPERM from os.open of the cleaner lockfile, the recovery hint always told the operator to remove the lockfile. But when the errno comes from the log DIRECTORY (pre-created root-owned/unsearchable) with no lockfile yet, os.stat raises FileNotFoundError so the owner lookup fell to '?' and the hint advised removing a nonexistent file instead of naming the real blocker. Split the stat-failure case: absent lockfile now points at log-dir ownership/permissions; present lockfile keeps the owner-based remove hint (numeric uid when the pwd lookup fails). Log-message-only; the run is still correctly skipped. Addresses the #16 review LOW on utils/dist.py cleaner-lock error path.
…okup In central/broker mode the UI's remote_session mints a Guacamole live-VM session by asking the worker that holds the VM for the task's machine label (central_guac.worker_vm_for_task). It reused apiv2 tasks/view, which is per-tenant scoped (_deny_if_hidden): the control plane's service token is not a cross-tenant principal, so the lookup 404'd and interactive sessions could not attach under multitenancy. Add a dedicated apiv2 endpoint tasks/machine/<task_id>/ returning ONLY the analysis VM label, gated on a staff/superuser caller (the control-plane service identity). Regular tenant tokens get the same generic 404 as a missing task, so no cross-tenant existence enumeration. central_guac now targets this endpoint. The endpoint is intentionally not per-tenant scoped, so it is allowlisted in the apiv2 coverage gate with justification; three tests bound the exemption (staff cross-tenant read works, non-staff 404, response is label-only). The end user is already authorized via can_manage_task at remote_session before this machine-to-machine call is made.
…ort page
Django forbids template variables/attributes beginning with '_' (TemplateSyntaxError
at parse time). Three analysis templates used '_'-prefixed vars in per-task controls
that only render for a user who can toggle/delete a task (owner/tenant-admin), so the
page 500'd for exactly those users and slipped past earlier testing:
- analysis/report.html: the visibility <select> looped `{% for _vis in
visibility_choices %}` and emitted `{{ _vis }}` -> renamed to `vis`. This 500'd
/analysis/<id>/ for any user with can_toggle_visibility.
- analysis/failed_processing.html + analysis/admin/index.html:
`{% can_delete_task ... as _can_delete %}{% if _can_delete %}` -> `can_delete`.
Add a parametrized parse guard (get_template on each template) so a re-introduced
'_'-leading var fails CI.
…lowAny nodes) The initial tasks/machine gate required a staff/superuser principal, but a central worker commonly runs apiv2 with [api] token_auth_enabled=no (DRF AllowAny -> every request is AnonymousUser). There an is_staff-only gate can never pass, so the control plane (itself anonymous in that mode) could never resolve the VM label and interactive attach was impossible. Gate now tracks apiv2's own auth posture, mirroring the existing token_auth_enabled reasoning already documented on tasks_delete_many: enforce staff/superuser ONLY when token auth is enabled (a regular tenant token still gets the generic 404, so no cross-tenant id enumeration); when token auth is disabled the entire apiv2 is open by operator choice and this label-only read follows that same posture. Add a test for the AllowAny path.
… (review #16 LOW) Adversarial review flagged that tasks/machine's cross-tenant label read was gated on `is_staff or is_superuser`, but this PR's cross-tenant authority is viewer_for().is_local_admin (a break-glass / IdP superuser); is_staff is never consulted by the tenancy predicate. An IdP principal provisioned into admin_groups (is_staff=True, is_superuser=False) AND one tenant's idp_groups (is_local_admin=False) could read another tenant's VM label and use the 200-vs-404 as a task-existence oracle across tenants -- the same weaker-than-model anti-pattern already fixed as a HIGH in can_ban_user, reintroduced here. Gate now uses viewer_for(user).is_local_admin (enforced only when token auth is enabled; the AllowAny path is unchanged). Added test_tasks_machine_is_staff_alone_gets_generic_404 using the REAL viewer_for to pin that an is_staff-only principal gets the generic 404. On a token_auth_enabled=yes deployment the central service token must map to a break-glass superuser so the legitimate central->worker call still passes.
…rse guard (review #16 LOW) The parse guard hardcoded 3 files and only proved the top-level template parses: get_template("report.html") does NOT catch a '_'-leading var in an {% include %} partial (includes compile at render, not parse). Replace the 3-file list with a walk over web/templates/analysis/**.html, parsing each file on its own, so a '_'-var in ANY analysis template (including include partials) fails. Add a walk-is-populated guard so an empty parametrize can't go silently green. Validated on the deb box (importlib): 112 analysis templates parse clean; an injected {{ _sneaky }} in an include partial (analysis/CAPE/index.html) that the old 3-file guard would have missed now goes RED.
… merges (review #16 MEDIUM) testpaths=["tests","agent"] means the arg-less `pytest --import-mode=append` step never collects anything under web/, so ~17 of this branch's test files (tenant-isolation, apiv2, guac, submission, ... suites, incl. the template parse guard) never gated a merge. Add a dedicated step running `pytest web/ --import-mode=importlib` (append raises ImportPathMismatchError under pythonpath=web; DJANGO_SETTINGS_MODULE + pythonpath come from pyproject's [tool.pytest.ini_options] and still apply with an explicit path arg). Validated on the deb box: full web/ tree = 380 passed, 0 failed / 0 errors.
… + review #16 follow-ups [HIGH] The tasks/machine gate was conditional on token_auth_enabled, so when token auth is OFF (the shipped default) the gate was skipped entirely and an ANONYMOUS caller under MT got a cross-tenant VM label + a 200-vs-404 task-existence oracle -- while every sibling per-task read still denied anonymous via can_view_task. token_auth_enabled=no removes AUTHENTICATION, not AUTHORIZATION. Gate now UNCONDITIONALLY on viewer_for().is_local_admin (the model's cross-tenant authority); MT-off keeps working because viewer_for marks every principal is_local_admin there. [MEDIUM] The only production caller (central_guac.worker_vm_for_task) must present an is_local_admin token: document that at [central_mode] worker_api_token_file, and make worker_vm_for_task check r.status_code + log on non-200 so an auth misconfig is diagnosable instead of silently returning (None, None) == "no VM" (identical to a local task). [LOW] docstring/urls/central_guac no longer say "staff-gated"; tests use the REAL viewer_for (via mt_enabled) not a stub; add pins for anonymous-denied, denied==missing indistinguishability + the missing-task branch, and MT-off back-compat; widen the template parse guard from analysis/ to all of web/templates (catches {% extends %} parents like base.html); declare pytest_plugins in web/audit/test_tenancy.py so it runs standalone. Box-validated (importlib): tasks_machine + template-walk suites green; full web/ 433 passed.
…ctive on token_auth=no workers) Central-mode interactive attach needs the control plane to read a running task's VM label from the worker's apiv2 tasks/machine. On a worker with [api] token_auth_enabled=no (DRF AllowAny) the request is anonymous so the is_local_admin gate denies it, and the worker's Django auth DB is empty (no principal can be is_local_admin) -- so requiring a Django principal is infeasible on ephemeral, DB-less workers. Add a machine-to-machine authorization path: tasks/machine authorizes when the caller presents the configured [api] control_plane_token as "Authorization: Token <v>", matched in constant time. It is ADDITIVE and FAIL-CLOSED -- fires only when a non-empty secret is configured AND a non-empty bearer matches; an empty/unset secret disables it and the endpoint authorizes by viewer_for().is_local_admin only. It never touches the ORM (works on an empty-auth-DB worker) and leaves global worker apiv2 auth untouched (no dispatch/readiness blast radius). Returns only the pool VM label. central_guac presents the secret via worker_api_token_file. Tests: correct-secret under anonymous/AllowAny -> label; wrong-secret -> 404; empty-secret -> path disabled (anonymous denied); is_local_admin path intact; anonymous-under-MT denied; MT-off see-all. Box-validated (importlib): tasks_machine suite + full web/ 436 passed.
…'t 500 (interactive tunnel)
guac-web loads web.guac_settings (web/asgi.py sets DJANGO_SETTINGS_MODULE=web.guac_settings),
NOT web.settings. guac/views.py gates every view with
@conditional_login_required(login_required, settings.WEB_AUTHENTICATION), but guac_settings never
defined WEB_AUTHENTICATION -> `import guac.urls` raised AttributeError -> EVERY /guac/ request
500'd ("Connection error" in the Guacamole client), so the central-mode interactive live-VM tunnel
never actually worked. The apiv2 coverage gate imports guac.views under web.settings (where the
setting exists), so it never caught this.
Define WEB_AUTHENTICATION in guac_settings, derived from web.conf exactly as web.settings does. Add
a static parity guard (web/guac/test_guac_settings_parity.py) asserting guac_settings defines every
settings.X the guac app references, so it can't regress. Validated: parity test GREEN with the fix,
RED against the pre-fix guac_settings.
…etloc, diagnostics, coverage - cuckoo.conf.default: reword worker_api_token_file to the shared-secret model (must equal the worker's [api] control_plane_token); stop recommending the dangerous global local_admins_manage_all_tenants=yes flip just for interactive. - central_guac: bracket IPv6 worker IPs in the tasks/machine URL and the qemu+ssh libvirt DSN (a valid IPv6 worker IP passes _valid_worker_ip but its bare ':' collided with the port separator -> malformed URL/DSN); refresh the is_local_admin-only docstrings/log to the additive shared-secret OR is_local_admin model. - job_directory: log.warning on a non-200 broker HTTP status instead of a silent degrade to (None, None) == "no job". - tests: cover worker_vm_for_task (200 label / non-200 guard / no-directory / undispatched) and the IPv6 netloc; narrow the anonymous-denied docstring to the in-function gate (a token_auth_enabled=yes install 403s anonymous at DRF dispatch, an earlier denial not a weaker one).
…est.user resolves The fork's guac index view gates the live-VM tunnel on request.user — login_required (when WEB_AUTHENTICATION is on) AND, under multitenancy, can_manage_task(request.user, task). But guac-web loads web.guac_settings, which ran on a throwaway sqlite DB with NO AuthenticationMiddleware, so request.user never existed: every /guac/ request 500'd with "'ASGIRequest' object has no attribute 'user'" on an OIDC+MT stack (poc2). Only the backend resolution (worker_vm_for_task/libvirt/guacd) had been exercised before; the real /guac/ HTTP view had not, so this stayed hidden. - guac_settings.MIDDLEWARE: add django.contrib.auth.middleware.AuthenticationMiddleware after SessionMiddleware so request.user is populated from the session. - guac_settings: adopt the deploy's central DB + auth backends from local_settings.py (the same overlay web.settings imports) so guac-web reads the SAME session/user store the browser is logged into. SECRET_KEY is already shared (both import web/web/secret_key.py). Absent local_settings (dev/single-node) it keeps the sqlite default -> AnonymousUser, matching upstream WEB_AUTHENTICATION=off behavior. - test: static guard — if the guac app reads request.user, guac_settings.MIDDLEWARE must include AuthenticationMiddleware (this class of break is invisible to the apiv2 coverage gate and to the settings-parity scan, since request.user is not a settings.X).
…r include)
guac/error.html and guac/wait.html did {% include "header.html" %}, but header.html is the
main-web nav and references named routes (e.g. {% url 'analysis' %}) that guac-web's minimal
urlconf (web.guac_urls) doesn't define -> NoReverseMatch -> every guac error / VM-still-booting
page 500s. This was latent (guac-web previously crashed earlier, at settings import) and is now
reachable once request.user resolves. Both templates are already complete standalone HTML docs;
drop the header include so they're self-contained, exactly like guac/index.html (the success
page). Validated on a central+OIDC stack: all five guac templates render, the authed error path
returns 200 (was 500), anon still 302s to login.
…t UI-side libvirt-over-SSH
Central-mode interactive attach intermittently failed with guac error 519 (UPSTREAM_NOT_FOUND)
even with a live VM: the guac websocket consumer resolved the VM's VNC port by opening the
WORKER's libvirt over SSH from the UI (qemu+ssh://cape@worker) and parsing the domain XML. That
call is fragile — it hangs, and it returns port='-1' during the brief autoport-assignment window
right after VM start — so guacd got no/`-1` port and the tunnel 519'd. (It "worked" whenever the
SSH-libvirt lookup happened to catch a clean value; that's why it was intermittent.)
Fix: the worker resolves its OWN VNC port locally (reliable, SSH-free) and reports it via the
apiv2 tasks/machine call the UI already makes.
- apiv2 tasks_machine: also return "vnc_port", from central_guac.local_vnc_port(task.machine)
(local libvirt; rejects <=0 and retries the -1 boot window). Still label+port only — bounded.
- central_guac: _worker_machine_body() (shared) + worker_vnc_port_for_task() (parses/validates
the reported port; rejects None/-1/0/garbage). worker_vm_for_task keeps its (label, guest_ip)
signature, so its callers are untouched.
- guac consumer: central path (worker_ip set) uses worker_vnc_port_for_task(); SINGLE-NODE path
(no worker_ip) keeps the existing local _get_vnc_port() byte-for-byte -> non-central/non-MT
unchanged.
- tests: worker_vnc_port_for_task parsing/rejection; tasks_machine response-surface asserts
updated to {error, machine, vnc_port}.
Validated on the box: tests/test_central_mode.py 34 passed (append); apiv2 tasks_machine suite
9 passed/1 skipped (importlib).
…ive attach) Interactive Guacamole attach 500'd on the websocket handshake for every OIDC/allauth user. channels' AuthMiddlewareStack populates scope["user"] via django.contrib.auth.get_user(), which load_backend()s the session's exact auth backend. guac-web (web.guac_settings) imports the allauth backend into AUTHENTICATION_BACKENDS but does NOT install the allauth app stack, so importing allauth.account.auth_backends raises: RuntimeError: Model class allauth.account.models.EmailAddress doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS -> ws handshake HTTP 500. Local/superuser (ModelBackend) sessions load fine, so attach looked like it worked; it only worked for local admins. The tunnel needs the caller's IDENTITY (to gate can_manage_task), not the auth METHOD. GuacAuthMiddlewareStack does the same cookie+session unwrap as channels' stack, but resolves the user straight from the session (user id + session auth-hash check, the same guarantee django.contrib.auth.get_user enforces) without loading the backend. Backend-agnostic: works for ModelBackend, allauth, and any future backend, and keeps guac-web free of the allauth app stack. - web/guac/channels_auth.py: resolve_session_user + BackendAgnosticAuthMiddleware + GuacAuthMiddlewareStack - web/web/asgi.py: wrap the websocket router in GuacAuthMiddlewareStack - web/guac/test_channels_auth.py: regression tests (allauth-backed session resolves; ModelBackend parity; tampered/missing hash + inactive -> Anonymous)
…ing the task When the suricata daemon crashes/restarts mid-analysis it closes the unix command socket. select() reports a closed socket as readable, and SuricataSC.json_recv()'s `while True: recv()` loop had no EOF guard, so recv() returning b'' spun forever. The CAPE suricata processing module then blocked past the 900s pebble task budget and the ENTIRE analysis was killed (status=failed_processing, NO report generated) — one module's failure took down the whole report. Observed on a central worker: suricata core-dumped ~13s into processing a mixed (decrypted-TLS) pcap, then auto-restarted; the module hung the full 900s. Offline/file mode parses the identical pcaps cleanly, so this is a socket-daemon-death hang, not un-parseable input. - json_recv: bail on empty recv (EOF) so send_command() raises, the module breaks out and returns partial results, and the analysis still produces a report (suricata section empty) rather than failing the whole task. - select() readability wait 600s -> COMMAND_TIMEOUT (30s): a healthy suricata acks socket commands in milliseconds; 600s was a pathological ceiling that on its own could exceed the 900s budget. tests/test_suricatasc_recv.py: EOF returns without hanging (regression guard), partial-then-EOF bails, complete + chunked replies still parse.
feat: optional multi-tenant task isolation (visibility + tenant scoping, default off)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )