Test benches
A test bench replays an archetype's day towards real intakes. It is the screen answering the question that neither the workflow nor the coverage settles: what does this archetype actually produce, and does an intake accept it?
/essais lists the benches; each one opens three tabs.
What a bench remembers
| Setting | What it decides |
|---|---|
| Archetype | The workflow describing the day |
| Population | The users, servers and websites of the rendering |
| User | A single one, or the whole population — each with their own day |
| Technologies | The selected generators, and one intake key per technology |
| Destination | One of the 7 SEKOIA regions, or a custom URL |
| Time zone | The time the population lives by — inherited from it, overridable here |
A bench is saved. That is the whole point: you open it, adjust an action in the archetype, rerun, compare. Re-entering eight intake keys on every trial would make that gesture impractical — precisely the gesture the screen exists for.
The three scopes
| Scope | What it produces | Why |
|---|---|---|
| Whole day | From midnight to midnight | To see the day the archetype describes |
| Time window | A slot, for instance 08:00–09:00 | To judge a morning without sending 24 hours |
| One minute | One exact minute | The slice the per-minute scheduler will emit |
The day is always computed in full, then sliced. That is not an implementation detail: it is what guarantees a minute is literally a subset of its hour, and of its day. Without it, the bench would lie about what the scheduler will send — it would render something else for the same minute.
The simulated day is yesterday by default. A whole day dated today would produce post-dated logs, which an intake would index in the future.
Preview then send
The gesture comes in two steps, like the Preview tab of an event type.
The preview builds the day, projects it towards the selected technologies, and renders only a sample of it. It never refuses: this is where you discover the volume, before sending it. Its counts are exact even above the cap, because projecting costs no rendering.
The send renders everything, then queues it. Splitting by technology is not an optimisation: a batch carries one intake key, and the key is carried by the technology.
The send button lives in a bar pinned to the bottom of the screen, with the volume that will leave and whatever would prevent it from leaving — cap exceeded, messages already past. It is not a shortcut: the bench is an instrument of iteration, and the preview separating the two buttons is several screens tall as soon as it renders its twenty messages. The bar is not a second button, it is the button: a send always starts from the settings of the displayed preview, and two controls for one gesture would end up carrying different ones.
What the preview shows
-
The timeline — one gesture per row, with what it puts into play and the technologies that saw it. This is where correlation reads: the same login appears at the same instant in the EDR and in the Windows log, because there is only one gesture, projected towards both. The Object column names what that gesture joins — the host, the process, the document — and it is that value that every payload carries. No other screen in the product shows this.
-
The volumes per technology — how many messages, across how many distinct event types.
-
The gestures per action — the mirror of the previous card: the two axes of the same total. For each workflow node, how many gestures it produced, how many messages came out of it, and how many technologies saw it among the bench's. It is the only card making the fan-out legible, and it is very uneven: out of seven technologies, a web browse is seen by all seven, a DNS resolution by five, a scheduled task by two. A gesture that is rare and little observed — typically the system noise a shared super-archetype brings — otherwise drowns in the business volume, and its apparent absence reads as a failure. Below the table, the actions from which no message came out are named, along with the technologies that drew nothing from them: “seen by 2 of 7” says there is something to look at, the name says which intake to look in.
It is mounted before the timeline on purpose: the timeline is truncated to its first 120 rows, and therefore never allows an absence to be concluded.
-
The forks of the day — for each fork of the archetype, how many users took each path: Evening VPN — Evening remote access: taken for 9 of 25, and the empty outlets likewise — in hours for an hourly fork. Rendered even when a path is taken everywhere, because the draw is a fact of the day and not merely the explanation of an absence: without it, a path taken by nine users out of twenty-five would appear only in the next card, as having produced nothing.
-
What produced nothing, and why. Two opposite causes, which must be told apart:
- a skipped action — “probability draw”, “path not taken”, “broken chaining”, “no server in the population”: that is settled in the workflow;
- a coverage gap — no selected technology realises the action: that is settled in the event types. A sales profile followed by an EDR and a proxy will thus see its cloud authentication stay silent, for want of a directory among the ticked technologies: the bench says so rather than rendering the action through an unrelated event type.
-
The shifted actions — those that happened after their upper bound, because chaining takes precedence over the window. Neither a surrender nor a defect: a morning rendered at 8 pm would pass for an engine gone wrong if nobody said it was the late arrival that pushed it back.
-
The rendered messages, as they will leave, spread over the technologies rather than taken from the top — the first twenty messages of a day all come from the arrival at the office.
The facts of a gesture: what every technology names the same way
A gesture does not carry only an instant and an entity: it also carries what it puts into play. A browse joins a host, at an address, for an application; an application launch starts a binary, with a pid; a document modification touches a file, in a directory.
Those facts are computed once, before the gesture is projected towards the technologies. All its projections therefore receive the same ones. Concretely, for one and the same browse:
| Technology | What it writes |
|---|---|
| Windows — Sysmon DNS query | QueryName: www.lemonde.fr · Image: …\chrome.exe |
| Windows — Sysmon network connection | DestinationHostname: www.lemonde.fr · DestinationIp: 151.101.1.91 |
| Sekoia Agent — DNS resolution | question.name: www.lemonde.fr · resolved_ip: 151.101.1.91 |
| Sekoia Agent — network connection | destination.ip: 151.101.1.91 · port: 443 · executable: …\chrome.exe |
| Netskope | urlhost: www.lemonde.fr · dstip: 151.101.1.91 · app: Le Monde |
| Zscaler | hostname: www.lemonde.fr · serverip: 151.101.1.91 · action: Allowed |
| FortiGate — traffic | dst=151.101.1.91 · dpt=443 · FTNTFGTapp=Le Monde · dstcountry=France |
| Palo Alto NGFW — traffic | dst_ip: 151.101.1.91 · dst_port: 443 · url_category: news-and-media |
| SentinelOne Cloud Funnel | dst.ip.address: 151.101.1.91 · src.process.name: chrome.exe |
Sekoia Agent does not name a host in its network connection: its format has no such field. It is the DNS resolution of the same gesture that names it, and it is that pair which makes the correlation legible — inventing a field for it would make the agent say something other than what it really emits.
Not every technology reads the facts yet. Eleven do today (Windows, Netskope, Zscaler, BIND, Sekoia Agent, HarfangLab, CrowdStrike Telemetry, FortiGate, Palo Alto NGFW, Palo Alto Cortex, SentinelOne Cloud Funnel); the others still draw from their own corpus, and two of them will therefore describe the same gesture while naming it differently. It is a known debt, and it is filled template by template.
The four firewalls and proxies on that list entered it together, and it is the most visible
catch-up: they are the ones the coverage files under the six gestures joining a destination, so that
they described every browse. FortiGate announced dst=10.0.1.100 dpt=3389 — an RDP session drawn
from its fourteen sample lines — while Windows and Netskope named the site actually visited, at the
same second and for the same user.
Three consequences are worth knowing.
The facts depend neither on the trial nor on the ticked technologies. They derive from the gesture's identity, which derives from the entity, the day, the workflow node and the rank of the occurrence. Two benches replaying the same day of the same user therefore name the same sites — and ticking one more technology does not change a single message of the others.
A fact is imposed by group, never by field. Imposing the host while leaving the application to
chance would make it say “GitHub” about drive.google.com: a message consistent with the others and
inconsistent with itself. The destination therefore leaves whole — host, URL, address, port,
application, category, country, policy, verdict. The rule also applies to the machine fields
doubling a readable one: a firewall writing “Le Monde” in plain text with the application identifier
for remote desktop right next to it contradicts itself in two neighbouring fields, so the identifier
follows the name.
The verdict belongs to the gesture, not to the observer. Two proxies can no longer contradict each other about the same browse. In the business flow it is always “allowed”: a gesture the workflow describes is a gesture that succeeded. Refusals will belong to the threat scenarios.
An authentication carries its own too. A sign-in puts into play an outcome, a session type — interactive, network, unlock, remote desktop — and a protocol. All three belong to the gesture, for the same reason as a browse's verdict: Windows drew its session type from four values while Sekoia Agent drew from two, so that the two tools described the same remote connection with the same timestamp, the same user, and two different session types seven times out of eight. They now agree by construction, and the outcome follows the same rule as the verdict: it is “success” in the business flow, except for the “Authentication failure” action, which says what it names.
The pid identifies a process, not a gesture. The pid and the process identifier derive from the user, the day and the binary — not from the occurrence. A given workstation's browser therefore keeps the same pid all day long, like a real process, and two technologies describing the same gesture name the same one. A technology is only given the pid if it already holds the gesture's binary: imposing one without the other would make it say “pid 4048, svchost.exe” next to “pid 4048, OUTLOOK.EXE” — the contradiction moved one step rather than fixed.
The process follows the destination. A browser speaks HTTP(S); any other service designates the
client that speaks it natively. Reaching the mail submission relay therefore launches a mail client,
never a browser — a chrome.exe egressing on port 587 was a message contradicting itself in two
neighbouring fields, when both came from the same fact. A destination can override the rule when
HTTPS is spoken by something else: outlook.office365.com is the access point of a thick Outlook,
while outlook.office.com and mail.google.com really are viewed in a tab. Same distinction for
machine gestures — a Windows update belongs to a system service, not to a browse.
The destinations live in a shared dataset (“Destinations web”), editable from the datasets screen: an application estate that is not the catalogue's can be added there without a deployment. Mail clients have their own (“Clients de messagerie”), for the same reason — the corpus carries none. Browsers, system processes and documents reuse the corpus's pools.
Which technology sees which gesture
A technology does not realise every action of a workflow, and that is what makes a trial legible: ticking a directory and a firewall must not produce the same log twice.
The split happens in two families.
The logs naming their gesture. Salesforce's lightning-page-view speaks of a Salesforce page,
sysmon-dns-query of a DNS resolution, nspolicy-download of a download. They realise one
action, the one their name describes.
The transaction logs. A proxy, a firewall, a socket-level agent do not see which business
gesture they carry: they see an outbound request. Netskope's nspolicy-allow, Windows's
sysmon-network-connection, Sekoia Agent's network-connection therefore realise all the
actions joining a destination — browsing, CRM, mail, instant messaging, video conference, file
transfer. It is the gesture's facts that tell the six apart: one and the same Netskope line will say
app: Salesforce / category: CRM for a CRM access and app: Le Monde for reading the news.
Two consequences.
The direction of the flow matters. A web server — Apache, IIS, a web application firewall — describes the inbound traffic towards the company's sites. Its log looks feature for feature like a proxy's, and describes the opposite: it therefore only realises the browsing, never the whole family.
Any technology covering the action sees the gesture, as soon as it can describe it. There is no observation ratio: if a technology is ticked and realises the action, it produces a message, on every occurrence. That is what makes the correlation legible — the same instant described by every source at once, exactly what a SOC correlates.
The nature of the flow matters too. A web proxy log only describes HTTP(S): it has nothing to say
about an SMTP submission. One mail gesture in four joins smtp.office365.com on 587, and both
Netskope and Zscaler stay silent there — they have no other event type to offer on that gesture. They
keep describing the other three, the ones going through the web. Firewalls and agents, for their
part, see all four: an SMTP flow is a flow like any other for whoever watches the socket. This is
therefore not an observation ratio coming back through the side door: the fallback reads on the
gesture's facts, and the technology only withdraws where none of its logs can describe what it sees.
A technology renders one event type per gesture, the heaviest of those realising the action. The same login does not produce three lines in the same EDR; an event type's weight serves to break the tie, and a zero weight reserves it for alerts.
The table is editable: an event type's screen lets you add or remove an action, and the correction survives a reimport of the corpus. What the product infers from the name is only a starting point.
The cadence: when each message leaves
A message carries its own instant. Sending everything at once therefore makes ten hours of a day arrive within the same second: the carried time and the arrival time contradict each other, and the intake indexes the contradiction. Three cadences answer that. It is set on the bench, under Settings, and overridden on every trial.
| Cadence | What it does | When to pick it |
|---|---|---|
| Immediate, paced (default) | Everything leaves now, one message per request, spaced by the interval | Iterating: adjust, rerun, compare |
| Chronological | Each message leaves at the time it carries; whatever is already past is set aside | Seeing what the per-minute scheduler will do |
| Live replay | The slice is realigned on now, and the timestamps are rewritten at send time | Demonstration, 1:1 replay |
The preview announces what the cadence will do with time — number of requests, first and last dispatch, duration — before you send. 25,000 messages at one second apart means 25,000 requests and nearly seven hours: that is the sort of thing better discovered at the preview.
In chronological cadence, a minute's batch leaves at the end of that minute. That is not a detail: it is exactly the slice the future scheduler will emit on every tick, so that the bench prefigures the planner instead of contradicting it by a minute.
In live replay, the timestamps are shifted before rendering. Delaying only the delivery would send at 4 pm a message claiming to be from 9 am; here, the message is dated by its arrival. An hour's slice therefore takes an hour, and a whole day twenty-four.
A scheduled trial can be stopped: “Annuler les envois programmés” removes from the queue the batches not yet sent, and says how many. What has left is no longer stoppable.
Scheduling for later
A future date is accepted without reservation: nothing is late, so nothing is set aside. That is the normal way to use the chronological cadence — you prepare tomorrow's day in the evening, and it unfolds on its own.
What happens then is worth knowing, because none of it is visible:
- Everything is rendered the moment you click. The messages are not manufactured at the due date: they go to sleep in Redis, full payload, for up to forty-eight hours. A whole-day trial means a thousand batches waiting.
- They survive restarts — of the sender as well as of Redis, which persists to disk. A sender restarted after the due date catches up the ripe batches and posts them in one go.
- The intake key is only read at the moment of posting. That is what stops a secret from sleeping in the queue for hours — but it means erasing the key, or deleting the bench, between the scheduling and the due date makes the remaining batches fail. The failure is definitive, without retry, and leaves a history row saying so.
- Cancelling stays possible as long as the batch has not left, and the screen permanently announces how many batches are still waiting in the queue.
The simulated day runs on the population's time
An archetype's day is anchored on midnight where the population playing it lives. An action set to 08:00 happens at 08:00 for its users — wherever they live, and whatever the season.
The time zone is set on the population (/datasets/…, “Metadata” card): it is the population
that lives somewhere, not the bench replaying it. A bench inherits it, and can override it in its
settings — enough to replay the same population at two different times without duplicating it. No
time zone set anywhere = UTC, that is, the original behaviour.
An IANA zone (Asia/Tokyo, Europe/Paris) and not an offset in hours: the offset is resolved for
the simulated day, so daylight saving follows on its own. A frozen offset would be wrong six months a
year, and every bench would have to be touched twice a year.
Three frames of reference, and the screen names them all
| What you read | In which time zone |
|---|---|
| The slice, the timeline, the window you type | The population's |
| The timestamp inside the payload | Always UTC |
| The dispatch due dates | Yours, the browser's |
The second deserves emphasis: the time zone moves when a gesture happens, never how it is timestamped. A workstation in Tokyo also ships its logs in UTC — its 08:00 morning therefore arrives dated 23:00 the day before, and that is exactly what the bench sends.
Two consequences to know
The day of the week is read in the population's time zone. An archetype whose actions are set to weekdays therefore produces an empty preview if you ask for a Saturday: that is not a failure, it is the workflow having nothing to do that day, and the “What produced nothing” lines say so. A Japanese Saturday stays a Saturday, even if its morning falls on a Friday in UTC.
It is also what you do not want to put up with while adjusting an archetype: the bench is an instrument of iteration, and waiting until Monday to see what a day produces is not one. The “Ignorer les jours actifs des actions” box (ignore the actions' active days), below the Simulated day field, lifts the filter for that trial. Three things to know about it:
- it is a setting of the trial, not of the bench. It is not saved, exactly like the date it depends on — you tick it because that day falls on a Saturday;
- it is one single rule, whatever the day: active days no longer filter. The counterpart is accepted — an action reserved for the weekend will also leave on a Tuesday;
- nothing is forced silently. The affected actions are listed under “Forced actions”, and the Simulated day tile recalls the option even on days when it changed nothing.
It only lifts the day filter. A missed probability draw, a path not taken, a broken chaining stay what they are, and “What produced nothing” keeps saying so.
A daylight-saving switch day is treated as a 24-hour day, at the offset it has at local midnight. After the switch, the gestures of that one day are therefore shifted by an hour. A 23- or 25-hour day would be unsolvable in an engine whose whole arithmetic is in minutes since midnight; staying silent about it would be worse.
A slice already past is set aside, never caught up
In chronological cadence, a message whose time has passed cannot leave “on time”. Catching up silently would lie: ten hours of a day would arrive within the same second, claiming to be from the morning.
Those messages are therefore set aside, and the rest of the slice leaves normally. Launching at 10 am a day that started at 8 am therefore sends the afternoon, and leaves the morning behind. It is the report that makes the difference with a window trimmed behind your back: the preview announces what will be set aside before the send, and the result says it again after, in figures.
The refusal remains in one case only: when there is nothing left to schedule. There is then no truncation to announce, and a send succeeding without sending anything would be more disconcerting than a refusal.
The warning names the lever for a whole slice, and that lever depends on the scope: in a time window, move the lower bound to the time given; in a whole day, switch to a time window — a whole day has no adjustable bound. In both cases, live replay remains the other answer: it realigns the slice on now and sets nothing aside.
A consequence worth knowing: a trial's default date is yesterday, and yesterday's day is over by construction. A chronological trial left on the default date is therefore refused, for want of anything left to send — not sent empty.
The cap: a refusal, not a truncation
Beyond 25,000 messages, the send is refused, naming the numbers and the two levers: narrow the scope, or pick a single user.
Truncating silently would be worse than refusing. A trial cut at the first 25,000 messages would render a morning while presenting itself as a day, and a false conclusion would be drawn from it. A quantified refusal costs one click. It is the same rule that allows an already-past slice to be set aside: what is forbidden is to subtract without saying so — not to subtract.
A population of more than 25 users is capped and reported: nothing can be done about its size, but the screen never passes 25 days off as 415.
The intake keys
A saved key is never shown again. The screen only shows its mask — ••••1a2b — and the “set”
state.
This revises the rule “the intake key is never persisted”, which stays true of manual dispatch and of
the history. The counterpart is structural, not disciplinary: a global omit on the Prisma client
removes the column from every read in the monorepo, and it takes a deliberate omit, visible on
review, to bring it back. Only one caller sets one, at the moment of posting — and not at the
moment of composing the job: a batch scheduled for 6 pm would otherwise leave a secret sleeping in
the queue for hours. The job therefore only carries the key's provenance, and the sender service
reads it back from the database just before posting it.
Direct consequence on the screen: leaving the field empty keeps the key in place. Erasing it requires an explicit gesture — emptying the field cannot mean “erase”, since that is exactly what “keep” means.
Following the dispatches
The send renders what has been queued, per technology. The real outcomes are watched on Dispatches: a trial places up to thirty or so batches, and waiting for all of them would time the request out for nothing.
Every batch names its technology there. A trial batch carries several event types of one technology — its origin is therefore the technology alone, and it is a complete origin.
What has not left yet does not appear in that list: the history row is born when the batch is posted. A bench's page and the dispatches page therefore announce separately how many batches are waiting their turn in the queue, and when the first and the last due dates fall. Without that count, a trial scheduled for the next day reads, come morning, as a trial never launched.
Determinism, and why it matters
Two trials of the same bench, on the same day, render exactly the same thing. That is what makes a trial comparable with the previous one — the only use of a bench.
Two properties guarantee it, and they are tested:
- adjusting one action moves no other — every action draws from its own random stream, otherwise touching the probability of lunch would shift the day's 74 mail checks, and the screen would pass for non-deterministic;
- ticking one more technology changes no other — every (gesture, technology) pair draws from its own.
What the bench does not simulate
It plays only the correlated layer: the workflow's gestures, projected towards the technologies. The ambient traffic specific to each technology and the alerts will come with the steps that produce them. A trial's volumes are therefore lower than a real production's.
The three alertHeavy generators (CrowdStrike Falcon, Microsoft Defender XDR, SentinelOne Identity)
have an empty business coverage by construction: selected on a bench, they will emit nothing. The
screen flags it at selection time.
A gesture's facts are not passed from one gesture to another: a DNS resolution and a browse are two distinct gestures, with two destinations. Nothing says yet that the first resolves what the second joins — within one gesture, however, the DNS query and the network connection do name the same host.
Finally, not every template reads the facts yet: the migration happens technology by technology. A non-migrated event type draws from its own pool, as before, and the screen shows it — its row carries an object its payload does not name.
Trying it locally
With a stub intake rather than a real region:
node apps/web/e2e/mock-intake.mjs
Destination “Custom URL”, http://localhost:4599/batch. The stub accepts the body
{ intake_key, jsons }, and GET /received returns what it has received.