Skip to content

Releases: 404SecNotFound/Replicant

v0.10.0

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 01 Sep 12:47
c129d62

v0.10.0: the 2026-09 roadmap, executed

13 buildable survivors of the five-persona roadmap (docs/roadmap-2026-09.md), across ten PRs (#89-#98), each code-reviewed with every new guard run red against the unfixed code first.

The keystone holds: every timing and delivery claim is loopback-only until the first observed rule fire. Posture stays "generates vendor-accurate CEF, detection-unverified."

Highlights: per-technique validation-transferability property; per-run analyst validation card (<manifest>.card.md); statistical fidelity suite; two structural false-positive foils (REP-006 shared-egress, REP-007 NAT source-collapse); CLI-first container image + pip docs; first reference detection spec (REP-001); destination-conditional synthetic marker + deployment boundary.

Behavior changes: the marker now defaults on for a non-loopback send (off for --to-file/loopback; --no-marker overrides, logged); packet counts vary per flow, so emitted values for a given seed differ from prior versions (same-seed reproducibility preserved within a version).

Not shipped (lab-gated): the operational pilot and the two items refining the unbuilt F2/F5 stay behind the launch gate, pending the LogRhythm lab test.

Full detail in CHANGELOG.md. Install the attached wheel: pip install ./replicant-0.10.0-py3-none-any.whl (add [web] for the UI), or build the container: docker build -t replicant .

Replicant v0.9.0 — end-to-end review fixes

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 01 Sep 04:46
36ad617

A defect release from an end-to-end review of the whole codebase (10 subsystem finders, every finding adversarially verified against the real code — 10 of 17 raised survived). Gates were green before and after.

Minor, not patch: one fix changes rendered output. REP-011 on Check Point now carries the source country it used to drop, so a capture of that technique on that vendor gains a field.

Behaved against its own contract

  • Scenario runs bypassed the F-08 send lock. run() held the one-sending-run-per-host lock; run_scenario() did not, so a scenario run sent unlocked and a concurrent replicant run could double the eps cap. Fixed, with a real second-process test.
  • Check Point dropped the geovelocity source country, blinding REP-011 on one vendor — a country-keyed rule never fired against Check Point logs, on the technique whose whole signal is the changing country. Now rendered on all three vendors (golden lines unchanged).
  • A malformed --duration crashed with a raw traceback (and tore down the Rich menu). Now a clean refusal at the model boundary, so CLI, menu and web all reject it the same way.

Robustness and web hardening

  • high_entropy_labels hung forever on an impossible request (reachable via param overrides). Now refuses.
  • TLS certificate verification, the secure default, was never tested — flipping it to CERT_NONE would have shipped green. Now guarded, with a positive control.
  • A header-token API poller minted a session per request, growing tens of thousands of dead sessions in 12h. Only the browser's URL-token navigation mints a cookie now.
  • The run-event stream fan-out had no lock, so a tab subscribing at the instant of a publish could miss the terminal event. Now serialized.
  • The Docs link sanitizer allowed protocol-relative URLs ([x](//host)). Now rejected.

The method

Nine of the ten guards were run against the unfixed code and observed to fail before being trusted. The tenth, the stream race, has a window that cannot be forced from Python; its guard is a concurrency load test plus mutual exclusion by construction, and is labelled as such rather than dressed up as a forced-red control.

Verified

992 Python + 172 frontend tests, black / ruff / mypy / tsc clean, 11 CI checks including four installer containers and a real systemd unit. The wheel attached here was built from a clean checkout of this tag and smoke-tested in an empty virtualenv outside the repository.

Unchanged and still true: every timing and delivery claim in this project is loopback only. Replicant has not been tested against a live LogRhythm collector.

Replicant v0.8.0 — security review closed, Node 18 dropped

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 31 Aug 02:11
6982340

The 2026-08 security review is fully closed, and one supported platform is dropped. Read the second half before upgrading if you build the web UI on Debian or Ubuntu.

One sending run per host, enforced (F-08)

The events-per-second cap is applied by a single process's emit loop, so two Replicant processes sending at once delivered twice the cap to your collector, and neither was doing anything wrong. Safety rule 4 exists to protect that collector; a cap any second invocation silently doubles is not protecting it.

A second sending run is now refused, and the refusal names the holding pid — "another process" is not actionable, and the holder is usually a forgotten web UI in another terminal. --no-send and --to-file never acquire the slot, because they cannot reach a collector and so cannot exceed anything.

A host-level lease keyed on collector destination was the alternative and was declined: leases expire, clocks drift, and a killed process orphans one. flock is released by the kernel on exit, kill -9 included, so there is no stale lock to clean up. It is a smaller promise, and unlike a lease it is one the code can keep.

What it does not cover is stated rather than implied: two hosts pointed at one collector are still two caps. Nothing on a single machine can see that.

Node 18 dropped, advisories cleared (F-14)

npm audit went from 6 advisories, one critical, to 0. vite 5 to 8, vitest 2 to 4. Node 18 left maintenance in April 2025, so the floor the installer declared was already behind the platform it named.

The cost is real, and here it is: Debian 12 and Ubuntu 24.04 ship Node 18 and can no longer build the web UI from their own repositories. Use --no-web for a CLI-only install, or install Node 20+ from NodeSource first. The CLI is unaffected on both. CI now covers debian:12 with --no-web and adds debian:13, which ships Node 20, for the full install.

jsdom is pinned to 26 rather than 30 deliberately: 30 requires Node 22.22+, above the floor this release declares, and a test-only dependency should not choose the supported platform.

The eighth golden line was never compared

All three vendor golden tests assert the reference contains eight golden lines, assert the fixtures match that count, and then byte-compare seven. The eighth line of FortiGate, PAN-OS and Check Point had never been compared to anything.

The lines were correct, so this was a coverage hole rather than a live defect. It is still worth stating plainly: this project's correctness story is the golden oracle, and the oracle was proving seven eighths of what its own test names claimed.

Smaller

  • REP-018 carries TA0005 Defense Evasion alongside TA0008. T1078 and T1550 are both Defense Evasion, and using valid accounts and alternate authentication material to move laterally is the evasion.
  • The test suite no longer reads and writes your real ~/.config/replicant. It had no conftest.py, so it was touching the saved collector profile and the persistent web token.
  • An external 58-to-62 person-week feature roadmap was triaged; roughly 13 were adopted. Reasons for each rejection are in docs/10x-roadmap-triage.md so they can be argued with rather than rediscovered.

Verified

975 Python and 170 frontend tests, black / ruff / mypy / tsc clean, 11 CI checks including four installer containers and a real systemd unit. The wheel attached here was built from a clean checkout of this tag and smoke-tested in an empty virtualenv outside the repository.

Unchanged and still true: every timing and delivery claim in this project is loopback only. Replicant has not been tested against a live LogRhythm collector.

Replicant v0.7.0 — catalog defect fixes

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 30 Aug 10:53
79db2dc

A defect release. Unlike v0.6.0, this one does change the emitted telemetry: plans for REP-002, REP-013, REP-014, REP-016 and REP-024 differ from every prior release at the same seed. Determinism is intact and no golden CEF line changed, but if you have tuned a detection against any of those five, read on before upgrading.

It also carries the v0.6.1 web UI fix, so there is no reason to install that tag separately.

Three techniques emitted telemetry their own catalog text denied

This is the class of defect that matters most here. Replicant's whole claim is that the telemetry is what the catalog says it is, so a detection validated against the wrong signal does not fail. It passes, and tells you nothing.

  • REP-024's relay lag was a constant one second at every preset. A floor-divide collapsed the shipped millisecond ranges to zero and a clamp made them one, so the technique whose stated purpose is defeating naive fixed-window timing correlation emitted precisely the fixed window it exists to defeat. Its benign foil used a fixed one-second offset on top of that. The lag is now drawn in float seconds with the sub-second remainder dithered onto the integer-second timeline, so the drawn variance survives with its expected value intact. Measured after the fix: gaps of 1, 2 and 3 seconds where only 1 existed.
  • REP-002 ignored its own window_s preset and --duration entirely. The builder read the port count and the inter-probe gap and nothing else, so the low preset finished in about six seconds against a declared 120-second window, roughly twenty times more aggressive than documented, and a one-hour duration override returned a byte-identical plan. Probes now spread across the window. Measured: spans of 119, 59 and 29 seconds against declared windows of 120, 60 and 30, and --duration 12s yields 20 probes across 11 seconds.
  • REP-014's benign foil was trivially separable on the technique's own primary feature. The catalog makes long session duration the property the miner and its foil share, and byte burstiness the thing that separates them. The foil topped out at a few minutes against a twelve-hour miner session, so a duration threshold alone scored perfectly, which is the shortcut the foil exists to deny. Measured after the fix: the foil spans 0.92 of one pool session at every preset, against 0.37, 0.046 and 0.010 before.

Two ATT&CK mappings that would mislead a detection engineer

  • REP-007 mapped T1110.004 Credential Stuffing, which MITRE defines as replaying breached credential pairs. Nothing in the builder models that. One user against many attempts is T1110.001 Password Guessing.
  • REP-012 mapped T1029 Scheduled Transfer, an Exfiltration technique, on a C2 beacon entry listing neither that tactic nor any transfer behavior. Wrong technique and wrong tactic; T1071 already carries the periodicity.

Three tactic lists also claimed a tactic none of their own techniques carry, and are corrected: REP-009, REP-017 and REP-018.

Smaller defects of the same family

REP-016's benign NXDOMAIN trickle ran past the window it is meant to blend into. REP-013's baseline servers could be drawn from the worm's own seed set, so a control could grow like the worm. Jitter above 100 percent produced negative intervals and backward timestamps. DNS labels were not clamped to the RFC 1035 63-octet limit. Catalog text had drifted from code in six places, and the README showed a different ATT&CK mapping for REP-007 than the catalog and CLI did.

The finding behind the findings

None of this was caught by 952 passing tests, because no test asserted that the code does what the catalog text promises. Every defect above sat in the gap between a documented distribution and the one actually emitted. Fourteen tests now close it, including one that fails CI if the README and the catalog disagree on any of the 24 entries.

Two conventions came out of it. The seed is part of a guard: a regression test whose seed happens to avoid the defect has never failed and is worth nothing, which is exactly what one of the incoming tests turned out to be. And a benign foil is a correctness requirement of the technique, not a decoration: two of the three behavioral defects were foils a detection could separate for free, on the very feature the foil exists to make indistinguishable. A foil that is trivially separable is worse than no foil, because it reports as coverage.

Also in this release, from v0.6.1

Every technique now draws its own signal path diagram in the web UI. The diagram previously mapped only REP-001 through REP-011 and silently fell back to the periodic-beacon glyph for everything newer, so a DGA cluster or an inbound perimeter scan rendered as a fixed-interval beacon with full confidence. Eight new glyphs, six honest reuses whose captions state their own story, and no fallback: an unmapped id renders a labelled gap, and a test reading the real catalog fails on any technique added without deciding its diagram.

Known behavior change

replicant run REP-002 --intensity low now occupies its full 120 seconds instead of finishing in about six. That is what the preset always declared.

Verified

968 Python tests and 170 frontend tests, black, ruff, mypy and tsc clean. Every new guard was run against the unfixed code and observed to fail before being trusted. The wheel attached here was built from a clean checkout of this tag and smoke-tested in an empty virtualenv outside the repository: version, catalog, a real FortiGate and Check Point CEF render, and the packaged UI, fonts and license texts all resolving from site-packages.

The credentials for every claim about timing and delivery remain loopback only. Replicant has still not been tested against a live LogRhythm collector.

Replicant v0.6.0 — the Factory web UI

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 30 Aug 06:11
ae0b930

A visual identity release. Nothing in the emitted telemetry moves: no new techniques, no CEF changes, no CLI changes. Upgrading matters only if you use the web UI.

The web UI is the Factory system, and dark-only

The UI now follows a "terminal war room at midnight" design system, implemented from three approved mocks that went through a builder/three-critic loop. The design contract is docs/webui-factory-design.md.

  • Fonts: Geist 400/500 and JetBrains Mono 400, both OFL 1.1, self-hosted with license texts beside the woff2 files (they ship inside the wheel). The mocks specified Switzer, but the ITF Free Font License v2.0 prohibits distributing the font through a repository or publicly accessible server, which is exactly what this repo and its releases are, so it was replaced before anything was committed.
  • Palette: #101010 canvas, warm graphite surfaces, hairline borders, and exactly two chromatic colors reserved for live data: signal orange and metric green. Weight 400 everywhere. No gradients, shadows, or glows. A test asserts every documented palette token encodes exactly the hex named beside it.
  • Light mode is removed. The Factory system is dark-only; the toggle, stored preference, and pre-paint script go with it.
  • The run panel is a war-room frame: metric tiles derived from the run's own counters, an instrumented sparkline with a labeled scale, dotted mean, and time axis, and a progress track. The mock's bytes tile deliberately does not ship: the stream carries no byte counter, and a readout that cannot be measured must not render. Labels say emitted, never sent or delivered, where only rendering is measured.
  • Honesty carried into color: "sent, unconfirmed" renders neutral rather than colored, active filter chips recess instead of taking the signal color, and emphasis inside log lines is brightness only.
  • A generated near-black circuit backdrop (18 KB WebP, provenance in NOTICE) sits behind the canvas under a flat scrim; cards are opaque, so text contrast is unchanged.

Fixed

  • Stock tailwind-merge classified the type-scale rungs as text color classes and silently deleted the font size whenever a real color followed in the same cn() call; elements then inherited their parent's size (the vendor segmented control rendered 16px against a class list saying 12px). cn() now registers the rungs, and a test pins every rung the config declares.
  • The Logs tab's info level styled itself with a token that does not exist, so its color was inherited by accident.

Verified

952 Python + 142 frontend tests, black/ruff/mypy/tsc clean, a contrast-and-size audit on the rendered page (zero AA failures across 277 text elements), no horizontal overflow at 1280 or 375, and a live run watched end to end through the new frame. The wheel attached here was built from a clean checkout of this tag and smoke-tested outside the repository.

Replicant v0.5.2

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 04 Aug 06:03
815f951

A readability release. No behaviour changes, no new techniques, and nothing in the emitted
telemetry moves. Worth upgrading only if you look at the web UI.

More than half the interface rendered below 12px

Reported plainly as "the text is quite small, you have to really focus your eyes". Measured on the
rendered page rather than argued about.

most common size in the whole UI 10.5px (30 of 104 elements)
below 12px 58 of 104
at or above the 16px browser default 3 of 104
smallest text on screen 8.5px, a rule id inside the signal-path diagram
contrast failures 1

Contrast was fine. It was audited during the reskin and that audit held. Size was never audited at
all, and that turned out to be the real gap.

It was also not a design decision. The design doc defines a palette, spacing and a motif and
contains no type scale whatsoever, while the code carried eleven hardcoded sizes across 84 call
sites
. Eleven sizes is not a scale. It is eleven separate decisions taken one component at a time
and never compared with one another.

What changed

The scale separates read from scan, which is the distinction that was missing. Prose is read
in sentences and needs size. Monospace and tabular data is scanned, and density genuinely helps
there. So scanned data stays tight at 12.5px while body prose moves from 13px to 14px, the technique
objective gets its own 15px, and nothing renders below 11px. Six named rungs replace the eleven ad
hoc values, so the next component cannot quietly invent a twelfth.

Two regressions were introduced by the change and caught by measuring again afterwards. Both are
recorded because the second had shipped once before.

  • SIGNAL FIELDS in the signal-path diagram grew wide enough to sit on top of its own values. The
    values moved rather than the label shrinking back.
  • Check Point wrapped to two lines in the vendor picker and doubled the control height. It needs
    84.1px in an 83.7px segment, so half a pixel of growth tipped it over, and it had always been
    marginal. Restored to 12px and given nowrap, so the next long vendor name overflows visibly
    instead of silently reflowing, which is exactly how it shipped that way the first time.

The screenshot script had been broken for months

scripts/capture-webui-screenshots.py clicked a button labelled Start run. That button was
renamed to name its destination some time ago, so the script had been failing since, and nobody
knew, because it only runs when someone regenerates screenshots. It now matches on a stable prefix.
All five images are regenerated against the new scale.

Unchanged

Every known limitation from v0.5.1 still stands, including F-08 (the events-per-second cap is
per process) and F-14 (the remaining advisories are development-only and their fixes drop
Node 18). Palo Alto and Check Point vendor fidelity remains [Unverified] against real appliances.

The attached wheel was built from a clean checkout of this tag and smoke-tested outside the
repository, including that the rebuilt web assets ship inside it.

Replicant v0.5.1

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 04 Aug 00:42
82ab0b9

A security and correctness release. It closes everything actionable from two independent
reviews, plus three defects that surfaced during a live LogRhythm lab session and that
neither review found.

Upgrade from v0.5.0. That release is affected by every item under Security below.

Security

  • Cross-site WebSocket hijack of the embedded terminal (F-01). Origin validation compared
    hostnames and ignored scheme and port, so any other development server on an analyst's laptop
    could drive the PTY using the ambient session cookie. An origin is now the (scheme, host, port)
    triple, required on every handshake.
  • The session cookie was the master token (F-04). It held the persistent launch token from
    ~/.config/replicant/web-token verbatim: no expiry, no rotation, no way to revoke one browser
    without regenerating the token file and breaking every other client. The cookie now carries a
    short-lived random session id. The token is also gone from EventSource and WebSocket query
    strings, where URLs reach server logs, browser history and Referer.
  • A terminal child could hang the whole server (F-05). Termination blocked on waitpid on the
    asyncio event loop, so a child ignoring SIGTERM froze every request the server was serving.
    Now non-blocking with SIGKILL escalation, and terminal sessions are capped.
  • Failed runs wrote no manifest (F-02).
  • Markdown in the Docs tab could execute same-origin code (F-03), plus a javascript: link
    hole found while fixing it. A CSP is now sent as defence in depth.
  • --no-auth opened HTTP but rejected every terminal session (F-06).
  • Web file output could truncate arbitrary paths (F-07).
  • Malformed durations were accepted silently (F-09): -1h parsed as a positive hour.

The telemetry was wrong on two of three vendors

Check Point hardcoded act=Reject / auth_status=Failed Login, and Palo Alto hardcoded
PanOSEventID=auth-fail, on the event:system login path. The engine only ever sends
status=success there, so every administrative login in REP-018 was self-contradictory, wrong
in exactly the field a correlation rule matches, on the technique whose whole premise is
successful logins moving host to host. FortiGate was always correct.

Runs that looked like they were working

  • A configured collector did not mean send. The web form's destination switch defaulted off, so
    a verified collector plus a technique plus the run button rendered every event and delivered
    none, while the stream, the progress and the eps readout all looked identical to a working run.
    Measured after the fix: CLI 200 datagrams, web 200 datagrams, identical parameters.
  • The single-run lock was invisible. A page reload showed an idle form while the server was
    hours into a run, and the button then failed with a 409 naming a run the operator could not see
    or stop. GET /api/runs/active reports it, and the form offers to stop it by name.
  • Send test log said verified against a collector that could not receive anything, because
    it was set from a UDP sendto succeeding, which only proves a route exists. The word is gone
    from the codebase. What replaced it is disclosure: the connect test reports what it proved and
    what it did not, and shows the source address beside the destination, because that is what makes
    a mistyped address visible.
  • A refused collector produced a traceback, not a message. The connect timeout also governed
    every later send.

Added

  • Every use case states its core objective, shown first in the web UI. The panel used to open
    with "emits synthetic X telemetry that exercises Y", true of all 24 entries and so answering
    nothing.
  • IPv6 collectors (F-10).
  • Self-describing manifests: vendor, duration, rate, send_stats.

Known limitations

  • F-08 is open. The events-per-second cap is per process, so several processes can multiply it
    against one collector. Terminal session caps bound it; the remedy is undecided.
  • F-14 is partial. Production dependencies audit clean; the remaining advisories are
    development-only and their fixes require dropping Node 18.
  • Palo Alto and Check Point vendor fidelity remains [Unverified] against real appliances.

Full detail in CHANGELOG.md.
The attached wheel was built from a clean checkout of this tag and smoke-tested outside the
repository.

Replicant v0.5.0

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 02 Aug 08:59
b4d8770

Replicant emulates a TTP by writing the telemetry the attack would have produced, so the shape of that telemetry is the product. v0.4.0 fixed when events arrive. This fixes how much of the behaviour you get when you ask for a window of it.

Fixed: four use cases ignored --duration

Asking for a two hour simulation has to produce two hours of telemetry. Four of the twenty-four use cases silently planned something else:

use case asked 2h, planned why
REP-005 / UC-004 5h 51m pinned to a fixed six hour off-hours window, flag ignored
REP-014 / UC-013 5h 59m read the value as a per-session length, then multiplied by session count
REP-019 / UC-018 2d 10h span was probe count times a random gap
REP-023 / UC-022 2h 59m span was session count times a fixed interval

A flag that works on twenty entries is worse than one that works on none: an operator learns to trust it and is then wrong four times in twenty-four without being told which. tests/test_duration.py now asserts all 24 by parameter, so a regression names the technique rather than arriving as "something is long".

The rule this establishes

--duration bounds the span. Where the interval between events is the detection signal, the interval is preserved and the event count falls.

That is what separates it from --speed, and the difference decides whether a rule can fire:

event count intervals
--duration 2h falls preserved
--speed 6 preserved divided by 6

A two hour beacon under duration is 24 callbacks five minutes apart. The same thing under speed is 240 callbacks fifty seconds apart, which no interval-keyed rule recognises. Where the gap between events is the signal, duration is the one to reach for.

Added: --duration on scenarios

A scenario's span used to be whatever its catalog stage offsets happened to add up to. SCEN-001 was 12h 14m and nothing could ask for two hours.

Duration scales the composition: stage offsets move proportionally and each stage is planned for a proportionally shorter window, so the chain keeps its order and its relative spacing while every technique inside it keeps its own interval and emits fewer events.

SCEN-003: natural 6h 56m (653 events)  ->  asked 2h: 1h 59m (619 events)

Two passes, because the scale factor cannot be known until the natural chain has been built: stage spans come from each technique's preset, not from the catalog. The untimed path stays a single pass and is byte-identical, which a test asserts.

A stage pinned to the clock outranks the request, and says so

REP-005 is off-hours bulk transfer, and off-hours is 00:00-06:00. It advances in whole days to clear that window, so SCEN-001 cannot be compressed below the jump:

SCEN-001: natural 12h 14m  ->  asked 2h: 7h 16m

manifest: "requested duration 7200s, composed 26214s: stage(s) REP-005 pin to an
           absolute window and were advanced whole days to clear it, which the
           scenario timeline cannot scale away"

Returning a quietly twelve hour run for a two hour request would be the same class of defect as the pacing one v0.4.0 opened with. A single off-hours technique asked for more than six hours is capped at the window for the same reason: spilling into the working day would stop it being the thing it demonstrates.

ScenarioManifest records the requested duration (safety rule 5), because two runs of the same scenario and seed can now cover very different windows.

Upgrade note

Four techniques now plan a different length for the same flags, and scenarios accept an option they previously rejected. Nothing was removed and nothing errors, which is why this is a minor bump rather than a patch.

Verification

728 Python tests and 118 frontend tests, black / ruff / mypy / shellcheck / tsc -b clean. All 10 CI jobs green on the tagged commit.

Known limitation

eventtime is integer epoch seconds, so one second is the finest gap a plan can express. That bounds both compression and how short a faithful run can be.

Replicant generates synthetic telemetry for detection engineering in a lab. It writes log text and never executes attacks.

Replicant v0.4.0

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 01 Aug 15:07
b19b080

Two things a live LogRhythm test found, and neither was a bug in what Replicant generates. The events were always correct. When they arrived, and whether the interface could be read, were not.

Upgrade note: live sends now take as long as the plan says

This is a behaviour change, and it is the reason for a minor bump rather than a patch. replicant run REP-001 --host 10.20.0.50 used to return in about three seconds. It now takes 238 minutes, because that is how long the beacon it describes actually lasts.

Nothing was removed and nothing errors. Every interface states the projected duration before it starts, and --pace burst restores the old behaviour exactly. Scripted callers that only need to prove a socket works should say --pace burst explicitly: that is what the installer's own loopback check now does.

Fixed: events are sent when the plan says they happen

Measured against a live collector: REP-001 --intensity low delivered 49 events in about 3 seconds, carrying event times spread over 238 minutes.

The plan has always held a per-event eventtime. The emit loop ignored it and fired as fast as the rate cap allowed, so what reached the SIEM was a snapshot claiming to be four hours of history. No interval-keyed detection can work on that: a beacon rule asking for N callbacks at a regular interval over M minutes sees every callback at once, so it either never fires or fires on the wrong shape.

--pace {burst,plan} and --speed N, on the CLI, the scenario runner, the Rich menu and the web run form.

  • plan reproduces the gaps the plan's own timeline holds. The default whenever events go to a collector.
  • burst is the previous behaviour, kept and defaulted for --to-file, where the wall clock means nothing.
  • --speed N compresses the timeline, event times included.

Compressing only the schedule would have re-created the original defect at 1/60 scale: events stamped 238 minutes ahead, delivered inside four. Moving both holds one invariant at any speed:

With --pace plan and --anchor now, an event is sent at the moment its own timestamp says it happened.

Verified over a real socket rather than in a test. A 14280 second plan at 1428x delivered 49 datagrams over 10.0s, carrying a 10s event-time span, with the furthest event time 0 seconds into the future.

The cost is stated rather than buried: compression preserves relative timing and changes absolute intervals, so a rule keyed on five minute gaps will not match a run compressed 60x. Real time to validate a rule, compressed for a smoke test.

--rate is unchanged and composes rather than competes. It stays the events-per-second flood guard and enters the schedule as a floor on spacing, so a plan holding several events in one second still cannot deliver them together. The two answer different questions and are kept apart in the UI.

Added: the choice is a control, not a flag you have to know about

POST /api/plan prices a run without starting it, so the web form can put each option's real duration on the option itself:

Pacing   (•) Plan time 3h 58m    ( ) Burst 0.2s
Sends each event when the plan says it happens. This run takes 3h 58m,
and event time matches send time throughout.

Radio buttons rather than a dropdown: a dropdown shows one option and hides the other, and the comparison is the entire value. The Rich menu asks the same question on a live send and prices the run before "Start run?". The CLI prints the projection before the first event.

Every run manifest now records pace and speed. The same seed and technique can put very different shapes on the wire, so the shape is part of the audit record.

Added: a light theme, and a responsive layout

Present in main since 29 July and unreleased until now.

  • The UI follows prefers-color-scheme on first load and remembers an explicit toggle after that.
  • Warm paper rather than cold white, with contrast measured pair by pair against the dark theme's own ratios. That audit found four defects in the shipped dark theme, including body text at 2.78:1.
  • Below 1024px the fixed-viewport shell becomes an ordinary scrolling page and the left rail becomes a disclosure.
  • The embedded terminal resolves its colours from the stylesheet and recolours in place, rather than tearing down the websocket and killing a running menu process.

Verification

691 Python tests and 118 frontend tests, black / ruff / mypy / shellcheck / tsc -b clean. All 10 CI jobs green on the tagged commit.

The scheduling arithmetic is asserted purely, with no clock and no sleeping. The delivered shape is asserted on observed send times against planned gaps, because the claim is about what a collector sees. End to end over a real socket: 5s of event time delivered in 0.17s under burst, over 5.04s under plan, and the no-flag default matching plan exactly.

Known limitation

eventtime is integer epoch seconds, so one second is the finest gap a plan can express and a hard ceiling on useful compression. Past roughly the plan's own gap size, every event collapses into the same second.

Replicant generates synthetic telemetry for detection engineering in a lab. It writes log text and never executes attacks.

Replicant v0.3.1

Choose a tag to compare

@404SecNotFound 404SecNotFound released this 29 Jul 11:29
83775d6

A packaging fix. Nothing changes for anyone running from a git clone, which is how 0.3.0 was tested and how CI runs. Everything in v0.3.0 still applies.

Fixed: a wheel install produced a tool that could not run

pip install of the 0.3.0 artifact succeeded and replicant --version printed 0.3.0. Every other command then failed with catalog not found, and the web UI served its "build the frontend" placeholder instead of the real interface.

Everything Replicant needs at run time lived outside the package and was reached by repository-relative paths. pyproject.toml packages replicant* and there is no MANIFEST.in, so the wheel contained 40 entries: the Python modules and py.typed, no catalogs and no built frontend. It was also dependent on the working directory, because the CLI fell back to Path.cwd(). That is why it went unnoticed. From a checkout it worked and from anywhere else it did not, and every test imports from the source tree, so the suite stayed green while the artifact was unusable.

  • data/ moved to replicant/data/, and the frontend build output moved to replicant/webui_dist/. Both now sit inside the package.
  • replicant/resources.py is the single place that knows where runtime files live.
  • package-data covers the catalogs, the built UI, and its fonts.
  • Catalog resolution prefers the packaged copy over working-directory guesses. An explicit catalog_path setting still wins.
  • docs/ is deliberately not packaged. A second copy inside the package would drift from the first.

This closes the "the built wheel is not yet self-contained" limitation recorded under 0.1.0.

Added: guards, because unit tests structurally could not catch this

  • tests/test_packaging.py asserts runtime files resolve inside the package, that package-data actually covers them, and that the catalog resolves from an empty working directory.
  • A wheel CI job builds the frontend, builds a wheel, installs it into a clean virtualenv and runs it from an unrelated directory.
  • The version is asserted to be single-sourced: replicant.__version__ and pyproject.toml have to agree.

Also corrected: replicant/__init__.py still described the package as FortiGate-only. It renders FortiGate, Palo Alto PAN-OS and Check Point, and has since Phase 3.

Verification

From a clean virtualenv outside the repository, in a directory containing no data/: replicant list shows 24 techniques, a run writes 36000 CEF lines, replicant scenario list works, and the web UI serves the real single-page app.

526 Python tests, 68 frontend, black / ruff / mypy / shellcheck clean. All 10 CI jobs green.

Replicant generates synthetic telemetry for detection engineering in a lab. It writes log text and never executes attacks.