feat: twelve research-anchored techniques (REP-012..REP-024, Tier A) - #5
Merged
Conversation
Takes the catalog from 11 to 23 entries. Every entry is anchored to a
peer-reviewed paper with measured results; the anchors and the feasibility
analysis are in docs/technique-catalog-expansion-research*.md.
New techniques, each chosen because it exercises a detection no existing
entry exercises:
REP-012 jittered and fleet-aggregate C2 callback BAYWATCH DSN 2016,
UVA ACSAC 2023. REP-001 is fixed-interval and trips any
periodicity test; this widens jitter and spreads a rare callback
across a fleet so the period only exists in the aggregate.
REP-013 self-propagating spread PORTFILER IEEE CNS 2021.
REP-003 is one source sweeping many hosts; this grows the
distinct-source count geometrically per generation.
REP-014 cryptomining pool session MineShark NDSS 2025,
MineHunter ACSAC 2021. Long-lived, low-rate, ~1:1 byte ratio.
REP-015 low-throughput DNS exfiltration Nadler et al. 2019.
Queries per HOUR, deliberately under the thresholds REP-004 trips.
REP-017 encrypted DNS (DoH) bypass CIRA-CIC-DoHBrw-2020.
The signal is an absence: resolver traffic stops as TLS to a DoH
resolver starts.
REP-018 lateral movement login chain Hopper USENIX Sec 2021.
A path with a mid-path credential switch, not a volume of failures.
REP-019 stealth scan below rate threshold TRW IEEE S&P 2004.
The negative control for REP-002/003.
REP-020 first contact, newly registered domain PREDATOR CCS 2016.
Organization-wide domain novelty, unlike REP-008's per-host IP.
REP-021 inbound perimeter scan reception IMC 2024.
The direction a real perimeter mostly logs, and the false-positive
source for outbound scan rules.
REP-022 multi-stage IDS alert chain Wilkens KCSM, ALERTPRO,
AACT. REP-009 is a rate spike with no ordering; this is ordered
across a held entity pair and buried in alert noise.
REP-023 TLS 1.3 C2 with flow-only signal RAID 2024.
Emits no handshake metadata at all, so a JA3-dependent rule gets
nothing. Low byte variance is the only signal left.
REP-024 internal host as proxy relay NDSS residential proxy.
Paired inbound/outbound legs with correlated byte volumes.
Benign look-alikes are treated as a correctness requirement, not decoration.
Bilot et al. (USENIX Sec 2025) found eight state-of-the-art provenance IDS
not deployment-ready despite near-perfect published results; a plan that
emits only the malicious pattern lets any rule score perfectly. So REP-012
ships a benign periodic destination, REP-014 a bursty long session, REP-015
a same-volume low-cardinality parent, REP-018 an admin star, REP-023
high-variance browsing, and REP-024 a sanctioned proxy with an identical
pattern.
Three questions the research docs flagged as unverified are now settled by
reading the code rather than assuming:
1. One plan CAN span two render paths. render() dispatches per record on
(log_type, subtype), so REP-017 and REP-018 needed no design change.
2. Inbound orientation works. _traffic_forward already reads src_intf and
dst_intf from extra, so REP-021 reverses the interface pair with no
profile change. Verified in emitted CEF.
3. No new render path was required for any of the twelve.
Also:
- entities: adds a scanner_external pool on 192.0.2.0/24, the one IANA
documentation range no other pool used. REP-021's source ceiling is 508
addresses, which is a safety constraint and not a tuning limit: the IMC
study observed 465,251 unique scanners and this cannot represent that.
When the ceiling binds the run summary says "capped" rather than silently
emitting fewer sources, and a test asserts no shipped preset hits it.
- REP-022's "stage" key is engine-internal and deliberately never rendered.
Real FortiOS has no such field, and emitting it would hand the answer to
the detection under test. A test proves the chain is still recoverable
from rendered CEF via attack-name order and ascending severity.
- REP-016 is intentionally absent. It needs a dns:dns-response path that
does not exist yet, and a DGA entry with no NXDOMAIN in it would be worse
than no entry. Asking for it raises NotImplementedError.
Tests: 427 pass, up from 325. 101 new, covering each technique's
distinguishing property plus cross-cutting determinism, time ordering,
three-vendor render parity, and synthetic-address-only assertions. The
existing max_events safety test caught a missing cap in REP-018 and a
degenerate spread factor in REP-013 (fanout//6 == 1 at the low preset gave
no growth at all, making it a slow REP-003); both fixed. ruff, black and
mypy clean.
404SecNotFound
changed the base branch from
feature/v0.2.0-catalog-expansion
to
main
July 25, 2026 20:27
404SecNotFound
added a commit
that referenced
this pull request
Sep 1, 2026
…ment boundary (roadmap #3) (#91) * feat: destination-conditional synthetic marker + manifest attestation + deployment boundary Roadmap 2026-09 item 3 (folds in #1 attestation and #5 deployment boundary). The single control that keeps Replicant from being banned the first time it burns a shift. - Marker default is now destination-conditional (Orchestrator._resolve_marker): ON for a non-loopback send, where analyst de-confliction on a shared collector outranks a flex slot no detection reads; OFF for --to-file and loopback, where the golden line is the oracle. flexString1 is unused by all three vendor profiles, so marking a live send corrupts no field a rule keys on. - --mark-synthetic forces it on everywhere; new --no-marker forces it off and logs a warning when it overrides a non-loopback send (settings.no_marker, which wins over benign_marker). - Manifest records the decision in a new marker_attestation field (RunManifest and ScenarioManifest), so a run's marking is auditable after the fact. - Applied uniformly through the existing _mark choke point, so run, scenario, and the connect test all follow the same rule. - docs/deployment-boundary.md states the 'detection lab, not production SIEM' boundary and the authorization workflow (marker on + manifest as the record). Positive control: the six new tests in test_synthetic_marker.py were run against the unfixed code and observed to fail (the end-to-end one shows lines captured but unmarked); they pass after the change. 1057 tests, gates clean. * review: close 5 findings on the conditional marker From /code-review on PR #91, verified against the code before fixing: 1. (main) Web /api/connect/test sent an UNMARKED line to a non-loopback collector while the CLI send_test marked it: the recurring 'fixed one send path, not the parallel one' class. server.connect_test now resolves the marker per destination like the CLI. Positive control: reverted the line, web guard went red, restored it green. 2. Scenario sends carried the literal flexString1=synthetic with no run id to trace them. run_scenario now generates a run_id, threads it into the emit path, and records it in ScenarioManifest (new run_id field), so a marked scenario line traces back to its manifest. 3. marker_attestation claimed a '--no-marker override' even on loopback/file where the default was already off; now only a real override is reported so. 4. Docs said 'off for --to-file' flatly; a run that both sends live and writes a file marks both (the file mirrors the wire). Wording made precise across README, CLAUDE.md, deployment-boundary, settings, and the docstring. 5. The --no-marker warning read as if lines were emitted though it resolves at plan time; reworded as configuration intent. +3 guards (web probe marking, loopback attestation, scenario run-id marking). 1060 tests, black/ruff/mypy clean.
404SecNotFound
added a commit
that referenced
this pull request
Sep 1, 2026
…92) * feat: per-technique validation-transferability property (roadmap #5) Coverage honesty at catalog granularity: does a green result exercise the SHIPPED production rule, or only its parser? - Technique gains transferability (transfers | parser-only) and transferability_note. A model validator requires a note when parser-only: a parser-only claim with no reason tells an engineer nothing. Positive control: neutered the validator, the reject test went red, restored it green. - REP-011 (synthetic srccountry tag vs live GeoIP/ASN + IdP identity), REP-016 (DGA lexical/NXDOMAIN features transfer, but names sit under the reserved .invalid TLD), and REP-020 (org-wide first-contact novelty transfers, but the cited PREDATOR registration-age scoring needs WHOIS the reserved TLDs cannot carry) are labelled parser-only with specific reasons. The rest default to transfers, subject to the overall delivery-unverified gate. - REP-024 transfers but discloses the integer-second eventtime ceiling (the honest half of the rejected sub-second-eventtime proposal folds in here). - Surfaced where a CLI-first engineer looks: a Transfers column in 'replicant list' plus a parser-only notes footer, and in the web catalog JSON. 1067 tests, black/ruff/mypy clean. * review: surface every transferability note on CLI + render it in the web card From /code-review on PR #92, verified before fixing: 1. REP-024's disclosed-limit note (the eventtime ceiling) was surfaced on no CLI surface: the footer only printed parser-only entries, though the field docstring and test treat a disclosed-limit-on-a-transferring-technique as a supported case. 'replicant list' now collects any technique with a note and labels each (parser-only) or (disclosed limit). Positive control: gated the footer on parser-only again, the CLI guard went red, restored it green. 2. The web TechniqueDetail card rendered none of the new fields though the API returned them (the roadmap names that card as the transferability surface). Added transferability/transferability_note to the api.ts Technique interface and a note block to the card; three frontend tests cover parser-only, disclosed-limit, and clean-transfer (renders nothing). 3. The 'Transfers' column mixed answer types (yes / parser-only); now symmetric yes / no, with the category detail in the footer. Also fixed the Rich-markup trap: [disclosed limit] parsed as a style tag and vanished, so the label is parenthesised. 1067 python + 175 frontend tests; black/ruff/mypy/tsc clean.
404SecNotFound
added a commit
that referenced
this pull request
Sep 1, 2026
…map #7) (#94) * feat: per-run analyst validation card for single-technique runs (roadmap #7) After an ad-hoc 'replicant run', the analyst otherwise reverse-engineers the pivot entities, window, and expected rule from a raw JSON manifest, the manual toil the tool sells against. run() now writes a copy-pasteable card beside the manifest (<stem>.card.md) and RunResult.summary() names it. The card is deterministic, derived from the plan's own events, and authors no rule logic (blueprint boundary): its search is a hunt pivot that LOCATES the run's events, not a detection rule. - Ties to #3: a marked run's find-events search keys on flexString1=<run_id>, so the exact events are one search away; an unmarked (loopback/--to-file/--no-send) run pivots on the top src/dst and the emitted window instead. - Ties to #5: the card carries the transferability verdict and note, so it states what a green result does and does not prove, plus the loopback-only delivery caveat. - Names objective, ndr_rule/ndr_uc, held/varied CEF fields, pivot entities, and the emitted window (real under --pace plan). Positive control: dropped the write_validation_card call and the four behavioural card tests went red, restored them green. 1079 tests; gates clean. * review: card follows the compressed wire, survives a write failure, reuses DUBAI_TZ From /code-review on PR #94, verified before fixing: 1. (main) The card's window and count came from the uncompressed plan.events, so a --speed run's card claimed the raw timeline as 'real'. run() now builds the card from compress_timeline(plan.events, speed)[:count] - what actually went out - fixing both the --speed window and the overstated count on a graceful stop. New test exercises --speed (via --no-send so it does not wait the plan timeline); positive control observed red against plan.events, green. 2. The card build/write was outside the try/except, so a card-write OSError could fail a send that had already completed. Now wrapped: a secondary artifact logs a warning and leaves card_path None instead of raising. 3. Unmarked pivot query no longer leads with a dangling 'and' when a technique carries no src/dst (joined, not concatenated). 4. Reuse: the card imports DUBAI_TZ from audit.manifest instead of a fifth hard-coded timezone(timedelta(hours=4)). 1080 tests; black/ruff/mypy clean.
404SecNotFound
added a commit
that referenced
this pull request
Sep 1, 2026
…heel job (roadmap #14) (#96) * feat: CLI-first container image + robust CI technique count (roadmap #14) The pip-installable CLI already exists (console_scripts + [web] optional extra); this adds the container half and a robust build gate. - Dockerfile: a CLI-first image (python:3.12-slim, non-root, ENTRYPOINT replicant). Web UI deliberately excluded so a CLI evaluator does not build a React bundle to see one FortiGate line. .dockerignore keeps everything but the package out of the build context, including the in-tree backup git repos and the checked-in webui_dist. - CI 'container' job builds the image and drives the CLI inside it (the positive control the author cannot run locally: no Docker daemon on the dev Mac). A broken Dockerfile fails there rather than being merged on inspection. - README documents 'pip install replicant' / '[web]' and the container trial, noting the PyPI publish itself is a release step. Also fixes a regression this session introduced: the transferability-notes footer (item #5) made 'replicant list | grep -c REP-0' return 28, turning the wheel job red on main. Both the wheel and container checks now count DISTINCT technique ids (grep -oE 'REP-[0-9]{3}' | sort -u), which is what '24 techniques' means and is immune to the footer. * review: fix Linux bind-mount perms, make the no-assets guard real, correct docs From /code-review on PR #96, verified before fixing: 1. (main) The README's headline 'docker run -v ... run' example failed on native Linux: the non-root image (uid 10001) cannot write a host-owned bind mount. Now documents --user $(id -u):$(id -g), and a new CI step exercises exactly that bind-mounted run on the Linux runner (previously untested). 2/4. The Dockerfile/README claimed 'replicant web' serves its build-it page (it errors 'web dependencies missing', no fastapi) and called webui_dist 'checked-in' (it is gitignored). Both corrected. 3. The 'no web assets' CI guard could never fail: webui_dist is gitignored and the job never built it, so the image lacked it regardless of .dockerignore. The container job now builds the frontend first (like the wheel job), so the guard actually tests the .dockerignore exclusion; deleting that line now fails it. Finding 5 (the distinct-count one-liner duplicated across wheel and container jobs) left as-is: both correct, a shared composite action would over-engineer a one-liner.
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
PR 2 of 3. Stacked on #4 (docs), so review that first or read this diff alone; it is self-contained code.
Takes the catalog from 11 to 23 techniques. Every entry is anchored to a peer-reviewed paper with measured results.
What each one adds that nothing else covers
Benign look-alikes are a correctness requirement here
Bilot et al. (USENIX Sec 2025) found eight state-of-the-art provenance IDS not deployment-ready despite near-perfect published results. A plan that emits only the malicious pattern lets any rule score perfectly and teaches the operator nothing. So REP-012 ships a benign periodic destination, REP-014 a bursty long session, REP-015 a same-volume low-cardinality parent, REP-018 an admin star, REP-023 high-variance browsing, and REP-024 a sanctioned proxy with an identical pattern. Tests assert each control is present.
Three unverified questions from the docs, now settled by reading the code
render()dispatches per record on(log_type, subtype), so REP-017 and REP-018 needed no design change._traffic_forwardalready readssrc_intf/dst_intffromextra, so REP-021 reverses the interface pair with no profile change. Confirmed in emitted CEF:src=192.0.2.1 deviceInboundInterface=port1 ... deviceOutboundInterface=port2.Two bugs the existing safety tests caught
test_builder_never_exceeds_max_eventsruns every technique atmax_events=10. REP-018 had no cap and produced 25. Fixed.max(1, fanout // 6), which is 1 at the low preset, so the infected population never grew and the technique degenerated into a slow REP-003. Now floored at 2.Notable decisions
scanner_externalpool on192.0.2.0/24, the one IANA documentation range no pool used. REP-021's ceiling is 508 addresses. That is a safety constraint, not a tuning limit: the IMC study observed 465,251 unique scanners and this cannot represent that. When the ceiling binds, the run summary sayscappedrather than silently emitting fewer sources, and a test asserts no shipped preset hits it.REP-022's
stagekey is engine-internal and never rendered. Real FortiOS has no such field, and emitting it would hand the answer to the detection under test. A test proves the chain is still recoverable from rendered CEF via attack-name order and ascending severity. Verified by hand:TCP.Port.Scan→HTTP.Unix.Shell.IFS.RCE→Log4j RCE/PHPUnit RCE→Web.Server.Password.Files.Access→Generic.Web.Shell.Access, on one held pair, noise on other pairs.REP-016 is intentionally absent. It needs
dns:dns-response, and a DGA entry with no NXDOMAIN in it would be worse than no entry. Coming in PR 3.Verification
427 tests pass, up from 325. 101 new: each technique's distinguishing property, plus cross-cutting determinism, time ordering, three-vendor render parity, and synthetic-address-only assertions. ruff, black, mypy clean.
replicant listrenders all 23; end-to-end CEF spot-checked for REP-021 and REP-022.Note on CI
The
Shelljob is failing on the base branch too, on a docs-only diff that does not touchscripts/.mainwas green on 2026-07-23 andbash -n scripts/install.shpasses locally. It looks like the Docker Hub pull of the pinnedkoalaman/shellcheck:v0.11.0image, not a code regression. Being confirmed separately; flagging rather than merging past it silently.