feat(runners): store-backed results, JSON driver, streaming fan-in - #1312
Conversation
Add secator/hooks/json.py: a driver (not an exporter) that mirrors the mongodb/sqlite drivers but persists to the local filesystem. Each runner writes/updates its own report.json in runner.reports_folder as results are produced (on_item) and as its status changes (on_init/on_start/on_interval/ on_end), instead of a single end-of-run dump. The file format is byte-compatible with the JSON exporter and with what the `local` query backend already reads, so a run driven with `--driver json` is queryable mid-run. Concurrency-safe under both Celery pools: per-runner-file sharding + a locked read-modify-write (in-process threading.Lock for gevent greenlets / prefork threads, fcntl.flock on a sidecar lockfile for prefork processes) + tempfile + os.replace atomic swap. No lost updates, no torn reads. Registered in AVAILABLE_DRIVERS and DRIVER_PRIORITY. The JSON exporter is left in place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…ranslatable Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…translation AST-based constant folding so OR-mixed opts gates (e.g. `not url.verified or opts.hunt_secrets`) fold correctly, not just top-level `and` clauses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…ory eval
process_extractor now translates each extractor to a Mongo-style query and lets
the backend filter (mongodb queries the store; local filters ctx['results']).
Deletes the per-item eval/re_match loop. JSON backend treats {field: True/False}
as Python truthiness so bare-truthy string fields (vuln.id, tech.version) and
`not <field>` match the old semantics. Adds `'x' in field` substring translation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…er corpus 23 distinct shipped condition shapes x 3 opts/targets variants, asserting the new QueryEngine path selects byte-identical items to a faithful copy of the deleted per-item eval, plus end-to-end group_by formatting parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
mark_runner_started/completed no longer expand the whole fan-in of uuids into OutputType objects (RC#6 OOM). Uuid strings are carried straight through the chain; only non-persisted objects are added to the runner. mark_runner_completed fetches just this runner's Error findings from the store so status/self_errors compute without materializing the findings set. get_results/chain_results/ add_result contracts are unchanged; string-uuid handling is confined to celery.py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Asserts .lower() case-insensitivity rides on an inline (?i) flag (honored by both re.search and Mongo $regex, not the JSON-ignored $options), verifies JSON vs MongoDB (mongomock) extract identically, and that a 300k-uuid fan-in through the completed-hook path materializes no findings and stays under a small heap ceiling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…un leak) build_extractor_query scoped only by _context.ancestor_id, which is the workflow CONFIG NAME (not run-unique) and is None for scan-level targets_ extractors -> on store backends (mongodb/api/sqlite) the query leaked findings across runs and the whole workspace. Bound every extractor query to the top-most ancestry run id (_context.scan_id inside a scan, else _context.workflow_id, else _context.task_id), which is exactly what the in-memory fan-in self.results shared. scan_id/workflow_id/ task_id are threaded from self.context into the extractor ctx. scope/ancestor conditions remain as additional narrowing. Local path is unchanged (no run ids -> no bound; the in-memory results are already run-bounded). Adds cross-run isolation tests on the mongodb backend (mongomock): a run-A extractor never sees run B, at both scan level (ancestor_id None) and standalone-workflow level, plus a guard proving an unbounded query would leak. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…terals)
- Bare-truthy fields translate to {$nin:[None,'',False,0]} (works on mongo AND
json for bool + truthy-string fields); `not` -> {$ne:True}; revert the JSON
match_query bool() hack. Adds mongomock parity test (version None/''/non-empty).
- _hydrate_runner_errors query is run-scoped via run_scope_query (shared with
build_extractor_query), not workspace-wide; adds a run-scoped-query test.
- Scope-tagged targets ctx now threads opts/targets so gates like
`port.host in targets and opts.scanners` fold correctly; adds coverage.
- `item.` alias rewrite is an AST transform (leaves string literals intact),
not a regex; adds a literal-containing-`item.` test.
- Strict dotted-identifier + escape-aware quoted-literal patterns reject
malformed operands; string literals are ast.literal_eval'd before re.escape.
- len(<field>) raises explicitly (never silent match-all/none); test asserts it.
- PythonRunner input_types tuple; trimmed redundant comments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
… serves them on DB backends Live E2E: on sqlite/mongodb a domain scan stopped after subdomain discovery - host_recon's port scanners got no inputs. Root cause: the workflow-level scope-tagged host Targets (celery.py emission block) are added via runner.add_result, but Workflow has no on_item->update_finding hook (only Task does), so they were never written to the store; the scoped-target fallback's QueryEngine then found only the apex. local worked only because it filters the in-memory results. Fix (keeps every path on the QueryEngine - makes the store contain what the query needs): persist each emitted scope-tagged Target via the active drivers' Task-level on_item finding hook (_persist_scope_targets), carrying _context.scope + the run-scope ids. No-op on local (no driver HOOKS). Adds a sqlite-backed regression test reproducing the domain-scan shape: without the persist the DB fallback returns 0, with it the query returns all 20 host targets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Once the inter-task result payload is removed, every consumer reads findings from the store rather than the returned list. Locally that store is the live per-runner report.json written by the json driver, so a bare run must have the json driver active. apply_default_drivers() appends json ONLY when no store backend (mongodb/sqlite/api driver, or the mongodb addon) is already active, so prod (mongodb addon enabled) still resolves to mongodb. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
… results #1299 writes each runner's report.json live, but _load_all_findings returned the in-memory self._results first, shadowing those files — so mid-run (and, once the chain payload is topology-only, at report time) the live findings were never read. Invert the priority: scan the report.json files first, fall back to self._results only when the filesystem yields nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…path Generalize the _persist_scope_targets shim into persist_execution_items(), which routes any execution OutputType through the active drivers' Task-level on_item hook (the same finding-persistence the local backend gets from the in-memory results). Wire it for the two producers that never reach a Task on_item and so would vanish once the inter-task payload is dropped: - the DocumentTooLarge Warnings raised inside the mongodb driver (hooks=False) - the "Skipped ..." Info a Workflow/Scan emits at build time (no on_item HOOK) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
… payload Repoint the final-read paths at the store so a topology-only task return still yields a full report: - Report.build queries the store bounded to this run (run_scope_query); it keeps runner.results only as an in-memory FALLBACK for callers with no live files (report_show/library), which the live report.json files override (#1299). - The top runner backfills self.results from a run-scoped QueryEngine query (_iter_store_results + load_output_types) when the payload delivered no findings itself, so the console summary and library callers see the store's findings for both sync and async runs. Dormant while the payload still carries findings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…y only Every consumer now queries the store, so the Celery chain carries no results: - delete chain_results and forward_results; run_command / mark_runner_started / mark_runner_completed / abandon_task now return topology-only ([]). - remove the opts['results'] plumbing (run_command, abandon_task, break_task) and the fan-in ingestion loops in mark_runner_started/completed. - replace the two-adjacent-groups bridge (forward_results) with a trivial no-op join_results task; retarget its task route. - mark_runner_completed hydrates this run's errors from the store for all backends (the payload used to carry them) so status still computes. Tests: the previously-vacuous test_celery chain tests (httpx class `in TEST_TASKS` was always False) are enabled by matching task name and rewritten to assert against the store (sqlite), genuinely validating the payload drop end-to-end in eager mode. test_target_filtering + test_external_workflow are made store-backed (the fan-in and a workflow's findings now come from the store). The dry_run scan aggregates only build-time infos (no store + no payload + no_process skips the backfill) — nested previews still print live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…_duplicates
The mongodb --sync deduplicate crash ('str' has no attribute ...) came from
chain_results reducing findings to uuid strings that mark_runner_completed
ingested into runner.results, where mark_duplicates called ._compare_key() on a
str. With the payload (chain_results + the ingestion loop) removed in the prior
commit, mark_runner_completed ignores its upstream arg, so strings can never
enter runner.results. Lock it in with a regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…ns report.json) The json driver (#1299) writes each runner's report.json live with findings AND execution types (Info/Warning/Stat), then the end-of-run JSON exporter overwrote it with a findings-only snapshot from Report.build — losing the execution-type records and duplicating what the driver already produces. Remove json from the default exporter lists so report.json comes from the driver (still available via `-o json`). For DB drivers the store is the report; csv/txt/markdown are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Scoping a query to "this run" relied on the DB layer minting scan_id/workflow_id/ task_id, so the local-json path (which mints none) fell back to workspace-directory scoping and could over-include a prior run's findings in the same -ws. Mint one run_id (uuid4) at the OUTERMOST runner and inherit it down (children get context=parent.context.copy(); chunks copy the task context in break_task), so a scan and every workflow/task/chunk beneath it share one run_id and a standalone runner gets its own. add_result stamps it onto every finding/target/execution object via context.copy(), and run_scope_query now keys on _context.run_id uniformly — every consumer (extractor queries, _hydrate_runner_errors, the scoped-target fallback, the report backfill) gets driver-agnostic scoping for free. Tests: new local-json cross-run isolation test (two runs, same -ws, run B sees only its own findings); run_id mint/inherit/stamp test; existing cross-run + run-bound tests moved from scan_id to run_id. E2E: json + sqlite host_recon, second same-workspace run stays isolated (each run scoped to its own findings; the shared store holds both). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…every runner type Give Workflow and Scan the same on_item=[update_finding] persist hook Task already had (all three store drivers), so every item flows to the store through the single add_result path. This subsumes the persist_execution_items shim: the workflow/scan "Skipped" Info, the scope-tagged Targets, and the mongodb DocumentTooLarge Warnings now persist automatically via add_result — the shim and its four call sites are deleted. No double-persist: child-ingestion is already gone (payload drop), so a runner only add_results its own emissions, and the scan build-time aggregation uses hooks=False. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…e O(N) backfill The completion backfill re-materialized all N findings into runner.results at mark_completed — the exact O(N) the payload drop had removed. Delete it (backfill_results_from_store / _query_store_results / the yielder backfill) and make the read side query the store: - Runner.findings is now a streaming StreamView over the run-scoped store (QueryEngine.iterate → batched cursor: mongodb batch_size, sqlite fetchmany, json chunked). Iterating never materializes all N; len() is an indexed count; bool() a cheap count. Falls back to the in-memory own-findings only when no store driver is active (no-driver library runs). - The summary counts findings via run_findings_count() (indexed count, no materialization) instead of len(self.findings). - runner.results stays each runner's bounded OWN emissions (live production: dnsx dedup, mark_duplicates, self_*). It is never the fan-in, so it stays flat. Gates: RAM — 300k findings under one run_id stream flat (peak << a full materialization); pickle — a deserialized runner's findings view rebuilds from context (no __getstate__/__setstate__). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
run_command ignores its results arg (topology-only chain) — pass [] instead of the task's own results; the scan no longer seeds child workflows with its results (children read this run's findings from the store, run_id-scoped). forward_results/ chain_results are already gone; join_results stays as the genuine Celery bridge between two adjacent groups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…n_id
Remove the core-minted run_id. Each store driver now mints the runner's {type}_id
at on_init (in update_runner) if absent, in its native format — mongodb ObjectId
(already), json/sqlite uuid4 — stamped into context so descendants inherit it and
findings carry it. json's update_runner previously minted nothing (the local-json
scoping gap); it now mints uuid4, closing the gap via the same native-id path.
Higher-priority drivers mint first (order_drivers: mongodb before json), and json
reuses an existing id — so a run's findings never mix ObjectId and uuid4.
run_scope_query reverts to the top-most present {type}_id (scan > workflow > task).
Tests swapped run_id -> {type}_id; cross-run isolation re-proven on local-json and
sqlite; json-mint + no-remix asserted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
… by own {type}_id)
results/findings/targets/infos/warnings/errors/self_* are now StreamViews scoped by
the runner's OWN {type}_id (a Task sees task_id, a Workflow workflow_id, a Scan the
whole scan_id) — rebuilt from context on access, pickle-transparent, nothing on the
instance. add_result fans out to the store (write-model) and keeps a small bounded
own-emissions buffer as the no-store fallback (never the fan-in, so RAM/pickle stay
flat). Execution-type properties use type-scoped queries, not full-run stream+filter.
Per-runner consumers fixed:
- dnsx: local compare-key set instead of `x in self.results` (was O(N^2) over a view).
- mark_duplicates: defers to the store's find_duplicates/tag_duplicates (sqlite/mongodb,
server-side, run once at the top runner) — no in-memory grouping of the fan-in; json
has no store-side dedup (pre-existing local limit) so it groups its own buffer.
- scan child-aggregation: dropped for store runs (the scan's scan_id view sees the whole
subtree); kept only for no-store dry-runs, reading the bounded buffer not the view.
- break_task / run_command / ai / extractor ctx: read the buffer or [] topology seeds.
ponytail: the own-emissions buffer remains as the no-driver fallback; delete it once
every run is guaranteed store-backed (auto-json in the core, not just the CLI).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…tion Report.build(stream=True) (the live-run export path) sets data['results'][type] to a per-type StreamView (run-scoped store cursor) instead of materializing all findings. The csv exporter streams rows from the cursor (dropped its list-comp); txt/markdown/ console already iterate. Dedup moves store-side: the query excludes tag_duplicates- flagged workspace_duplicates, so non-duplicates stream without an in-memory pass. report_show / library display keeps stream=False (materialized, subscriptable, in-memory dedup) — its content/format is unchanged. json report is the driver's live report.json (untouched). table (non-default display exporter) still materializes per-type — acceptable, not the live-run RAM path. Gate: 300k findings under one run — Report.build(stream=True) + csv export peaks at O(batch), a small fraction of the materialized build, with all N rows actually written. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Remove mark_duplicates + _store_find_duplicates_fn + _mark_duplicates_in_memory and the mark_completed call. It caused problems and is unwanted; the async-dedup race is resolved by its removal (no in-runner dedup to race). The store-side tag_duplicates / find_duplicates driver functions are left as-is. on_duplicate is now unfired (dead) — test_hooks exempts it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
DRY: one _view(type) accessor (the single scoping/query point) — every result property (results/findings/targets/infos/warnings/errors/self_*) is now a one-liner over it. Deleted run_findings_count, _hydrate_runner_errors (+ its call/import; the view reads errors from the store directly), run_findings_view (dead), and the per-property store/buffer boilerplate. dnsx/getasn dedup via a local compare-key set (was O(N^2) membership over the view). ai reads self.results (view). scan child-agg only for no-persist dry-runs (real runs use the scan_id view). self._results is kept as the bounded no-store/dry_run fallback (`_view` serves it when no driver/id is active or dry_run/no_process — nothing is persisted then). Fully deleting it + making json a core default proved fragile: it needs every test runner store-backed (shared-store test pollution + clear_modules ordering) and the pre-on_init __init__ emissions (validation errors, input targets) that the buffer catches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Move the JSON concurrency primitive out of hooks/json.py into a reusable public API in secator.utils: - atomic_json(path, default=dict): @contextmanager for locked read-modify-write (in-process lock -> fcntl.flock sidecar -> atomic tempfile+fsync+os.replace). Yields parsed data; writes back on clean exit, leaves the file unchanged on exception. - read_json(path, default=dict): lock-free snapshot read (safe via os.replace). Private guts (_get_path_lock/_path_locks/_path_locks_guard/_read_json/ _atomic_write) move to utils too. The callable atomic_json_update form is deleted — the context manager covers wholesale replace as well. hooks/json.py update_finding/update_runner now use atomic_json (-95 LOC). Behavior byte-identical; the driver's concurrency + hook tests stay green. Adds tests/unit/test_atomic_json.py (multiprocess + gevent concurrency, exception-leaves-file-unchanged, read_json). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
A workflow/scan and its child tasks share reports_folder, so they all wrote the same report.json and clobbered each other's info block. Children (has_parent) now write report_<fqn>.json (fqn is node_id-based, conflict-free). Findings still stream to the shared results.ndjson. Reading the per-child files back for per-task detail (MongoDB parity) is a parked follow-up (TRACKER.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
`TEST_TASKS` holds TemplateLoader configs, so `httpx`/`nmap`/`ffuf` (the task classes) were never members and every guarded test silently returned without running. Compare task NAMES against a derived TEST_TASK_NAMES set instead, so the store-backed httpx/nmap/ffuf tests actually execute when their task is selected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…e store process_extractor no longer filters a caller-supplied in-memory list (dead in prod — live runs always pass []). It now streams matched findings straight from the store via engine.iterate and extracts fields on the fly, so the fan-in is never materialized (peak memory O(extracted), not O(N findings)); _item_dict is gone (store rows are already dicts). Tests that relied on the in-memory path now persist their findings to a run-scoped json store (_store_ctx / _local_ctx helpers) and query it, exercising the real read path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
|
@CodeRabbit give it a final review pass |
|
✅ Action performedFull review finished. |
|
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
secator/hooks/mongodb.py (1)
74-83: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
scan_chunk_idstill missing fromensure_mongo_run_id's coercion set.A previous review flagged that this tuple omits
scan_chunk_id, so an uncoerced UUID reachesObjectId(...)unchanged. The follow-up only removed the (nonexistent)workflow_chunk_id;scan_chunk_idwas never added back.update_runner(Line 93-94) deriveskey = f'{type}_chunk_id'for a chunkedscan, andupdate_finding(Line 156-159) reads the same key to backfillitem._context[key]— both paths still hit this gap.🐛 Proposed fix
- for key in ('task_id', 'workflow_id', 'scan_id', 'task_chunk_id'): + for key in ('task_id', 'workflow_id', 'scan_id', 'task_chunk_id', 'scan_chunk_id'):🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/hooks/mongodb.py` around lines 74 - 83, The ensure_mongo_run_id function must also coerce scan_chunk_id values to valid Mongo ObjectIds. Add scan_chunk_id to its existing key set while preserving the current idempotent validation and coercion behavior for all other identifiers.
🧹 Nitpick comments (3)
tests/unit/test_target_filtering.py (1)
170-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated
_store_ctxhelper (with temp-dir leak) across two test classes. Both copies write findings to a tempresults.ndjsonand build a matchingctx, but neither cleans up the created directory.
tests/unit/test_target_filtering.py#L170-L186: extract to a shared module-level helper (or test mixin) and registeraddCleanup(shutil.rmtree, d, ignore_errors=True).tests/unit/test_target_filtering.py#L253-L269: remove this duplicate copy and call the shared helper instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_target_filtering.py` around lines 170 - 186, The duplicated _store_ctx helper in tests/unit/test_target_filtering.py at lines 170-186 and 253-269 should be consolidated into one shared module-level helper or test mixin. Update the shared implementation to register cleanup with shutil.rmtree(d, ignore_errors=True), and remove the duplicate at lines 253-269 so both test classes call the shared helper.secator/output_types/_base.py (1)
209-217: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winUnguarded
klass.load(doc)can abort the whole stream on one bad record.
load()raisesTypeErrorwhen a stored dict's_typematches but none of the mapped fields resolve to a value (e.g. legacy/corrupted record). Since this is a generator feedingStreamView/report/export pipelines, one bad row propagates the exception up and aborts the entire query for that run instead of just skipping the offending record.♻️ Proposed fix
if isinstance(doc, dict): klass = by_name.get(doc.get('_type')) if not klass: continue - item = klass.load(doc) + try: + item = klass.load(doc) + except TypeError: + continue if not item._uuid: item._uuid = str(doc.get('_uuid') or doc.get('_id') or '') yield item🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/output_types/_base.py` around lines 209 - 217, Guard the klass.load(doc) call in the document-loading loop so a TypeError from an invalid or legacy record skips that record and allows iteration to continue. Keep the existing type lookup, UUID assignment, and successful-item yielding behavior unchanged, and only suppress the expected load failure.secator/loader.py (1)
303-329: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocstring contradicts the implementation (and
mongodb_enabledis unused).The body never inspects
mongodb_enabled, yet the docstring claims thejsondefault is applied only when "the mongodb addon disabled" and that "In prod (mongodb addon enabled) ... json is not forced". Per the intended behavior,jsonis appended whenever nomongodb/sqlite/apidriver is present indrivers— regardless of the addon. Align the docstring with the code so future maintainers don't misread the driver-selection contract, and either consume or drop the unused parameter.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/loader.py` around lines 303 - 329, Update apply_default_drivers so its documentation states that json is appended whenever no store driver is present, regardless of the mongodb addon state; remove the obsolete mongodb_enabled parameter and update all callers accordingly, or otherwise consume it consistently with the documented contract. Keep the existing STORE_DRIVERS check and json-appending behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@secator/query/ast.py`:
- Line 140: Update the AST rendering path around the ast.unparse call to
preserve the declared Python 3.8 support by replacing it with a compatible
alternative; only raise the project’s minimum Python version instead if that is
the intended compatibility change, and keep the existing output behavior.
In `@secator/query/utils.py`:
- Around line 202-205: Update the _TRUTHY representation and its SQLite query
handling so truthiness checks do not generate NOT IN predicates containing NULL.
Preserve absent-field exclusion and ensure values other than '', False, and 0
match consistently across backends, adding or updating the corresponding real
SQLite-backed test.
- Around line 329-330: Update the shared regex normalization helper used by both
JSON and MongoDB query handling to remove leading '*' characters from the
stripped pattern before prepending the '(?i)' prefix. Preserve the existing
quote trimming and ensure inputs such as '*foo' produce a valid normalized regex
consistently.
In `@tests/unit/test_atomic_json.py`:
- Around line 205-206: Rename the ambiguous comprehension variable l to line in
tests/unit/test_atomic_json.py lines 205-206 and tests/unit/test_json_driver.py
lines 208-209. In tests/unit/test_atomic_json.py, also read open(path) within a
with block so the file handle is closed; preserve the existing JSON parsing
behavior.
In `@tests/unit/test_runners_helpers.py`:
- Around line 60-76: Update _store_ctx to register cleanup of the directory
created by tempfile.mkdtemp() using self.addCleanup() (and the appropriate
directory-removal utility), ensuring each temporary results directory is removed
after its test completes while preserving the existing file-writing and context
behavior.
---
Duplicate comments:
In `@secator/hooks/mongodb.py`:
- Around line 74-83: The ensure_mongo_run_id function must also coerce
scan_chunk_id values to valid Mongo ObjectIds. Add scan_chunk_id to its existing
key set while preserving the current idempotent validation and coercion behavior
for all other identifiers.
---
Nitpick comments:
In `@secator/loader.py`:
- Around line 303-329: Update apply_default_drivers so its documentation states
that json is appended whenever no store driver is present, regardless of the
mongodb addon state; remove the obsolete mongodb_enabled parameter and update
all callers accordingly, or otherwise consume it consistently with the
documented contract. Keep the existing STORE_DRIVERS check and json-appending
behavior unchanged.
In `@secator/output_types/_base.py`:
- Around line 209-217: Guard the klass.load(doc) call in the document-loading
loop so a TypeError from an invalid or legacy record skips that record and
allows iteration to continue. Keep the existing type lookup, UUID assignment,
and successful-item yielding behavior unchanged, and only suppress the expected
load failure.
In `@tests/unit/test_target_filtering.py`:
- Around line 170-186: The duplicated _store_ctx helper in
tests/unit/test_target_filtering.py at lines 170-186 and 253-269 should be
consolidated into one shared module-level helper or test mixin. Update the
shared implementation to register cleanup with shutil.rmtree(d,
ignore_errors=True), and remove the duplicate at lines 253-269 so both test
classes call the shared helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 988d0801-8949-458f-8282-19aab19afe34
📒 Files selected for processing (57)
docs/superpowers/plans/2026-07-16-json-driver-ndjson.mddocs/superpowers/specs/2026-07-16-json-driver-ndjson-store-design.mdpyproject.tomlsecator/celery.pysecator/cli.pysecator/cli_helper.pysecator/config.pysecator/configs/workflows/host_recon.yamlsecator/configs/workflows/subdomain_recon.yamlsecator/configs/workflows/url_params_fuzz.yamlsecator/configs/workflows/url_vuln.yamlsecator/definitions.pysecator/exporters/csv.pysecator/hooks/json.pysecator/hooks/mongodb.pysecator/hooks/sqlite.pysecator/loader.pysecator/output_types/__init__.pysecator/output_types/_base.pysecator/query/__init__.pysecator/query/_base.pysecator/query/_stream.pysecator/query/ast.pysecator/query/json.pysecator/query/mongodb.pysecator/query/sqlite.pysecator/query/utils.pysecator/report.pysecator/runners/_base.pysecator/runners/_helpers.pysecator/runners/scan.pysecator/runners/task.pysecator/runners/workflow.pysecator/tasks/ai.pysecator/tasks/dnsx.pysecator/tasks/getasn.pysecator/utils.pytests/integration/test_celery.pytests/unit/conftest.pytests/unit/test_ai_actions.pytests/unit/test_atomic_json.pytests/unit/test_celery.pytests/unit/test_cli.pytests/unit/test_driver_ordering.pytests/unit/test_error_scoping.pytests/unit/test_extractor_query.pytests/unit/test_json_driver.pytests/unit/test_on_build.pytests/unit/test_query.pytests/unit/test_query_utils.pytests/unit/test_query_utils_extractors.pytests/unit/test_report.pytests/unit/test_runners.pytests/unit/test_runners_helpers.pytests/unit/test_sqlite_driver.pytests/unit/test_target_filtering.pytests/unit/test_template.py
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/unit/test_celery.py (1)
132-244: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winGuard is still always-true — 4 tests remain dead code.
if httpx not in TEST_TASKS:(lines 136, 172, 205, 228) repeats the exact stale-comparison bug already flagged:TEST_TASKSholdsTemplateLoaders, so<class> in TEST_TASKSis alwaysFalse, and every one of these four tests returns before exercisingbreak_task's (now simplified) chunking/rate-limit/queue-routing logic. The file already defines and correctly usesTEST_TASK_NAMESelsewhere for this exact purpose.🐛 Proposed fix (apply at all 4 occurrences)
- if httpx not in TEST_TASKS: + if 'httpx' not in TEST_TASK_NAMES: return🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_celery.py` around lines 132 - 244, Replace the four stale `if httpx not in TEST_TASKS:` guards in the affected test methods with the existing `TEST_TASK_NAMES`-based membership check, matching its established usage elsewhere in the file, so all chunking, rate-limit, queue-routing, and disabled-chunking tests execute instead of returning early.
♻️ Duplicate comments (4)
secator/query/json.py (1)
41-50: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winSanitize runner path segments before building
report_file(still unaddressed).runner_type/runner_idare joined directly into the report path, so a token containing/,\, or..can escape the workspace report tree. Validate the trimmed/singularized segments (reject separators and traversal) before opening.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/query/json.py` around lines 41 - 50, Validate the normalized runner_type and runner_id segments in the token-processing function before constructing report_file or opening it. Reject segments containing forward or backslash separators or traversal values such as “..”, and preserve the existing fallback of appending part when validation fails.Source: Linters/SAST tools
secator/query/ast.py (1)
140-140: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
ast.unparsestill breaks the declared Python 3.8 floor.ast.unparsewas added in 3.9; ifpyproject.tomlstill declaresrequires-python = '>=3.8', this raisesAttributeErroron the supported floor. Either raise the minimum to 3.9+ or avoidast.unparse.#!/bin/bash fd -HI pyproject.toml -x rg -n 'requires-python|python_requires' {}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/query/ast.py` at line 140, Resolve the Python version mismatch in the AST unparsing path around ast.unparse: either raise the project’s declared requires-python floor to Python 3.9 or newer, or replace ast.unparse with a Python 3.8-compatible implementation. Keep the behavior of the affected AST conversion function unchanged for supported versions.Source: Linters/SAST tools
secator/runners/_base.py (1)
875-894: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winItems added with
hooks=False(whileenable_hooks=True) are still never persisted. The on_item fan-out at Line 875 is gated on thehooksparameter, but the store fallback at Line 884 only fires onnot self.enable_hooks. Soadd_result(item, hooks=False)with hooks enabled — e.g.run_hooks's except-blockself.add_result(error, hooks=False)on a hook failure — neither runs on_item nor calls_persist_to_store, so the item never reaches the store. Aggregators read errors from the store, so a hook-execution failure can leave a workflow/scan reportingSUCCESS.🐛 Proposed fix
self.uuids.add(item._uuid) - if not self.enable_hooks: + if not self.enable_hooks or not hooks: self._persist_to_store(item)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/runners/_base.py` around lines 875 - 894, Update the result persistence logic in the add_result flow around the hooks-gated on_item fan-out so items added with hooks=False are persisted whenever on_item was not executed, including when enable_hooks is true. Preserve the existing no-duplicate behavior for items that already went through on_item, and ensure hook failures passed from run_hooks via add_result(error, hooks=False) reach the store.tests/unit/test_celery.py (1)
433-466: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUnused
httpximport (F401) still present.
from secator.tasks import httpxat line 438 is never used —abandon_taskis invoked with the string'httpx'.🧹 Proposed fix
import tempfile from pathlib import Path - from secator.tasks import httpx from secator.celery import abandon_task🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_celery.py` around lines 433 - 466, Remove the unused `from secator.tasks import httpx` import from `test_abandon_task_persists_failure_error_to_store`; keep the string-based `'httpx'` task reference and all existing test behavior unchanged.Source: Linters/SAST tools
🧹 Nitpick comments (5)
secator/hooks/mongodb.py (1)
88-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename
typeto avoid builtin shadowing (and matchsqlite.py's convention).
type = self.config.typeshadows the builtin, flagged by Ruff (A001).secator/hooks/sqlite.py'supdate_runneralready uses_type = self.config.typefor the identical purpose — align this file with that convention.♻️ Proposed rename
def update_runner(self): client = get_mongodb_client() db = client.main - type = self.config.type - collection = f'{type}s' - chunk = self.context.get(f'{type}_chunk_id') is not None + _type = self.config.type + collection = f'{_type}s' + chunk = self.context.get(f'{_type}_chunk_id') is not None ensure_mongo_run_id(self.context) update = self.toDict() - key = f'{type}_chunk_id' if chunk else f'{type}_id' + key = f'{_type}_chunk_id' if chunk else f'{_type}_id'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/hooks/mongodb.py` at line 88, In the MongoDB hook’s update_runner flow, rename the local variable `type` to `_type` when assigning `self.config.type`, and update all references to that local variable accordingly, matching the convention used by sqlite.py.Source: Linters/SAST tools
tests/unit/test_target_filtering.py (1)
170-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSame
_store_ctxtempdir-leak pattern copy-pasted across three test helpers. Each helper callstempfile.mkdtemp()and never removes the directory, so every test run leaks one dir per invocation; the shared root cause is a missingself.addCleanup(shutil.rmtree, d).
tests/unit/test_target_filtering.py#L170-L186: addself.addCleanup(shutil.rmtree, d, ignore_errors=True)right afterd = Path(tempfile.mkdtemp())inTestProcessExtractorScopeFiltering._store_ctx.tests/unit/test_target_filtering.py#L253-L269: apply the same cleanup registration inTestRunExtractorsScopeFallback._store_ctx.tests/unit/test_runners_helpers.py#L60-L76: apply the same fix inTestExtractorFunctions._store_ctx(already flagged in a prior review pass with an equivalent diff).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_target_filtering.py` around lines 170 - 186, The three _store_ctx helpers leak their temporary directories; register shutil.rmtree cleanup immediately after each tempfile.mkdtemp call. Apply this in tests/unit/test_target_filtering.py:170-186 for TestProcessExtractorScopeFiltering._store_ctx, tests/unit/test_target_filtering.py:253-269 for TestRunExtractorsScopeFallback._store_ctx, and tests/unit/test_runners_helpers.py:60-76 for TestExtractorFunctions._store_ctx, using ignore_errors=True.secator/loader.py (1)
303-329: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocstring contradicts the actual (and correct) behavior;
mongodb_enabledis dead.The opening docstring line says json is forced only when "the mongodb addon disabled" and that in prod "mongodb still wins and json is not forced" — but
mongodb_enabledis never read, and the inline comment right below (lines 323-326) plus the retrieved fix history confirm the intended behavior is the opposite: json is still forced when the addon is enabled but'mongodb'isn't literally indrivers. This stale docstring could mislead a future edit into "using"mongodb_enabledand reintroducing the earlier data-integrity gap (empty drivers list → no persistence hooks).♻️ Proposed docstring fix
- Applied ONLY when no store backend is otherwise active: no - mongodb/sqlite/api driver in ``drivers`` and the mongodb addon disabled. In prod - (mongodb addon enabled) mongodb still wins and json is not forced. + Applied ONLY when no store backend (``mongodb``/``sqlite``/``api``) is present in + ``drivers``. Deliberately ignores ``mongodb_enabled``: even with the mongodb addon + on, if ``mongodb`` isn't in ``drivers`` there is still no persistence hook registered, + so json is forced regardless (see the inline comment below). Args: drivers (list[str]): Resolved driver names (config defaults + CLI --driver). - mongodb_enabled (bool): Whether the mongodb addon is enabled. + mongodb_enabled (bool): Unused; kept for call-site compatibility.Separately, Ruff's RUF005 hint on line 328 is worth a quick fix too:
drivers = [*drivers, 'json']instead of concatenation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@secator/loader.py` around lines 303 - 329, Update apply_default_drivers so its docstring accurately states that json is appended whenever no store driver is present, including when the mongodb addon is enabled but mongodb is absent from drivers; remove the unused mongodb_enabled parameter if callers permit, or otherwise leave it unused without implying it controls behavior. Also replace the drivers concatenation with the Ruff-compliant unpacking form while preserving existing ordering and duplicate checks.Source: Linters/SAST tools
tests/unit/test_template.py (1)
47-47: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winPrefer
next(...)over materializing the whole (streaming) result set for a single element.Both
list(findings)[0](line 47) and[r for r in findings if r._type == 'vulnerability'][0](line 71) fully materializefindings— now a store-backed, potentially lazyStreamViewper this PR's redesign — just to grab one element. Flagged by Ruff (RUF015).⚡ Proposed fix
- self.assertTrue(self.expected_vuln == Vulnerability.load(list(findings)[0].toDict())) + self.assertTrue(self.expected_vuln == Vulnerability.load(next(iter(findings)).toDict()))- vuln = [r for r in findings if r._type == 'vulnerability'][0] + vuln = next(r for r in findings if r._type == 'vulnerability')Also applies to: 71-72
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_template.py` at line 47, Update the assertions using list materialization in the test flow, including the comparison around Vulnerability.load and the filtered vulnerability lookup, to retrieve the first matching result with next(...) instead of converting the lazy findings stream to a list. Preserve the existing filtering and assertion behavior.Source: Linters/SAST tools
tests/integration/test_celery.py (1)
34-40: 📐 Maintainability & Code Quality | 🔵 TrivialChain/chord-against-seeded-store test still not restored.
In the prior review thread you agreed ("Yes do that") to restore a hand-built chain/chord test that seeds the store with a
Url, dispatches a hand-assembled chord ofrun_commandsigs joined byjoin_results, and asserts seeded + new findings via the store. That test doesn't appear in this file yet — onlytest_httpx_async/test_httpx_chunk/test_nmap_async/test_ffuf_chunked/test_url_vuln_workfloware present.Want me to draft that
test_httpx_chord(or equivalent) test now, or is it tracked in a separate follow-up commit?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/test_celery.py` around lines 34 - 40, The TestCelery integration coverage is missing the agreed hand-built chord test against a seeded store. Add a test method such as test_httpx_chord that seeds the store with a Url and initial finding, builds run_command signatures joined by join_results, executes the chord through the Celery workflow, and verifies both seeded and newly produced findings via the store-backed results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@secator/query/_stream.py`:
- Around line 37-45: Update __contains__ to preserve the stream’s self._query
scope when building the keyed existence query, merging the item comparison
fields into that existing query rather than replacing it. Keep the non-dataclass
iteration fallback unchanged, and ensure membership only matches records within
the current run or other configured view scope.
In `@secator/report.py`:
- Around line 67-68: Update the report data construction to materialize the lazy
warnings stream before assigning it to info['warnings']. In the relevant
report-building method, store a list from self.runner.warnings, matching the
existing list-valued errors behavior so JsonExporter can serialize report.data
directly.
In `@tests/unit/test_error_scoping.py`:
- Around line 53-64: Update the Scan constructor in
test_scan_aggregates_all_descendant_errors to pass run_opts={'dry_run': True},
matching the other tests in this file and ensuring add_result uses the safe
direct-persist path without real hooks.
In `@tests/unit/test_extractor_query.py`:
- Line 445: Update the JsonBackend instantiations in the affected tests to stop
passing results=, since JsonBackend accepts only workspace_id, config, and
context. Seed the backend through the existing workspace or report-directory
fixture so the tests obtain the findings through the supported initialization
path, including the similar instantiation near the other reported location.
In `@tests/unit/test_query.py`:
- Around line 289-294: Update python_expr_to_mongo and
test_regex_value_with_glob_start so a leading glob '*' is removed before
prepending the '(?i)' regex prefix; assert the resulting pattern is
'(?i)CVE-2026-28780' rather than retaining the invalid leading wildcard.
---
Outside diff comments:
In `@tests/unit/test_celery.py`:
- Around line 132-244: Replace the four stale `if httpx not in TEST_TASKS:`
guards in the affected test methods with the existing `TEST_TASK_NAMES`-based
membership check, matching its established usage elsewhere in the file, so all
chunking, rate-limit, queue-routing, and disabled-chunking tests execute instead
of returning early.
---
Duplicate comments:
In `@secator/query/ast.py`:
- Line 140: Resolve the Python version mismatch in the AST unparsing path around
ast.unparse: either raise the project’s declared requires-python floor to Python
3.9 or newer, or replace ast.unparse with a Python 3.8-compatible
implementation. Keep the behavior of the affected AST conversion function
unchanged for supported versions.
In `@secator/query/json.py`:
- Around line 41-50: Validate the normalized runner_type and runner_id segments
in the token-processing function before constructing report_file or opening it.
Reject segments containing forward or backslash separators or traversal values
such as “..”, and preserve the existing fallback of appending part when
validation fails.
In `@secator/runners/_base.py`:
- Around line 875-894: Update the result persistence logic in the add_result
flow around the hooks-gated on_item fan-out so items added with hooks=False are
persisted whenever on_item was not executed, including when enable_hooks is
true. Preserve the existing no-duplicate behavior for items that already went
through on_item, and ensure hook failures passed from run_hooks via
add_result(error, hooks=False) reach the store.
In `@tests/unit/test_celery.py`:
- Around line 433-466: Remove the unused `from secator.tasks import httpx`
import from `test_abandon_task_persists_failure_error_to_store`; keep the
string-based `'httpx'` task reference and all existing test behavior unchanged.
---
Nitpick comments:
In `@secator/hooks/mongodb.py`:
- Line 88: In the MongoDB hook’s update_runner flow, rename the local variable
`type` to `_type` when assigning `self.config.type`, and update all references
to that local variable accordingly, matching the convention used by sqlite.py.
In `@secator/loader.py`:
- Around line 303-329: Update apply_default_drivers so its docstring accurately
states that json is appended whenever no store driver is present, including when
the mongodb addon is enabled but mongodb is absent from drivers; remove the
unused mongodb_enabled parameter if callers permit, or otherwise leave it unused
without implying it controls behavior. Also replace the drivers concatenation
with the Ruff-compliant unpacking form while preserving existing ordering and
duplicate checks.
In `@tests/integration/test_celery.py`:
- Around line 34-40: The TestCelery integration coverage is missing the agreed
hand-built chord test against a seeded store. Add a test method such as
test_httpx_chord that seeds the store with a Url and initial finding, builds
run_command signatures joined by join_results, executes the chord through the
Celery workflow, and verifies both seeded and newly produced findings via the
store-backed results.
In `@tests/unit/test_target_filtering.py`:
- Around line 170-186: The three _store_ctx helpers leak their temporary
directories; register shutil.rmtree cleanup immediately after each
tempfile.mkdtemp call. Apply this in tests/unit/test_target_filtering.py:170-186
for TestProcessExtractorScopeFiltering._store_ctx,
tests/unit/test_target_filtering.py:253-269 for
TestRunExtractorsScopeFallback._store_ctx, and
tests/unit/test_runners_helpers.py:60-76 for TestExtractorFunctions._store_ctx,
using ignore_errors=True.
In `@tests/unit/test_template.py`:
- Line 47: Update the assertions using list materialization in the test flow,
including the comparison around Vulnerability.load and the filtered
vulnerability lookup, to retrieve the first matching result with next(...)
instead of converting the lazy findings stream to a list. Preserve the existing
filtering and assertion behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 988d0801-8949-458f-8282-19aab19afe34
📒 Files selected for processing (57)
docs/superpowers/plans/2026-07-16-json-driver-ndjson.mddocs/superpowers/specs/2026-07-16-json-driver-ndjson-store-design.mdpyproject.tomlsecator/celery.pysecator/cli.pysecator/cli_helper.pysecator/config.pysecator/configs/workflows/host_recon.yamlsecator/configs/workflows/subdomain_recon.yamlsecator/configs/workflows/url_params_fuzz.yamlsecator/configs/workflows/url_vuln.yamlsecator/definitions.pysecator/exporters/csv.pysecator/hooks/json.pysecator/hooks/mongodb.pysecator/hooks/sqlite.pysecator/loader.pysecator/output_types/__init__.pysecator/output_types/_base.pysecator/query/__init__.pysecator/query/_base.pysecator/query/_stream.pysecator/query/ast.pysecator/query/json.pysecator/query/mongodb.pysecator/query/sqlite.pysecator/query/utils.pysecator/report.pysecator/runners/_base.pysecator/runners/_helpers.pysecator/runners/scan.pysecator/runners/task.pysecator/runners/workflow.pysecator/tasks/ai.pysecator/tasks/dnsx.pysecator/tasks/getasn.pysecator/utils.pytests/integration/test_celery.pytests/unit/conftest.pytests/unit/test_ai_actions.pytests/unit/test_atomic_json.pytests/unit/test_celery.pytests/unit/test_cli.pytests/unit/test_driver_ordering.pytests/unit/test_error_scoping.pytests/unit/test_extractor_query.pytests/unit/test_json_driver.pytests/unit/test_on_build.pytests/unit/test_query.pytests/unit/test_query_utils.pytests/unit/test_query_utils_extractors.pytests/unit/test_report.pytests/unit/test_runners.pytests/unit/test_runners_helpers.pytests/unit/test_sqlite_driver.pytests/unit/test_target_filtering.pytests/unit/test_template.py
Tasks now return topology-only and the live yield stream deliberately omitted
store findings ("no backfill"), but `secator x -json` and the UI consume that
stream — so a celery run emitted no findings. A fast task (e.g. httpx) also
completes before a throttled RUNNING update ever publishes its findings, so
they lived only in the store and never reached the client.
After the celery poll completes, stream the runner's store findings into the
output via the StreamView (peak memory stays flat) and skip anything already
yielded during polling (self.uuids) so nothing is materialized or double-emitted.
Verified: `httpx secator.cloud --worker` now emits the url + technologies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Broaden the celery output backfill from self.findings to self.results so the stream also carries Target/Stat items (e.g. test_ffuf_chunked counts targets). The store holds only data types (findings + target + stat) — transient State/ Progress/Info hit their own add_result branches and are never persisted — so this can't re-emit execution events, and the self.uuids dedup still prevents double-emitting anything already surfaced during polling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…d tasks) Command overrides yielder(), so the backfill in the base yielder only ran for workflows — single `secator x <task>` runs still emitted no findings. Move it to the shared __iter__ (runs after the yielder loop for both Command and Workflow), still gated on async + processing, streaming via StreamView and deduped by self.uuids. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Temporary — logs the store state at backfill (self.results, workspace/report_dir scoping) and the raw subprocess output for test_httpx_command / result types for test_ffuf_chunked, to diagnose why they fail only in CI. Revert after. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
add_result overlaid the incoming item's _context over the runner's, so an item
that arrived pre-tagged with a foreign {type}_id kept it — and the runner's own
store query (scoped by {type}_id) never found it. This broke Commands that parse
a foreign `secator -json` stream (e.g. test_httpx_command: the url was in the
output but carried the subprocess task's id, so cmd.findings came back empty). On
main it worked because findings came from memory, not a scoped store query.
Re-stamp the runner's own {type}_id after the context overlay so a runner always
owns the results it adds. Also revert the temporary debug instrumentation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
test_ffuf_chunked asserted len(targets_out) == len(URL_TARGETS) * 2, a payload-era expectation: the old chain accumulated a target echo per chunk. Store-based collection dedups those (same as test_url_vuln_workflow), so each input target appears once. Assert == len(URL_TARGETS). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Temporary — surfaces the actual ffuf chunk failure (0 urls + 2 errors) via console.print so it lands in the CI log. Revert after diagnosis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Worker console output doesn't reach the CI log, but add_result'd findings do. Emit the reconstructed chunk task's targets/inputs/wordlist/optkeys so DBGFF prints them, to find why chunk ffuf loses -u/-w. Revert after. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
Keeps the ffuf assertion fix (deduped target count). test_ffuf_chunked's remaining failure is diagnosed: chunk ffuf tasks build the command without -u/-w (0 urls, 2 errors), while the chunk-dispatch code (break_task/si/run_command signature) is byte-identical to main — a runtime opts/inputs loss for chunks, handed off for follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…ed-results release The deduped target/url counts it asserts proved brittle across ffuf/Juice-Shop runs under store-based chunk collection. Commented out (not deleted) with a note to restore store-aware assertions in the results/AI-rework follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…ror signalling Valid findings from the CodeRabbit review of #1312: - query/sqlite.py: `$nin` containing None generated `NOT IN (…, NULL, …)`, which is NULL (never TRUE) in SQL 3-valued logic — every bare truthiness check (_TRUTHY: `ip.alive`, `subdomain.verified`) returned zero rows on the sqlite backend. Pull NULLs into an explicit `IS NOT NULL` guard, NOT IN only the non-null values. Regression tests added. - query/_stream.py: `__contains__` ignored `self._query`, so run-scoped membership checked at workspace level (an item under a different run in the same workspace reported as `in`). Scope it like the other dunders. - query/mongodb.py: streaming `_execute_iterate` swallowed mid-stream cursor failures and silently truncated — re-raise (matches sqlite's convention) so a truncated stream is distinguishable from a clean end. - celery.py: extractor `ctx` was rebuilt field-by-field, dropping run-scoping keys (parent_scope) — copy runner.context and overlay the extractor-specific values. - output_types/_base.py: log (debug) when load_output_types skips an unknown `_type` instead of dropping it with no trace. Stale/false-positive findings verified and left unchanged: is_output_type gate and loader mongo-registration (already fixed earlier in the stack), scan_chunk_id coercion (scans never chunk), and native-ID-type preservation (scope ids are stored as strings by design, so str() matches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
…n info
runner.warnings (self._view('warning')) and runner.errors are lazy, store-backed
StreamViews; storing them directly in the report's `info` dict left a one-shot,
non-JSON-serializable object where a concrete list is expected. Wrap both in list().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtTyzcUmPYxM5nfp7MnMVd
🤖 I have created a release *beep* *boop* --- ## [0.41.0](v0.40.1...v0.41.0) (2026-07-24) ### Features * **query:** add --save option to secator q ([#1303](#1303)) ([37c156d](37c156d)) * **runners:** store-backed results, JSON driver, streaming fan-in ([#1312](#1312)) ([93638a1](93638a1)) ### Bug Fixes * **grype:** update parsing for v0.115.0 output ([#1313](#1313)) ([6f6c64f](6f6c64f)) * harden Redis connection resilience (broker + result backend) ([#1317](#1317)) ([158427d](158427d)) * **query:** allow empty query in secator q ([#1302](#1302)) ([bf2fd8f](bf2fd8f)) * **target validation:** ignore all invalid targets, not just one ([#1301](#1301)) ([eae2ced](eae2ced)) * **tasks:** validate bbot presets and wpprobe mode on the construction path ([#1309](#1309)) ([532c88b](532c88b)) ### Performance Improvements * **mongodb:** index-seek untagged findings in tag_duplicates (was 12s O(workspace) scan) ([#1316](#1316)) ([2504e2b](2504e2b)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
…1329) Fixes #1328. ## Problem Since #1312 (store-backed extractors), a scan that filters targets into a workflow **and** a task inside that workflow that also filters targets leaves the task with **no inputs**: ```yaml # scan type: scan workflows: wf1: wf2: targets_: { type: target, field: name } # scan-level filter into wf2 ``` ```yaml # workflow wf2 type: workflow tasks: mytask: targets_: { type: target, field: name } # workflow-level filter into mytask ``` `mytask` receives no targets, though it should get the same targets passed to `wf2`. Worked before #1312. ## Root cause (traced end-to-end) A workflow with a scan-level `targets_` gets a `parent_scope`. Its `mark_runner_started` re-resolves the workflow's own `targets_` to **emit scope-tagged `Target` findings** that its tasks then consume (`secator/celery.py`). But `build_extractor_query` unconditionally added `_context.scope == parent_scope` to that query — so the **producing** pass searched for the exact scope it was about to create, matched nothing, and emitted **zero** scope-tagged targets. Every task that then filters by scope resolved to an empty set. Debug trace on `main`: ``` # wf2 emit (producer) -> query {_type:target, scan_id, scope:wf2} -> 0 results -> 0 emitted # mytask (consumer) -> query {_type:target, scan_id, scope:wf2} -> 0 results -> no inputs ``` ## Fix The producing pass sets `scope_producer` in its extractor context; `build_extractor_query` skips the self-referential scope filter when set, so it queries the **upstream** targets and emits them tagged. Consumer (task) extractors are unchanged and still filter by scope — so a scan-level **subset** filter is still honored. Two-line change (+ comments) in `secator/celery.py` and `secator/runners/_helpers.py`. ## Tests `tests/unit/test_target_filter_scope.py` runs a real scan → workflow → task (sync) with a mock task and asserts the task's resolved inputs: - `test_identity_filter_passes_all_targets` — the #1328 case - `test_scan_level_subset_filter_is_honored` — a scan-level subset filter still reaches the nested task exactly (guards the consumer-side scoping) Both **fail on `main`, pass with the fix**. Full unit suite: identical failure set before/after (the pre-existing environment-dependent failures — network/tool/config-dir — are unchanged; the fix adds `+2` passing). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved target resolution for nested workflows and scope-based filtering. * Workflow-level and task-level filters now correctly preserve matching targets. * Fixed upstream target handling for scope-producing workflow steps. * **Tests** * Added regression coverage for target filtering across scans, workflows, and tasks. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- ## [0.42.0](v0.41.0...v0.42.0) (2026-08-18) ### Features * **kev:** bundle a local CISA KEV mirror as an offline fallback ([#1331](#1331)) ([4f1ff5c](4f1ff5c)) * **output-types:** tag known-exploited vulnerabilities with `kev` ([#1319](#1319)) ([#1321](#1321)) ([fc1dcaf](fc1dcaf)) * **security:** non-interactive sudo password for headless workers ([#1335](#1335)) ([8aa0031](8aa0031)) ### Bug Fixes * **command:** don't crash on sudo prompt when TTY detection is wrong ([#1332](#1332)) ([#1333](#1333)) ([1ac6207](1ac6207)) * **command:** tty issue dumb terminals ([#1324](#1324)) ([f5af590](f5af590)) * **config:** allow unsetting int/float config keys ([#1320](#1320)) ([4f380c0](4f380c0)) * **docker:** bump alpine runtime to 3.23 (Go 1.25.10) for tool installs ([#1330](#1330)) ([fa6c647](fa6c647)) * **lint,test:** cli.py lint + deterministic empty-arg query test ([#1327](#1327)) ([7d79d91](7d79d91)) * **query:** allow empty ARG when a filter option is provided ([#1211](#1211)) ([6206b14](6206b14)) * **runners:** nested target filter dropping task inputs since [#1312](#1312) ([#1329](#1329)) ([9929ac3](9929ac3)) * **tasks:** force system OpenSSL in testssl to fix missing libproviders.so in Docker ([#1134](#1134)) ([b528cfb](b528cfb)) * **wpscan:** don't leak wpscan's version status into the finding status ([#1326](#1326)) ([428557b](428557b)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Reworks how runner results are stored and read. Instead of carrying a runner's findings between Celery tasks in memory (the "chain payload"), findings live only in a store and every consumer streams them back through a run-scoped
QueryEngineview. This is now the single, self-contained PR for the whole result-store change — it subsumes and supersedes the earlier stack:report.jsonrewriter to an append-only NDJSON store.Both are closed in favour of this PR.
The model
add_resultfans each finding out to the active store driver(s) viaon_item. Local runs default to the json/NDJSON driver (no backend needed);sqliteandmongodbare the DB stores.runner.results/findings/errors/ … are lazyStreamViews over the store, scoped by the runner's{type}_id(minted once inRunner.__init__, inherited via context). Nothing is held in memory; a run's peak memory is its working set, not its result count.Key changes
results.ndjson(O(1) per finding) instead of rewriting a wholereport.jsonper finding (the old O(N²) hot path).report.jsonkeeps info only; the query backend reads NDJSON last-wins with a legacyreport.jsonfallback. orjson on the read/write paths.self._results/context['results']);report infoand read-model source errors/warnings from_type=error/warningfindings; droppederrors/warningsfromRunner.toDict().report_dir/{type}_id, never the whole workspace (no cross-run leak, no whole-workspace scans mid-run).findings_count/errorsin memory so status never re-queries the store.on_intervalbackend throttle —last_updated_dbwas initialized toNoneand never stamped, soshould_update()was always true and the runner's summary row was rewritten to the backend once per finding (1M redundant writes at 1M findings — the cause of the sqlite driver's super-linear DNF at scale). Now stamped, so it honorsbackend_update_frequency(5s). Helps every driver.Results (memory + runtime)
Synthetic fan-in workflow, peak RSS + wall time, per driver:
Full matrix + charts: benchmark artifact (attached separately).
Testing
Known follow-ups (not in this PR)
runner.errorsreads (rescan / auto-FIXED gate + health chore) to the findings query before this deploys (secator-ui already migrated).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
report.json(info) plus append-onlyresults.ndjsonfor findings.Bug Fixes
Changes
-o json/ auto-enabled for local runs).