The NIS2-audit (NIS2 security-operations) event emitter for eIDAS signing services. One standard way for every service to emit structured security events — auth failures, authZ/IDOR denials, DPoP/proof failures, egress/NetworkPolicy violations, secret/key access, privileged/admin actions, and "first-awareness" incident detections — to the SIEM / central log management, with high-precision synced timestamps so the NIS2 24 h / 72 h / 1 month reporting clock is defensible.
Scope: this library targets Azugo services — its entrypoints take
*azugo.Context by design, and it is versioned in lockstep with the Azugo-based platform
kit. DataSubjects values must be pseudonymous internal identity references, never
national identifiers, names, or e-mail addresses.
Events are the frozen broker.Envelope tagged security, stamped with a ULID id,
a high-precision occurrence time, and the request's correlation/trace ids. A pluggable
Sink decides where they go:
| Sink | Destination |
|---|---|
LogSink |
structured log lines on the request logger → the log pipeline ships them to the SIEM (the common path) |
BrokerSink |
the broker event stream → fans into the SIEM |
A scheduled sweep, a retention purge or an outbox drainer has no request to borrow a logger or
correlation ids from. EmitBackground is the entry point for those: same tagging, sanitizing,
stamping and validation, minus the correlation ids there is nothing to take.
// A log sink that serves BOTH paths needs a logger of its own for the background one.
audit := secevents.NewEmitter(secevents.NewLogSinkFor(app.Log()))
// From a scheduled task:
_ = audit.EmitBackground(ctx, &broker.Envelope{
EventType: "document.retention_swept",
Operation: broker.OpDelete,
Outcome: broker.OutcomeSuccess,
Attributes: map[string]any{secevents.AttrSeverity: string(secevents.SeverityInfo), "erased": 4},
})The sink must implement BackgroundSink — LogSink (built with NewLogSinkFor) and BrokerSink
both do. It is a separate interface from Sink so adding it broke no existing implementation; one
that does not implement it is told so rather than having its event dropped.
Do not hand-roll this. Writing the sink's log line yourself is what the absence of this entry
point used to force, and it loses the token stripping Stamp performs — which is the part that
keeps a credential out of the security stream.
go get github.com/gmb-lib/go-sec-eventsSee CHANGELOG.md for what each release changed, and what it means for code that
already uses this library, before you bump.
import "github.com/gmb-lib/go-sec-events/secevents"
// LogSink: events become structured log lines the platform ships to the SIEM.
audit := secevents.NewEmitter(secevents.NewLogSink())
// …or BrokerSink where the SIEM ingests from the broker:
// audit := secevents.NewEmitter(secevents.NewBrokerSink(pub, secevents.DefaultTopic))Emit with the typed helpers — severity, category and the lean attribute shape are fixed for you, and severity maps to the log level so SIEM alerting works without parsing:
// In an authZ middleware / handler:
audit.AuthZDenied(ctx, secevents.Denial{
Actor: broker.Actor{ID: ctx.User().ID(), Type: "user"},
Resource: broker.Resource{Type: "document", ID: id},
RequiredScope: "documents:read",
Reason: "object not owned by caller",
IDOR: true, // raises severity to high
})
// On a network-policy / egress alarm:
audit.EgressViolation(ctx, secevents.Egress{Target: host, Policy: "default-deny"})FirstAwareness records the moment a possible significant incident is detected and
returns the high-precision occurrence time, so the caller can anchor the 24-hour clock
and the incident register:
anchor, err := audit.FirstAwareness(ctx, secevents.Detection{
Severity: secevents.SeverityCritical,
Summary: "anomalous egress to unknown host",
IncidentRef: "INC-2026-014",
// DetectedAt: detectorTimestamp, // optional; defaults to now
})
// persist `anchor` as "when we first became aware" in the incident register| Helper | event_type |
Default severity |
|---|---|---|
Authentication |
auth.login |
info / warning on failure |
AuthZDenied |
authz.denied |
warning / high on IDOR |
ServiceTokenFailure |
auth.service_token |
warning / high on replay·proof |
EdgeBlock |
edge.block |
warning |
EgressViolation |
egress.violation |
high |
SigningEgressCall |
egress.signing |
info / warning on failure |
SecretAccess |
secret.access |
info / high on failure |
DataTierAnomaly |
data.anomaly |
high |
ConfigChange |
config.change |
warning |
PrivilegedAccess |
access.privileged |
high |
FirstAwareness |
incident.first_awareness |
high |
PII posture. Security events are metadata only — identifiers and bounded operational metadata (string attribute values are truncated to
MaxAttrValueLen(256) runes, soReason/Detail/Summaryare ticket-style references, not narratives). The emitter strips free-text/content attribute keys defensively, and the publisher strips bearer-token-shaped keys; pseudonymise actors where possible. A privileged/break-glass access is also a GDPR access — emit the GDPR-audit record viago-gdpr-audittoo.
go build ./...
go test ./...
go vet ./...Bug reports and pull requests are welcome. CONTRIBUTING.md names the gate a change has to pass, what a change to this library needs, and the sign-off every commit carries.
Suspected vulnerabilities go through the private route in SECURITY.md — never a public issue.
MIT — see LICENSE.