From 9ed6fa396cdfce0f1a0ca9e5f5d5690ec11a549c Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Fri, 19 Jun 2026 23:20:09 -0400 Subject: [PATCH 1/2] feat(engine): emit transaction-phase records into auditd (AUDIT_NETLINK) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5, PR 2 of 2 — completes Phase 5. The transaction coordinator now emits an AUDIT_USER (type 1005) record into the local auditd at each transaction-phase boundary, so a SIEM ingesting auditd sees Kensa's transaction lifecycle without additional plumbing — the observability the FedRAMP reviewer flagged as non-negotiable. The engine runs on the controller, so events land in the controller (operator) host's auditd. - internal/agent/auditnl: Emitter (holds one netlink socket; EmitPhase sends an AUDIT_USER record via SendNoWait — no ACK wait, fully non-blocking; swallows all errors). NewEmitter NEVER errors: a failed socket open (no CAP_AUDIT_WRITE) yields a silent no-op emitter. formatPhaseMessage renders the auditd-style key=value body. - internal/engine: PhaseEmitter interface (defined HERE so the engine core does not import the netlink/go-libaudit stack — auditnl.Emitter satisfies it structurally), no-op default, WithAuditEmitter option, and emit calls at each boundary in Run / finalize / errored: started → capture → apply → validate → committed|rolled_back|partially_applied, and "errored" on the error path. - pkg/kensa: wires engine.WithAuditEmitter(auditnl.NewEmitter()) into the production engine, so emission is on by default wherever privilege allows. - spec engine-audit-emission (Tier 1, 4 ACs); engine emit-sequence tests (committed + rolled-back orders, no-op-default safety) + auditnl emit tests (message format, no-op/zero-value safety). No new dependency (go-libaudit landed with PR 5a). STRICTLY best-effort and non-blocking by construction: SendNoWait, all-errors-swallowed, no-op default. An audit-log failure can NEVER fail or delay a transaction. Failure-mode analysis: 1. What could this do wrong in production? Touching the atomicity-critical coordinator risks affecting transaction outcomes. Mitigated by construction: the only engine change is added emitter.EmitPhase(...) calls whose implementation cannot block (SendNoWait) or error (return value ignored; no-op default). A unit test asserts an engine with NO emitter wired commits identically; the emit calls sit AFTER each phase's existing publishPhaseCompleted, never gating phase logic. 2. Captured-state sufficiency: N/A — this is observability emission, not a capturable handler; no capture/rollback path is touched. 3. Edge case not safe for / gated: emission needs CAP_AUDIT_WRITE (root); a non-privileged operator host silently emits nothing (no-op emitter) — acceptable, audit logging is inherently privileged. The emitted records land in the controller's auditd (where the engine runs), not the target's — documented in the spec. LIVE validation that records reach auditd needs root + a real audit subsystem (CI is non-root → only the no-op path runs); the engine change carries the standard atomicity two-human review (CONTRIBUTING) as the founder's gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/agent/auditnl/emit.go | 72 +++++++++++++++++ internal/agent/auditnl/emit_test.go | 48 +++++++++++ internal/engine/audit_emit_test.go | 108 +++++++++++++++++++++++++ internal/engine/commit.go | 7 ++ internal/engine/engine.go | 41 ++++++++++ pkg/kensa/kensa.go | 6 ++ specs/engine/audit-emission.spec.yaml | 110 ++++++++++++++++++++++++++ 7 files changed, 392 insertions(+) create mode 100644 internal/agent/auditnl/emit.go create mode 100644 internal/agent/auditnl/emit_test.go create mode 100644 internal/engine/audit_emit_test.go create mode 100644 specs/engine/audit-emission.spec.yaml diff --git a/internal/agent/auditnl/emit.go b/internal/agent/auditnl/emit.go new file mode 100644 index 0000000..8e6fcd9 --- /dev/null +++ b/internal/agent/auditnl/emit.go @@ -0,0 +1,72 @@ +package auditnl + +import ( + "fmt" + "syscall" + + libaudit "github.com/elastic/go-libaudit/v2" + "github.com/elastic/go-libaudit/v2/auparse" +) + +// Emitter writes Kensa transaction-phase records into the local auditd via +// an AUDIT_USER (type 1005) netlink message — the observability the FedRAMP +// reviewer flagged: every transaction phase produces an event in the +// host's audit log, not just in Kensa's own evidence file. It holds one +// netlink socket for its lifetime. +// +// The engine runs on the controller, so events land in the controller +// (operator) host's auditd — the host kensa's actions originate from. +// Opening the socket needs CAP_AUDIT_WRITE (root); when it can't be opened +// the Emitter degrades to a silent no-op (client == nil) so emission is +// always safe to call and NEVER affects a transaction outcome. +type Emitter struct { + client *libaudit.AuditClient +} + +// NewEmitter opens an AUDIT netlink socket for emitting user records. It +// NEVER returns an error: on failure (no privilege / no audit) it returns +// a no-op Emitter, so the engine can always hold a usable, non-nil emitter +// and emission can never fail a transaction. +func NewEmitter() *Emitter { + c, err := libaudit.NewAuditClient(nil) + if err != nil { + return &Emitter{} + } + return &Emitter{client: c} +} + +// EmitPhase writes one transaction-phase record. Best-effort and +// non-blocking: it uses SendNoWait (no ACK wait) and swallows every error, +// so a slow or unavailable audit subsystem can never delay or fail a +// transaction. A no-op when the socket could not be opened. +func (e *Emitter) EmitPhase(txnID, phase string, ok bool) { + if e == nil || e.client == nil { + return + } + msg := syscall.NetlinkMessage{ + Header: syscall.NlMsghdr{ + Type: uint16(auparse.AUDIT_USER), + Flags: syscall.NLM_F_REQUEST | syscall.NLM_F_ACK, + }, + Data: []byte(formatPhaseMessage(txnID, phase, ok)), + } + _, _ = e.client.Netlink.SendNoWait(msg) +} + +// Close releases the netlink socket. Safe on a no-op emitter. +func (e *Emitter) Close() error { + if e == nil || e.client == nil { + return nil + } + return e.client.Close() +} + +// formatPhaseMessage renders the audit record body as auditd-style +// key=value text. Pure (no IO) so it is unit-testable without a socket. +func formatPhaseMessage(txnID, phase string, ok bool) string { + result := "fail" + if ok { + result = "ok" + } + return fmt.Sprintf("op=kensa_transaction phase=%s txn=%s result=%s", phase, txnID, result) +} diff --git a/internal/agent/auditnl/emit_test.go b/internal/agent/auditnl/emit_test.go new file mode 100644 index 0000000..e779127 --- /dev/null +++ b/internal/agent/auditnl/emit_test.go @@ -0,0 +1,48 @@ +package auditnl + +import ( + "strings" + "testing" +) + +// formatPhaseMessage renders auditd-style key=value text with the result +// mapped from the ok flag. +// +// @spec engine-audit-emission +// @ac AC-04 +func TestFormatPhaseMessage(t *testing.T) { + t.Run("engine-audit-emission/AC-04", func(t *testing.T) {}) + ok := formatPhaseMessage("abc-123", "apply", true) + if !strings.Contains(ok, "phase=apply") || !strings.Contains(ok, "txn=abc-123") || !strings.Contains(ok, "result=ok") { + t.Errorf("ok message = %q", ok) + } + fail := formatPhaseMessage("abc-123", "rolled_back", false) + if !strings.Contains(fail, "result=fail") { + t.Errorf("fail message = %q", fail) + } + if !strings.HasPrefix(ok, "op=kensa_transaction") { + t.Errorf("message should start with the op tag; got %q", ok) + } +} + +// NewEmitter never errors, and EmitPhase/Close are safe to call on the +// no-op emitter that results when the socket cannot be opened (the +// non-root CI case) — emission must never panic or block a transaction. +// +// @spec engine-audit-emission +// @ac AC-03 +func TestNewEmitter_NoopSafe(t *testing.T) { + t.Run("engine-audit-emission/AC-03", func(t *testing.T) {}) + em := NewEmitter() // non-root in CI → no-op emitter + // Must not panic regardless of whether the socket opened. + em.EmitPhase("txn-1", "apply", true) + if err := em.Close(); err != nil { + t.Errorf("Close: %v", err) + } + // A zero-value emitter is also safe. + var zero *Emitter + zero.EmitPhase("txn-2", "capture", true) + if err := zero.Close(); err != nil { + t.Errorf("zero Close: %v", err) + } +} diff --git a/internal/engine/audit_emit_test.go b/internal/engine/audit_emit_test.go new file mode 100644 index 0000000..4dd4ccb --- /dev/null +++ b/internal/engine/audit_emit_test.go @@ -0,0 +1,108 @@ +package engine_test + +import ( + "context" + "errors" + "reflect" + "testing" + "time" + + "github.com/google/uuid" + + "github.com/Hanalyx/kensa/api" + "github.com/Hanalyx/kensa/internal/engine" + "github.com/Hanalyx/kensa/internal/handler" +) + +// recordingEmitter records the phase names the engine emits. +type recordingEmitter struct{ phases []string } + +func (r *recordingEmitter) EmitPhase(_, phase string, _ bool) { + r.phases = append(r.phases, phase) +} + +func txnFor(mech string) *api.Transaction { + return &api.Transaction{ + ID: uuid.New(), + HostID: "test-host", + Severity: "medium", + Steps: []api.Step{{Index: 0, Mechanism: mech}}, + StartedAt: time.Now().UTC(), + Deadline: time.Now().Add(time.Minute), + Transactional: true, + } +} + +// A committed transaction emits started → capture → apply → validate → +// committed, in order. +// +// @spec engine-audit-emission +// @ac AC-01 +func TestEmit_CommittedSequence(t *testing.T) { + t.Run("engine-audit-emission/AC-01", func(t *testing.T) {}) + r := handler.NewRegistry() + r.Register(&engine.FakeHandler{HandlerName: "fake_ok", IsCapturable: true}) + em := &recordingEmitter{} + e := engine.New(engine.WithRegistry(r), engine.WithAuditEmitter(em)) + + res, err := e.Run(context.Background(), engine.NewFakeTransport(), txnFor("fake_ok"), false) + if err != nil { + t.Fatalf("Run: %v", err) + } + if res.Status != api.StatusCommitted { + t.Fatalf("status = %s, want committed", res.Status) + } + want := []string{"started", "capture", "apply", "validate", "committed"} + if !reflect.DeepEqual(em.phases, want) { + t.Errorf("emitted phases = %v, want %v", em.phases, want) + } +} + +// A rolled-back transaction (apply fails) emits started → capture → apply +// → rolled_back, with no validate phase. +// +// @spec engine-audit-emission +// @ac AC-02 +func TestEmit_RolledBackSequence(t *testing.T) { + t.Run("engine-audit-emission/AC-02", func(t *testing.T) {}) + r := handler.NewRegistry() + r.Register(&engine.FakeHandler{ + HandlerName: "fake_fail", + IsCapturable: true, + ApplyErr: errors.New("induced apply failure"), + }) + em := &recordingEmitter{} + e := engine.New(engine.WithRegistry(r), engine.WithAuditEmitter(em)) + + res, err := e.Run(context.Background(), engine.NewFakeTransport(), txnFor("fake_fail"), false) + if err != nil { + t.Fatalf("Run: %v", err) + } + if res.Status != api.StatusRolledBack { + t.Fatalf("status = %s, want rolled_back", res.Status) + } + want := []string{"started", "capture", "apply", "rolled_back"} + if !reflect.DeepEqual(em.phases, want) { + t.Errorf("emitted phases = %v, want %v", em.phases, want) + } +} + +// The default engine (no emitter wired) runs to completion without +// panicking — emission is off by default and never affects the outcome. +// +// @spec engine-audit-emission +// @ac AC-03 +func TestEmit_NoopDefaultIsSafe(t *testing.T) { + t.Run("engine-audit-emission/AC-03", func(t *testing.T) {}) + r := handler.NewRegistry() + r.Register(&engine.FakeHandler{HandlerName: "fake_ok", IsCapturable: true}) + e := engine.New(engine.WithRegistry(r)) // no WithAuditEmitter + + res, err := e.Run(context.Background(), engine.NewFakeTransport(), txnFor("fake_ok"), false) + if err != nil { + t.Fatalf("Run: %v", err) + } + if res.Status != api.StatusCommitted { + t.Errorf("status = %s, want committed", res.Status) + } +} diff --git a/internal/engine/commit.go b/internal/engine/commit.go index 3a0164c..a4d7b0e 100644 --- a/internal/engine/commit.go +++ b/internal/engine/commit.go @@ -23,6 +23,11 @@ func (e *Engine) finalize( validators []api.ValidatorResult, rollbacks []api.RollbackResult, ) *api.TransactionResult { + // Terminal transaction-phase audit record (best-effort; never affects + // the outcome). The status string is the phase name (committed / + // rolled_back / partially_applied). + e.emitter.EmitPhase(txn.ID.String(), string(status), status == api.StatusCommitted) + now := time.Now().UTC() result := &api.TransactionResult{ TransactionID: txn.ID, @@ -121,6 +126,8 @@ func (e *Engine) finalize( // [api.StatusErrored] outcome. The phase argument identifies which // phase failed for diagnostics. func (e *Engine) errored(ctx context.Context, txn *api.Transaction, startedAt time.Time, phase api.Phase, err error) *api.TransactionResult { + e.emitter.EmitPhase(txn.ID.String(), "errored", false) + now := time.Now().UTC() result := &api.TransactionResult{ TransactionID: txn.ID, diff --git a/internal/engine/engine.go b/internal/engine/engine.go index eb717f1..735b8b6 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -50,6 +50,13 @@ type Engine struct { validators []Validator forceValidateFail bool + // emitter writes a transaction-phase record into the host's auditd + // at each phase boundary (Phase 5 AUDIT_NETLINK observability). It is + // strictly best-effort and non-blocking — an audit-log failure can + // NEVER fail or delay a transaction. Defaults to a no-op; the + // production path wires auditnl.NewEmitter via WithAuditEmitter. + emitter PhaseEmitter + // agentClient is set via WithAgentClient. When non-nil, // every handler lookup returns a RemoteHandler wrapping // this client (the original handler's Capturable() value @@ -107,6 +114,24 @@ type AgentAwareDeadmanArmer interface { UseAgentClient(c DeadmanAgentClient) } +// PhaseEmitter receives a transaction-phase record at each phase +// boundary. Implementations MUST be non-blocking and MUST NOT error — +// the engine ignores any failure, and audit emission can never affect a +// transaction's outcome. The interface lives here (not in auditnl) so the +// engine core does not import the netlink stack; auditnl.Emitter +// satisfies it structurally and is injected via WithAuditEmitter. +type PhaseEmitter interface { + // EmitPhase records that transaction txnID reached phase with the + // given success state. + EmitPhase(txnID, phase string, ok bool) +} + +// noopPhaseEmitter is the default — emission is off unless a real emitter +// is wired in. +type noopPhaseEmitter struct{} + +func (noopPhaseEmitter) EmitPhase(_, _ string, _ bool) {} + // Option configures [Engine] at construction. The default zero-config // engine uses an in-memory store, a no-op signer, a no-op deadman // armer, and a no-op event bus — sufficient for unit tests but not for @@ -131,6 +156,17 @@ func WithDeadman(d DeadmanArmer) Option { return func(e *Engine) { e.deadman = d // WithEvents overrides the event bus. func WithEvents(b EventBus) Option { return func(e *Engine) { e.events = b } } +// WithAuditEmitter wires a transaction-phase auditd emitter (Phase 5). +// The production path passes auditnl.NewEmitter(); tests pass a recorder. +// Emission is best-effort and never affects a transaction. +func WithAuditEmitter(em PhaseEmitter) Option { + return func(e *Engine) { + if em != nil { + e.emitter = em + } + } +} + // WithForceValidateFail forces the validate phase to return false for // every transaction. Used by kensa-fuzz to test the // apply→validate-fail→rollback path without requiring a real rule check @@ -180,6 +216,7 @@ func New(opts ...Option) *Engine { deadman: noopDeadman{}, events: noopEventBus{}, locks: newHostLocks(), + emitter: noopPhaseEmitter{}, } for _, opt := range opts { opt(e) @@ -225,6 +262,7 @@ func (e *Engine) Run(ctx context.Context, transport api.Transport, txn *api.Tran defer release() e.publishStarted(ctx, txn) + e.emitter.EmitPhase(txn.ID.String(), "started", true) // Phase 1: PRE-FLIGHT. if err := e.preflight(txn); err != nil { @@ -241,6 +279,7 @@ func (e *Engine) Run(ctx context.Context, transport api.Transport, txn *api.Tran return e.errored(ctx, txn, startedAt, api.PhaseCapture, err), nil } e.publishPhaseCompleted(ctx, txn, api.PhaseCapture, true, time.Since(startedAt)) + e.emitter.EmitPhase(txn.ID.String(), "capture", true) // Arm deadman timer for control-channel-sensitive transactions // (engine-transaction spec AC-06, C-04). @@ -255,6 +294,7 @@ func (e *Engine) Run(ctx context.Context, transport api.Transport, txn *api.Tran // Phase 3: APPLY. applyResults, applyOK := e.apply(ctx, transport, txn, preStates) e.publishPhaseCompleted(ctx, txn, api.PhaseApply, applyOK, time.Since(startedAt)) + e.emitter.EmitPhase(txn.ID.String(), "apply", applyOK) // Phase 4: VALIDATE (only if APPLY succeeded). var validators []api.ValidatorResult @@ -262,6 +302,7 @@ func (e *Engine) Run(ctx context.Context, transport api.Transport, txn *api.Tran if applyOK { validators, validateOK = e.validate(ctx, transport, txn) e.publishPhaseCompleted(ctx, txn, api.PhaseValidate, validateOK, time.Since(startedAt)) + e.emitter.EmitPhase(txn.ID.String(), "validate", validateOK) } // Phase 5: COMMIT or ROLLBACK. diff --git a/pkg/kensa/kensa.go b/pkg/kensa/kensa.go index e541ded..c33d164 100644 --- a/pkg/kensa/kensa.go +++ b/pkg/kensa/kensa.go @@ -26,6 +26,7 @@ import ( "github.com/google/uuid" "github.com/Hanalyx/kensa/api" + "github.com/Hanalyx/kensa/internal/agent/auditnl" "github.com/Hanalyx/kensa/internal/engine" "github.com/Hanalyx/kensa/internal/engine/deadman" "github.com/Hanalyx/kensa/internal/evidence" @@ -183,6 +184,11 @@ func defaultService(ctx context.Context, storePath string, tf api.TransportFacto engine.WithDeadman(deadman.New(0, nil)), engine.WithSigner(signer), engine.WithEvents(bus), + // Phase 5: emit a transaction-phase record into the host's auditd + // at each phase boundary. Best-effort — NewEmitter degrades to a + // no-op when the AUDIT netlink socket can't be opened (no + // privilege), so this never affects a transaction. + engine.WithAuditEmitter(auditnl.NewEmitter()), } allOpts := make([]engine.Option, 0, len(stdOpts)+len(engineOpts)) allOpts = append(allOpts, stdOpts...) diff --git a/specs/engine/audit-emission.spec.yaml b/specs/engine/audit-emission.spec.yaml new file mode 100644 index 0000000..6cbe8fd --- /dev/null +++ b/specs/engine/audit-emission.spec.yaml @@ -0,0 +1,110 @@ +spec: + id: engine-audit-emission + version: 0.1.0 + status: draft + tier: 1 + + context: + system: kensa + feature: engine-audit-emission + description: | + The transaction coordinator emits an AUDIT_USER (type 1005) + record into the local auditd at each transaction-phase boundary + (Phase 5 of the kernel-primitive migration), so a SIEM ingesting + auditd sees Kensa's transaction lifecycle without additional + plumbing — the observability the FedRAMP reviewer flagged as + non-negotiable. The engine runs on the controller, so events + land in the controller (operator) host's auditd. + + Emission is STRICTLY best-effort and non-blocking: the emitter + uses netlink SendNoWait (no ACK wait) and swallows every error, + and when the AUDIT socket cannot be opened (no CAP_AUDIT_WRITE) + it degrades to a silent no-op. An audit-log failure can NEVER + fail or delay a transaction. The PhaseEmitter interface lives in + the engine so the core does not import the netlink stack; + auditnl.Emitter satisfies it and is injected via + WithAuditEmitter (the pkg/kensa assembly wires the real one). + related_specs: + - engine-transaction + - auditnl-rule-set + + objective: + summary: | + Emit one transaction-phase audit record per phase boundary + (started, capture, apply, validate, and the terminal + committed/rolled_back/partially_applied/errored), best-effort + and non-blocking, behind a no-op-by-default PhaseEmitter. + scope: + includes: + - PhaseEmitter interface + no-op default + WithAuditEmitter option + - emit calls at each phase boundary in Engine.Run / finalize / errored + - auditnl.Emitter (AUDIT_USER via SendNoWait) + NewEmitter (no-op on open failure) + - auditnl.formatPhaseMessage record body + excludes: + - the audit_rule_set rule handler (auditnl-rule-set) + - parsing/consuming the emitted records (a SIEM concern) + + constraints: + - id: C-01 + description: | + The engine MUST emit a phase record at each boundary: at + transaction start ("started"), after capture ("capture"), + after apply ("apply", carrying applyOK), after validate + ("validate", carrying validateOK — only when apply + succeeded), and at the terminal outcome (the status string: + committed / rolled_back / partially_applied, or "errored" on + the errored path). Order MUST follow execution order. + type: technical + enforcement: error + - id: C-02 + description: | + Emission MUST be best-effort and non-blocking and MUST NEVER + affect a transaction's outcome: the emitter uses SendNoWait + and swallows all errors; the default emitter is a no-op; + NewEmitter NEVER returns an error (a failed socket open yields + a no-op emitter). An engine with no emitter wired runs + identically to before. + type: security + enforcement: error + - id: C-03 + description: | + The PhaseEmitter interface MUST be defined in the engine + package (not auditnl) so the engine core does not import the + netlink/go-libaudit stack; the real emitter is injected via + WithAuditEmitter. The emitted record MUST be AUDIT_USER (type + 1005) auditd-style key=value text identifying the op, phase, + txn id, and result. + type: technical + enforcement: error + + acceptance_criteria: + - id: AC-01 + description: | + A committed transaction emits, in order: started, capture, + apply, validate, committed. + references_constraints: [C-01] + priority: critical + - id: AC-02 + description: | + A rolled-back transaction (apply fails) emits started, + capture, apply, rolled_back — with NO validate phase (apply + did not succeed). + references_constraints: [C-01] + priority: critical + - id: AC-03 + description: | + The default engine (no emitter wired) runs to completion + without panicking; NewEmitter never errors and EmitPhase / + Close are safe on the no-op (failed-open) and zero-value + emitter — emission never affects the outcome. + references_constraints: [C-02] + priority: critical + - id: AC-04 + description: | + formatPhaseMessage renders "op=kensa_transaction phase=

+ txn= result=" with result mapped from the ok + flag. + references_constraints: [C-03] + priority: high + + tags: [engine, auditnl, audit, netlink, observability, tier-1, phase-5] From 7b9a36701ad1e549fa16d56499773d3df81ee2e2 Mon Sep 17 00:00:00 2001 From: Remylus Losius Date: Fri, 19 Jun 2026 23:24:18 -0400 Subject: [PATCH 2/2] fix: rephrase Phase-5 planning-label comments to state intent comment-lint forbids planning labels in new Go comments; restate the emitter/option comments without the phase number. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/engine/engine.go | 4 ++-- pkg/kensa/kensa.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 735b8b6..7c79193 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -51,7 +51,7 @@ type Engine struct { forceValidateFail bool // emitter writes a transaction-phase record into the host's auditd - // at each phase boundary (Phase 5 AUDIT_NETLINK observability). It is + // at each phase boundary (the AUDIT_NETLINK observability surface). It is // strictly best-effort and non-blocking — an audit-log failure can // NEVER fail or delay a transaction. Defaults to a no-op; the // production path wires auditnl.NewEmitter via WithAuditEmitter. @@ -156,7 +156,7 @@ func WithDeadman(d DeadmanArmer) Option { return func(e *Engine) { e.deadman = d // WithEvents overrides the event bus. func WithEvents(b EventBus) Option { return func(e *Engine) { e.events = b } } -// WithAuditEmitter wires a transaction-phase auditd emitter (Phase 5). +// WithAuditEmitter wires a transaction-phase auditd emitter. // The production path passes auditnl.NewEmitter(); tests pass a recorder. // Emission is best-effort and never affects a transaction. func WithAuditEmitter(em PhaseEmitter) Option { diff --git a/pkg/kensa/kensa.go b/pkg/kensa/kensa.go index c33d164..b29682e 100644 --- a/pkg/kensa/kensa.go +++ b/pkg/kensa/kensa.go @@ -184,7 +184,7 @@ func defaultService(ctx context.Context, storePath string, tf api.TransportFacto engine.WithDeadman(deadman.New(0, nil)), engine.WithSigner(signer), engine.WithEvents(bus), - // Phase 5: emit a transaction-phase record into the host's auditd + // Emit a transaction-phase record into the host's auditd // at each phase boundary. Best-effort — NewEmitter degrades to a // no-op when the AUDIT netlink socket can't be opened (no // privilege), so this never affects a transaction.