André Ataíde
June 26, 2026

Wardex v2.2.1: Explicit Rejection Logging and Persistent State Store

Completing the CRA-ready auditability layer — silent failures become structured logs, and cross-execution memory arrives with BLAKE3 hash chain integrity.

Context

Wardex v2.2 introduced the Configuration Provenance Link (CPL) — cryptographic binding of every release gate decision to the exact configuration that produced it. But auditability has two dimensions: what happened to the config (CPL covers this) and what happened during evaluation (v2.2 did not). Rejections were silent. EPSS score fetch failures were swallowed. Acceptance load errors produced no output. A CRA Article 14 auditor inspecting a wardex run could see the final decision but not the reasoning trail of filtered inputs, skipped artefacts, or degraded data sources.

v2.2.1 closes this gap. It also introduces a persistent state store that gives Wardex cross-execution memory — the ability to track risk trends, record decisions, and verify its own history with cryptographic integrity.

What changed

Explicit Rejection Logging

All 26 previously silent rejection behaviors now produce structured stderr messages. A new unified logging package (pkg/ui/logging.go) provides colored, TTY-aware output with severity prefixes:

PrefixColorMeaning
[FAIL]Red+BoldFatal error
[WARN]YellowOperational warning
[INFO]CyanContextual information
[HINT]CyanResolution suggestion
[REJECT]Red+BoldDenied acceptance

Critical path changes:

Acceptance loadingaccept.Load() now accepts an io.Writer parameter and logs every rejected acceptance (expired, tampered, config-changed, report-mismatch) as [REJECT]. Previously, a team running wardex evaluate against a stale acceptance would see a generic failure. Now they see exactly why: [REJECT] acceptance expired: valid_until 2026-06-20, current 2026-06-26.

EPSS score fetchingepss.FetchScores() logs malformed scores and out-of-range values as [WARN] instead of silently dropping them. When EPSS data is degraded, the operator knows the risk score may be less precise.

SBOM processing — CycloneDX logs empty component IDs and missing CVSS scores. The Grype converter tracks and logs empty/duplicate CVE skips. OpenVEX logs unrecognized state drops. Each skipped artefact is now visible in the output.

Policy and trust operations — The trust store logs admin key decode failures. The policy loader logs new file creation. The maturity scorer logs unknown domains. The analyzer gap checker logs nonexistent control references.

Breaking change: accept.Load() and epss.FetchScores() signatures changed to accept io.Writer. All callers have been updated.

Persistent State Store

A new pkg/statestore/ package (10 files, ~1,200 lines) provides cross-execution memory with BLAKE3 hash chain integrity and optional WORM (Write Once Read Many) protection. Every decision Wardex makes is recorded, chained, and verifiable.

The state store records three types of data: individual decisions (CVE blocked/allowed, risk score, timestamp), trend points (aggregated risk over time), and chain entries (BLAKE3 hashes linking each record to its predecessor). The chain is tamper-evident — modifying any past record breaks the hash chain and is detectable via wardex state verify.

New CLI commands:

$ wardex state status
State store: enabled
Chain integrity: VERIFIED
Records: 847
Retention: 90 days

$ wardex state history
DATE       CVE            SCORE  DECISION  HASH
2026-06-26 CVE-2024-3094  9.8    BLOCKED   a1b2c3...
2026-06-26 CVE-2024-21626 7.5    BLOCKED   d4e5f6...
2026-06-25 CVE-2024-1234  3.2    ALLOWED   g7h8i9...

$ wardex state trend
Risk trend (last 30 days): ▃▅▇▆▅▃▂▁▁▂▃▄▅▆▇▇▆▅▃▂▁▁
Direction: DECREASING (-12.3%)

$ wardex state dashboard
┌─────────────────────────────────────────┐
│  State Store Dashboard                  │
├─────────────────────────────────────────┤
│  Chain status:    VERIFIED              │
│  Total records:   847                   │
│  Blocked:         234                   │
│  Allowed:         613                   │
│  Avg risk score:  4.2                   │
│  Trend:           DECREASING            │
│  WORM protection: ENABLED               │
└─────────────────────────────────────────┘

WORM protection uses platform-native immutable file attributes: FS_IMMUTABLE_FL via ioctl on Linux, UF_IMMUTABLE on macOS, and FILE_ATTRIBUTE_READONLY on Windows. When enabled, even root cannot modify historical state records without explicitly disabling WORM first.

Configuration in wardex-config.yaml:

state_store:
  enabled: true
  dir: .wardex
  retention_days: 90
  worm: true

The state store integrates with the evaluation engine — when state_store.enabled=true, every wardex evaluate run records the decision. The --trend flag appends a trend analysis to the output.

Security Hardening

v2.2.1 removes weak cryptographic material that lingered in git history. The HMAC fallback using "REDACTED_WEAK_SECRET_REMOVED" has been excised via filter-repo. Real crypto keys in the your-org/ directory have been replaced with templates. The .gitignore now excludes *.pem, *.key, *.p12, .env, .wardex/, *.wexstate, your-org/, wardex-trust.yaml, and wardex.keyring.

Documentation and Architecture

New architecture documentation includes a 724-line engineering blueprint (doc/architecture/ENGINEERING_BLUEPRINT.md) covering the state store design, and a navigable Mermaid diagram set (doc/architecture/diagrams.html) with 8 architectural views. The technical view document has been extended with a persistent state store section.

Integration patterns

CRA-compliant audit pipeline

Combined with v2.2's CPL, v2.2.1 enables a complete audit pipeline for CRA Article 14 compliance. The config hash binds decisions to policy; the rejection log explains filtering; the state store records the full history; and the BLAKE3 chain proves nothing was tampered with:

# .github/workflows/cra-audit.yml
on:
  schedule:
    - cron: '0 6 * * 1'
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: wardex audit verify-chain \
          --audit-log wardex-gate-audit.log
      - run: wardex state verify
      - run: wardex state dashboard

Risk trend monitoring

The trend analysis enables teams to track whether their risk posture is improving or degrading over time. The sparkline output is designed for terminal dashboards and can be piped to monitoring systems:

# Record trend point after each evaluation
wardex evaluate --config wardex-config.yaml \
    --sbom ./sbom.json --trend

# Weekly trend report
wardex state trend --days 7

WORM-protected compliance archives

For regulated environments where audit records must be immutable, enabling WORM ensures that historical state cannot be altered — not by accident, not by malice, not even by a compromised root account. The wardex state cleanup command respects WORM: it can only remove records whose retention period has expired, and only when WORM is explicitly disabled for the cleanup operation.

Architectural choices

Structured logging over ad-hoc fmt.Printf. A unified logging package ensures consistent formatting, color coding, and severity classification across all 26 rejection points. The io.Writer interface allows test capture without TTY coupling. This is not cosmetic — structured logs are machine-parseable, enabling SIEM ingestion of the evaluation reasoning trail.

BLAKE3 hash chain over Merkle tree. A linear hash chain is simpler to verify, cheaper to append, and sufficient for the write-once-read-many access pattern of audit state. Merkle trees offer efficient partial verification for large datasets — Wardex state records are small enough that full-chain verification is instant.

WORM as platform-native ioctl, not filesystem layer. Using FS_IMMUTABLE_FL and UF_IMMUTABLE directly avoids depending on filesystem-specific features (ZFS snapshots, Btrfs cow). The tradeoff is that WORM must be explicitly managed per-file rather than inherited from directory attributes — acceptable for a state store with controlled write paths.

Opt-in state store, not mandatory. Teams that do not need cross-execution memory can leave state_store.enabled=false. The evaluation engine works identically with or without state recording. This preserves the zero-config deployment model while offering audit depth for regulated environments.

Breaking accept.Load() signature for correctness. Changing the function signature to accept io.Writer is a breaking API change. The alternative — a global logger or package-level configuration — would couple acceptance validation to UI concerns. The explicit writer parameter keeps the dependency graph clean and makes test verification trivial.

The EU Cyber Resilience Act (CRA) Article 14 requires manufacturers to maintain audit trails for automated compliance decisions. Combined with CPL from v2.2, the explicit logging and persistent state store in v2.2.1 provide the cryptographic and operational substrate for demonstrating that every deployment gate decision was traceable, explainable, and untampered.