Softanza
StartFrançais
Softanza
Research · narration

The Security Event

Softanza detects a great deal. A WebAuthn signature counter that fails to advance — the textbook cloned-credential signal. A SAML assertion presented twice. An HMAC that does not match. A nonce reused for the same key. A sandboxed actor reaching for an effectful capability.

by Mansour Ayouni · in the repository since 2026-07-31 · series · the security series · area Security & governance

A refusal is an event — typed at birth by the gate that refused, redacted by construction, correlated for free

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

What the library was throwing away

Each of those is detected today, explained honestly to whoever called, and then forgotten: the explanation goes into a Why() slot that the next call overwrites. Nothing is timestamped, so nothing correlates; the second attempt looks exactly like the first. The security doc already names the cure in one line — "A refusal is an event, not a silent failure" — and I0 is that sentence given a shape.

Six questions, closed fields

An event answers what happened, who tried, which thing, how it ended, why, and when — and it gets those facts from the gate that produced them, never from parsing text afterwards:

Haro
oE = StzSecurityEvent("secret.reveal.refused")
oE.ByActor(oLlm).About(oSecret).Doing("reveal").AtRisk(4)
oE.Refused("actor is not effectful")
oE.Show()

illustration

Promised output
Security event secret.reveal.refused [error]
  a secret reveal was refused
  REFUSED secret.reveal.refused by advisor (sandboxed) on <secret 'stripe-live' (secret) from literal> -- actor is not effectful
  at 1785492119737 (wall), 43.51 (mono)
  ATT&CK T1552

Read what arrived without being told: the severity (error) and the ATT&CK technique (T1552, credentials from password stores) came from the closed kind catalog; the actor's name, its sandboxed posture and its capability set came from the stzSystemActor itself; the reason is the gate's own words. Nothing was inferred, and a SIEM will not have to guess any of it back.

Both clocks are stamped at the moment of detection: wall time for the forensic "when", monotonic for ordering that survives an NTP correction (the perf grind's clocks-are-scopes law, inherited).

The redaction law, enforced by construction

An incident record must never become the breach it describes. The secret above was created with a real-looking literal value — and the event carries its descriptor:

Haro
? "the value 'sk_live...' appears in the event: " + (StzFindFirst("sk_live", oE.ToOcsfJson()) > 0)
? "what the subject says instead   : " + oE.Subject()

illustration

Promised output
the value 'sk_live...' appears in the event: 0
what the subject says instead   : <secret 'stripe-live' (secret) from literal>

This is structural, not a convention: About() asks an object for its Descriptor(), and the only way to get a value out of an stzSecret is Reveal(effectfulActor) — which this class never calls. The guard proves the value is absent from the subject, the canonical form, the human line, and the JSON that leaves the process.

Correlation, free

Inside a trace scope — and an observed appserver opens one per request — the event stamps the active trace id, exactly as stzLog does:

Haro
StzOpenTraceScope("")
# ... an event and a log line, written by unrelated code ...
StzCloseTraceScope()

illustration

Promised output
event trace : 881702bc4503da9ab5191f4b91fb5750
log   trace : 881702bc4503da9ab5191f4b91fb5750
same story  : 1

Two objects that know nothing about each other, one identity. That is the backbone the I5 incident timeline will pull on, and it cost nothing to acquire because P9 already built it.

Speaking OCSF from day one

The house interop rule (a native shape and an industry serialization, from the first phase) applies here as it did to spans and metrics. OCSF is the schema modern SIEMs ingest:

Haro
? oEvt.ToOcsfJson()

illustration

Promised output
{"category_uid":6,"class_uid":6003,"time":1785492119740,"severity_id":4,"status_id":2,"message":"REFUSED sig.nonce.replayed by peer-3 (external) on key:billing from 10.0.0.7 -- nonce already used for this key","actor":{"user":{"name":"peer-3"}},"metadata":{"product":{"name":"Softanza","vendor_name":"Softanza"},"version":"1.0.0"},"metadata_trace_id":"881702bc4503da9ab5191f4b91fb5750","unmapped":{"kind":"sig.nonce.replayed","outcome":"refused","reason":"nonce already used for this key","subject":"key:billing","posture":"external","action":"","risk":0,"origin":"10.0.0.7","attack_technique":"T1550"}}

auth.* maps to Authentication (category 3 / class 3002), http.* to HTTP Activity (4 / 4002), everything else to API Activity (6 / 6003); severity and status map to OCSF's own ids. The mapping is deliberately conservative, and everything that does not map cleanly rides in unmapped — which is precisely what that OCSF field is for. Better an honest unmapped than a confidently wrong class id.

Honest limits

The catalog is closed: an unknown kind refuses at construction, with the known kinds listed. That is deliberate for I0 (the names are an API, like http.request.ms), and app-defined kinds are an open question for I3, when detections start referring to kinds by name. Kinds are strings, not :symbols — Haro parses :a.b.c as :a followed by member access, so a dotted catalog name must be quoted.

And the honest scope note: an event that is built and dropped is no better than a Why() slot. I0 only makes remembering possible; I1 gives it a hash-chained ledger to live in, and I2 wires the thirteen seams that currently detect and forget.

Proved by

The article names these scenario guards of the library:

Cite

Mansour Ayouni. "The Security Event". Softanza narrations, 2026. https://mayouni.github.io/stzsite/en/narrations/stz-security-event-narration.html

Not run: its code touches files, the network, the clock or chance. The article is published as written; its blocks are illustrations. The code of the articles is Haro code. Where an article was written before the language took its present name, the name was changed and nothing else. Genre, area and series read from the file's name, the year from its first commit: derived, until the article carries its own header. The file on GitHub(opens in a new tab)