Hunt-page ruleset tracking: favorites, rule counts, hunt provenance - #321
Hunt-page ruleset tracking: favorites, rule counts, hunt provenance#321vhmartinezm wants to merge 8 commits into
Conversation
…nance New fields parsed on existing resources (all additive; an older server leaves them None): - YaraRuleset: favorite, favorited_at, rule_count (None means the server had no answer, distinct from 0), historical_hunt_count, and new_results_count (only when the list is asked to include counts). - HistoricalHunt: rule_id (the source ruleset), rule_modified (freeze-time audit value), and source_rule_changed — a tri-state answering "has the source ruleset's body changed since the hunt froze it?" (None = unknown, not 'unchanged'). New endpoints and filters: - ruleset_favorite(id, favorite): idempotent star/unstar; the response carries favorites_used/favorites_limit; over-budget refusals surface a machine-readable FAVORITE_LIMIT error. - ruleset_list(name=, status=, favorites_only=, has_new_results=, since=, include_counts=): the hunt-page filters, conjunctive and optional. - live_results_count(since=): per-live-hunt result counts in a window, one aggregate for every 'new results' badge. - live_feed(livescan_id=): scope the feed to one live hunt. Sync and asyncio clients both. The rules live-suite tests now create a uid-namespaced single-rule ruleset (deterministic rule_count, no name collisions on the shared stack) and exercise the favorite round-trip, provenance, counter increment, and the changed-since-freeze flip; their cassettes are removed to re-record against a stack that serves the new fields.
The sync api.py was hand-edited; scripts/regenerate_sync.py places live_results_count in aio's order and applies ruff's formatting, which is what the unasync-mirror CI gate diffs against. The rules live-tests' three read-after-write assertions (the counter, and both sides of the changed-since-freeze flip) now poll: those GETs read the replica, and on a real-replica stack the stale read of the flip is a silent False. Sleeps are free on VCR replay.
Ids exceed JavaScript's safe-integer range; the counts entries carry the same digit string YaraRuleset.livescan_id does.
Recorded against the branch server image (both tests green live first); the offline suite replays them — 163 passed with no stack.
|
Reviewed against
Nothing under
No cassette in
Per
Related, minor:
|
Review findings, all four: - specs updated in the same PR as required: 03-endpoints gains ruleset_favorite / live_results_count rows, the real ruleset_list signature and live_feed's livescan_id; 02-resources catalogues both new resources — including why YaraRulesetFavorite empties RESOURCE_ID_KEYS (the server reads the toggle from the PUT body; the empty key list is the only thing routing id there) and the bool→int body serialisation; 05's commonly-imported list carries both. - the rules live-tests now exercise every previously-uncovered surface against the real stack (and the cassettes record it): live_start → status=active filter → include_counts observed as a computed 0 (distinct from null) → live_results_count (our zero-result hunt ABSENT from counts, keyed by the same digit strings ruleset_get renders) → the livescan_id-scoped feed → live_stop, with the stop in a finally because a running hunt blocks ruleset deletion. - pure-unit builder tests (hunt_tracking_builder_test.py, the known_good_test pattern) pin the request shapes: the favorite PUT's body routing incl. 1/0 bools, counts query routing + None omission, the list filters' int bools and byte-compatible no-filter request, and livescan_id stringification. - the unstar stays a contract assertion with slot hygiene documented: ruleset_delete soft-deletes and the budget counts only deleted=false rows, so a failed run's star frees itself with the rule. The limit pin vs used bound is now commented as deliberate. Also: test/eicar.yara deleted (no test references it since the uid_yara move; the helper docstring no longer names the file).
|
All four addressed in 40bc463: specs 02/03/05 updated in-PR (including the RESOURCE_ID_KEYS=[] rationale and the 1/0 body bools); the live tests now exercise every flagged surface against the real stack and the cassettes record it — live_start → status=active → include_counts observed as a computed 0 → live_results_count (zero-result hunt absent, digit-string keys matching ruleset_get) → livescan_id feed → live_stop-in-finally; pure-unit builder tests added (hunt_tracking_builder_test.py, the known_good pattern); the unstar/slot question is answered in a comment (ruleset_delete soft-deletes and the budget counts deleted=false only, so a failed run self-heals) with the limit-pin-vs-used-bound distinction made explicit; test/eicar.yara deleted. |
|
Reviewed against 1. 2.
The last commit correctly notes nothing references 3. The 4. 5. (minor) |
…ew 2)
All five follow-ups:
- live_results_count moved to the _single Live-hunts table in
specs/03 — it returns one resource, and _single-vs-_paginate is that
document's organizing invariant.
- specs/04's fixture inventory drops the retired test/eicar.yara and
names the new pure-unit module.
- Parse-side pins for the counts resource: the cassettes only carry
EMPTY counts (fresh zero-result hunt), so the {livescan_id, count}
entry shape, the digit-string join key and the null-counts coalesce
now have canned-payload tests.
- The livescan_id feed assertions no longer read as if they verify the
scoping: with a zero-result hunt they pin the wire shape and the
empty pass-through only, and the comments now say so (the scoping
semantics are pinned by the server's own HTTP suite).
- FAVORITE_LIMIT's machine-readable contract is now documented
(specs/05: no typed exception by design; the path is
exc.request.errors with the code plus the same counters a successful
toggle returns) and pinned by a respx refusal test — mocked because a
genuinely full budget on the shared stack would race every other run.
|
All five addressed in b80f825: the counts row moved to the _single Live-hunts table (it returns one resource); specs/04's inventory drops the retired fixture and names the new module; parse-side pins added for the counts entry shape, the digit-string join key and the null-counts coalesce (canned payloads — the cassettes only carry empty counts by construction); the feed assertions' comments now say exactly what they pin (wire shape + empty pass-through — the scoping semantics are pinned by the server's own suite); and FAVORITE_LIMIT's machine-readable contract is documented in specs/05 (no typed exception by design; the path is exc.request.errors with the code plus the same counters a successful toggle returns) and pinned by a respx refusal test, mocked because a genuinely full budget on the shared stack would race every other run. |
|
Reviewed against Three things worth acting on, all in tests/docs. 1. The favorite round-trip is not actually pinned on VCR replay. Star and unstar are both 2. 3. Latent: the empty Minor / no action needed: |
|
Review Src side is clean: Four things worth action. 1. Every recorded
Both are cheap to add inside the existing running-hunt block, and one is an assertable negative: the hunt has zero results, so 2.
3. specs/04 invariant 7: "Prefer the pure-unit tier for builder + parse logic … Use this tier for any bug that can be reproduced without network involvement." This test asserts exactly one thing — a 400 envelope populates If it stays a respx body, invariant 5 requires a new off-harness respx body to "say why in its docstring." The comment argues respx-over-e2e (fair — you cannot hold five team slots on a shared stack) but not why it is sync-only rather than on 4. Ticket ID in the branch name.
Minor. |
…ts died at get_sources)
ReviewChecked against Verdict: the src-side change is clean. All four touched specs are updated in the same PR, the resource/builder plumbing is right ( 1.
|
TL;DR
SDK support for the hunt-page ruleset tracking the internal artifact API now serves: favorites with a server-owned budget, ruleset-list filters, per-live-hunt result counts, a per-hunt feed scope, and source-rule provenance on historical hunts. Sync and asyncio clients both; all field parsing is additive (an older server leaves the new attributes
None).Requires
releasepublishes:latest), then this PR todevelop.What's new
Resources:
YaraRuleset:favorite,favorited_at,rule_count(Nonemeans the server had no answer — distinct from 0),historical_hunt_count,new_results_count(only when the list was asked to include counts).HistoricalHunt:rule_id(the source ruleset),rule_modified(freeze-time audit value),source_rule_changed— tri-state: has the source ruleset's body changed since the hunt froze it?None= unknown, not "unchanged".YaraRulesetFavorite(the toggle's response: star state +favorites_used/favorites_limit) andLiveHuntResultCounts(countsof{livescan_id, count}; a hunt absent fromcountscollected 0;livescan_idis the same digit stringYaraRuleset.livescan_idnow carries — the join key; ids exceed JavaScript's safe-integer range, so they are never bare JSON ints).Methods (sync + asyncio):
ruleset_favorite(id, favorite)— idempotent star/unstar; over-budget refusals carry a machine-readableFAVORITE_LIMITerror.ruleset_list(name=, status=, favorites_only=, has_new_results=, since=, include_counts=).live_results_count(since=).live_feed(livescan_id=).Tests
The two rules live-tests now build a uid-namespaced single-rule ruleset (unique name on the shared stack, deterministic
rule_count) and exercise the favorite round-trip, the name/favorites filters, hunt provenance, the counter increment, and the changed-since-freeze flip. Read-after-write assertions poll (replica-lag tolerant; sleeps are free on VCR replay). Cassettes re-recorded against a live stack. The sync client is regenerated viascripts/regenerate_sync.py.