Softanza
DémarrerEnglish
Softanza
Recherche · narration

Detection over Sequences

Softanza is full of rules. A graph rule asks whether a sandboxed actor can reach an effectful capability. A posture invariant asks what is true of the system right now. An org rule asks whether one position both approves and executes. Every one of them judges structure at a single instant.

par Mansour Ayouni · dans le dépôt depuis le 2026-07-31 · série · la série sécurité · domaine Sécurité et gouvernance

Cet article est écrit en anglais, comme la bibliothèque.

Every other rule in the library judges one moment — an incident is a story

Every code block below is real, and every output block is its actual output (the run lives in base/test/system/_secdetect_narration_demo.ring; the guard suite is base/test/system/security_detection_narrated.ring, 33 assertions). Incident analysis I3 — doc/design/SOFTANZA_INCIDENT_ANALYSIS.md.

The shape the library never had

But "five failed logins inside a minute" is not a structure — it is a sequence. "A failed sign-in, then a reach for a production secret" is a story with an order and a clock. No rule shape in the library could express that, which is why I2's ledger, useful as it is, still needed a reader.

Three shapes, deliberately few

Haro
oD = StzDetection("credential-stuffing")
oD.WhenKind("auth.login.failed").PerActor().Repeats(5).Within(60000)

oD2 = StzDetection("guess-then-reach")
oD2.WhenKind("auth.login.failed").ThenKind("secret.reveal.refused")
   .BySameActor().Within(300000)

oD3 = StzDetection("cloned-authenticator")
oD3.WhenKind("auth.passkey.clone_suspected").OnAnyOccurrence()

illustration

BURST (n of a kind inside a sliding window), SEQUENCE (b within a window after a), ANY (one occurrence is already the story — a cloned authenticator needs no second opinion). Three, not thirty: each is computable over a bounded ledger without inventing a query language, and together they cover the shapes the library's own catalog can produce.

PerActor() matters more than it looks: credential stuffing is per account, not per installation. The guard proves the difference — nine failures spread across two accounts fire the grouped detection once (for the account that actually burst) and the ungrouped one once (for the installation).

What the library ships knowing

Detection content is usually something you buy or write. Softanza ships eight detections over its own catalog, because it knows its own event names:

Haro
StzDefaultDetectionSet().Show()

illustration

Sortie promise
Detection set softanza-default -- 8 detection(s).
  Detection credential-stuffing [error] -- 5x auth.login.failed within 60000ms, per actor
    repeated authentication failures against one account
  Detection secret-probing [error] -- 3x secret.reveal.refused within 300000ms, per actor
    an actor repeatedly reaching for secrets it may not have
  Detection escalation-attempts [error] -- 3x capability.refused within 60000ms, per actor
    an actor repeatedly attempting acts beyond its capabilities
  Detection guess-then-reach [error] -- auth.login.failed then secret.reveal.refused within 300000ms, same actor
    a failed sign-in followed by a reach for a secret -- the classic shape of a stolen-credential attempt
  Detection cloned-authenticator [error] -- any auth.passkey.clone_suspected
    a signature counter that did not advance
  Detection replayed-request [error] -- any sig.nonce.replayed
    a nonce reused for the same key -- an active replay
  Detection forged-request [error] -- any sig.signature.forged
    a signature that did not verify -- forged, tampered, or wrong key
  Detection replayed-assertion [error] -- any sso.assertion.replayed
    a SAML assertion presented twice

The whole arc, on a real run

Nothing scripted below: a real stzAuth refuses five real bad passwords, a real stzSecretStore refuses a real sandboxed actor, the I2 seams fill the ledger, and the detections read it.

Haro
StzOpenSecurityLedger(512)
# ... five failed logins for 'victim', then an LLM actor reaches for stripe-live ...
aFindings = StzDefaultDetectionSet().CheckAgainst(StzSecurityLedgerQ())

illustration

Sortie promise
the ledger holds 7 event(s)
  [error] credential-stuffing/victim -- 5 x auth.login.failed within 60000ms by 'victim' -- repeated authentication failures against one account
  [error] guess-then-reach/victim -- auth.login.failed then secret.reveal.refused within 68ms by 'victim' -- a failed sign-in followed by a reach for a secret -- the classic shape of a stolen-credential attempt

Two findings for two real stories, from events nobody wrote by hand.

A defect the demo caught: one story, one finding

The first implementation reported that second story five times — each of the five failed logins pairs with the same secret reach, so five pairs matched. Technically correct, operationally useless: a detection that repeats itself per matching pair is how alert fatigue starts, and alert fatigue is how real alerts get ignored.

The fix is one finding per actor per detection, and the guard now pins it so it cannot come back. Worth stating as a rule: a detection's job is not to report every match, it is to report every story.

The corroboration law

One anomalous read is a rumor.

Corroborated() marks a detection that must not raise an error-severity alarm on a single signal. If the matched evidence spans fewer than two distinct event kinds, the finding is emitted as a warning that says so:

lonely-signal -- auth.passkey.clone_suspected occurred (single signal: reported as a warning until a second, independent signal corroborates it)

The sequence shape satisfies corroboration by construction — it spans two kinds by definition — so the same flag leaves it at error. This is the perf system's fifth law (a self-check compares two independent measurements) wearing security clothes.

It fails the same build

Verdicts are findings in the unified rule shape, so no adapter is needed anywhere:

Haro
oRep = new stzRuleReport("nightly-ci")
oRep.Ingest(aFindings)
? "IsSound() = " + oRep.IsSound()

illustration

Sortie promise
IsSound() = 0
Rule report 'nightly-ci': 2 finding(s), 2 error(s), 0 warning(s) -> UNSOUND (errors present)

A tripped detection now fails CI exactly as a capability escalation or a code-rule violation does — one gate over code, agents, security, workflow, orgcharts, and now incidents. And a quiet ledger keeps the gate sound, which the guard also checks: a detector that cannot stay silent is not a detector.

Honest limits

Detections read the retained window — bounded means forgetting, and a campaign slower than the ledger's capacity outruns it unless sealed out (I1's export). Windows use wall time, so a clock adjustment moves them; ordering within the process is monotonic, but cross-process correlation is wall-based by necessity. There is no baseline learning, no frequency modelling, no threat intel: these are declared patterns, which is exactly what makes them reviewable and version-controllable.

I4 gives them a heartbeat — the sentinel that runs detections on a cadence, fires on transitions only, and opens an incident from what fired.

Prouvé par

L'article nomme ces gardes de scénario de la bibliothèque :

Lectures liées

Citer

Mansour Ayouni. « Detection over Sequences ». Softanza narrations, 2026. https://mayouni.github.io/stzsite/fr/narrations/stz-security-detection-narration.html

Non exécuté : son code touche aux fichiers, au réseau, à l'horloge ou au hasard. L'article est publié tel qu'il est écrit ; ses blocs sont des illustrations. Le code des articles est du code Haro. Là où un article a été écrit avant que la langue prenne son nom actuel, le nom a été changé et rien d'autre. Genre, domaine et série lus dans le nom du fichier, année tirée du premier commit : dérivés, jusqu'à ce que l'article porte son propre en-tête. Le fichier sur GitHub(s'ouvre dans un nouvel onglet)