Skip to content

feat: twelve research-anchored techniques (REP-012..REP-024, Tier A) - #5

Merged
404SecNotFound merged 1 commit into
mainfrom
feat/v0.2.0-tier-a
Jul 25, 2026
Merged

feat: twelve research-anchored techniques (REP-012..REP-024, Tier A)#5
404SecNotFound merged 1 commit into
mainfrom
feat/v0.2.0-tier-a

Conversation

@404SecNotFound

Copy link
Copy Markdown
Owner

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

New Anchor Distinct from
REP-012 jittered + fleet 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 exists only 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 MineShark NDSS 2025, MineHunter ACSAC 2021 Long-lived, low-rate, ~1:1 byte ratio. Neither a callback (many short sessions) nor exfil (large and asymmetric)
REP-015 low-throughput DNS exfil Nadler et al. 2019 REP-004 runs at 20-200 q/s. This runs at queries per hour, under the thresholds REP-004 trips
REP-017 DoH bypass CIRA-CIC-DoHBrw-2020 The signal is an absence: resolver traffic stops as TLS to a DoH resolver starts
REP-018 login chain Hopper USENIX Sec 2021 REP-007 is failure volume, REP-011 is one user in two countries. Neither models a path with a mid-path credential switch
REP-019 stealth scan TRW IEEE S&P 2004 Same shape as REP-002/003, parameterized to stay under thresholds. The negative control
REP-020 newly registered domain PREDATOR CCS 2016 REP-008 is novel IP per host. This is org-wide domain novelty
REP-021 inbound perimeter scan IMC 2024 Every existing scan technique is outbound. This is the direction a real perimeter mostly logs
REP-022 multi-stage IDS alert chain Wilkens KCSM, ALERTPRO, AACT REP-009 is a rate spike with no ordering. This is kill-chain ordered on a held pair, buried in noise
REP-023 TLS 1.3 flow-only C2 RAID 2024 Emits no handshake metadata at all, so a JA3-dependent rule gets nothing
REP-024 proxy relay NDSS residential proxy Models a host used as infrastructure, not as attacker or target

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

  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/dst_intf from extra, so REP-021 reverses the interface pair with no profile change. Confirmed in emitted CEF: src=192.0.2.1 deviceInboundInterface=port1 ... deviceOutboundInterface=port2.
  3. No new render path was needed for any of the twelve.

Two bugs the existing safety tests caught

  • test_builder_never_exceeds_max_events runs every technique at max_events=10. REP-018 had no cap and produced 25. Fixed.
  • REP-013's spread factor was 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_external pool on 192.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 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 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.ScanHTTP.Unix.Shell.IFS.RCELog4j RCE / PHPUnit RCEWeb.Server.Password.Files.AccessGeneric.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 list renders all 23; end-to-end CEF spot-checked for REP-021 and REP-022.

Note on CI

The Shell job is failing on the base branch too, on a docs-only diff that does not touch scripts/. main was green on 2026-07-23 and bash -n scripts/install.sh passes locally. It looks like the Docker Hub pull of the pinned koalaman/shellcheck:v0.11.0 image, not a code regression. Being confirmed separately; flagging rather than merging past it silently.

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
404SecNotFound deleted the branch main July 25, 2026 20:26
@404SecNotFound
404SecNotFound changed the base branch from feature/v0.2.0-catalog-expansion to main July 25, 2026 20:27
@404SecNotFound
404SecNotFound merged commit e9df96d into main Jul 25, 2026
16 checks passed
@404SecNotFound
404SecNotFound deleted the feat/v0.2.0-tier-a branch July 25, 2026 20:30
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant