Important
CRA Article 14 (v2.0): The Cyber Resilience Act's active exploitation notification obligations enter into force in September 2026. Wardex v2.0 implements the full path: CISA KEV catalogue correlation, a distinct exit code (12), an HMAC-SHA256 signed notification artefact, and a chained audit entry with the three regulatory deadlines. This path cannot be overridden by risk acceptances.
Wardex is a CLI and Go library that turns security and compliance decisions into auditable evidence. Two independent modes — neither requires the other.
The release gate evaluates each vulnerability in the context of the asset that contains it: system criticality, effective exposure, compensating controls already active. Rather than a static CVSS threshold that blocks everything or nothing, the output is a decision with a timestamped, signed record that survives an audit.
The gap analysis crosses what the security function declared against what is operationally confirmed, mapped against the chosen framework catalogue. The result is not a control list — it is the separation between genuine coverage, what exists only in policy, and what operates outside of governance.
ISO/IEC 27001:2022 · SOC 2 · NIS 2 · DORA · CRA Article 14 · NIST CSF 2.0
wardex assess controls.yaml --framework iso27001 # default
wardex assess controls.yaml --framework nis2
wardex assess controls.yaml --framework dorago install github.com/had-nu/wardex/v2@latestRequires Go ≥ 1.26. Ensure $(go env GOPATH)/bin is in your $PATH.
To build from source:
git clone https://github.com/had-nu/wardex.git
cd wardex && make builddocker pull ghcr.io/had-nu/wardex:2.2.0helm upgrade --install wardex deploy/helm/wardex/ \
--set acceptSecret.value=$(openssl rand -hex 32)See deploy/helm/wardex/ for the full chart reference.
Test Wardex with the example files included in the repository:
# Convert Grype output to Wardex format
wardex convert grype test/usability/grype-results.json > vulns.yaml
# Evaluate with asset context
wardex evaluate \
--evidence vulns.yaml \
--config doc/examples/wardex-config.yaml
# Dry-run — preview without writing artefacts
wardex evaluate --evidence vulns.yaml --config doc/examples/wardex-config.yaml --dry-run
# Exit codes: 0 (ALLOW) · 3 (Tampered) · 4 (Store inconsistent) · 10 (BLOCK) · 11 (Gap) · 12 (Exploited)The EU Cyber Resilience Act's active exploitation notification obligations enter into force in September 2026. Wardex v2.0 implements the Article 14 reporting path.
# Download the CISA KEV catalogue
curl -sSL https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json -o kev-catalogue.json
# Convert Grype output with KEV correlation
wardex convert grype grype-output.json --kev kev-catalogue.jsonWhen a vulnerability is classified as actively exploited (actively_exploited: true), wardex evaluate:
- Exits with code 12 (
ActivelyExploited) — distinct from the normal gate block (10) - Generates an Article 14 notification artefact signed with HMAC-SHA256
- Records a chained audit entry with three CRA deadlines
- Cannot be overridden by risk acceptances
wardex evaluate --evidence vulns.yaml --config wardex-config.yaml frameworks/iso27001/*.ymlwardex art14 list
wardex art14 show <artefact-id>
wardex art14 verify <artefact-id>
wardex art14 mark-dispatched <artefact-id> --phase early-warning
wardex art14 finalize <artefact-id> --patch-date 2026-06-09T12:00:00Zwardex accept active-exploit --cve CVE-2024-3094 --justification "..." --art14-artefact wardex-art14-....jsonExit codes (v2.2): 0 CPL entry verified · 1 CPL MISMATCH/MISSING · 2 CPL Operational error · 3 Integrity failure / Tampered · 4 Store inconsistent · 10 Gate blocked · 11 Compliance fail · 12 Actively exploited
# Compute canonical configuration hash (SHA-256 or BLAKE3)
wardex config hash --config wardex-config.yaml
wardex config hash --config wardex-config.yaml --algorithm blake3
# Verify audit log chain integrity
wardex audit verify-chain --audit-log wardex-gate-audit.log
# Verify hashes in the audit log match archived configurations
wardex audit verify-link --audit-log wardex-gate-audit.log --config-archive ./configs/CPL establishes a cryptographic link between each release gate decision and the configuration in effect at the time. The hash is computed over canonical YAML content (sorted keys, stripped comments, normalized whitespace) — guaranteeing reproducibility across environments. Supports SHA-256 and BLAKE3 (modern algorithm with superior performance). Hashes with different algorithms are distinguished by prefix (sha256:, blake3:) and never silently compared. Divergences between the audit log and archived configurations can be notified via webhook to SIEM.
CPL exit codes: 0 All entries OK · 1 MISMATCH/MISSING detected · 2 Operational error
Wardex compares what infosec has declared against what is operationally active, and identifies the delta against the framework.
Two YAML files with a layer field identifying the origin:
# documented-controls.yaml — policies declared by infosec
- id: CTRL-IAM-001
name: Multi-Factor Authentication
layer: documented
domains: [access_control]
maturity: 4
evidences:
- type: policy
ref: https://wiki.internal/sec/mfa-policy
# implemented-controls.yaml — operationally confirmed controls
# (produced by Bridgr or maintained manually)
- id: CTRL-IAM-001
name: Multi-Factor Authentication
layer: implemented
domains: [access_control]
maturity: 4
effectiveness: 0.90
evidences:
- type: tool
ref: okta-mfa-config-2026The same ID appearing in both files is the expected case: a control that is both declared and confirmed operational. IDs present in only one file are the signal of interest.
wardex assess documented-controls.yaml implemented-controls.yaml \
--framework iso27001 \
-o markdownThe report separates results into four compliance states:
| Category | Meaning |
|---|---|
| Covered | Present in implemented layer, maturity >= 3, with operational evidence. |
| Policy without practice | Documented only. No corresponding implemented control. |
| Practice without governance | Implemented but without a documented policy. |
| Gap | Absent from both layers for a catalogue control. |
The LayerDelta section identifies the real drift between intent (policy) and execution (code), exposing compliance illusions.
If your asset inventory is declared, the report produces a per-asset compliance table:
wardex assess documented-controls.yaml implemented-controls.yaml \
--assets assets.yaml \
--framework iso27001 \
-o json --out-file posture.json# assets.yaml — v2.0.0 Schema
- id: ASSET-PAY-001
name: Payment API
type: application
criticality: 0.9
scope: [iso27001]
controls: [CTRL-IAM-001, CTRL-CRYPTO-002]
exposure:
internet_facing: true
network_zone: dmz
data_classification: restricted
threats:
- id: T-01
scenario: "API abuse"
likelihood: high
owner: platform-teamThe gate evaluates vulnerabilities using the model:
R(v, α) = (CVSS(v)/10) × EPSS(v) × C(α) × E(α) × (1 − Φ(α))
CVSS/10 normalises the base score to [0, 1]; combined with EPSS, the product represents severity weighted by exploitation probability. C is asset criticality, E is effective exposure, and Φ is compensating control effectiveness (clamped at 0.80 — a compensating control reduces risk at most 80%). The final R lies in [0, 1.5]. Thresholds in wardex-config.yaml use the same scale.
The result is compared against the risk_appetite defined in wardex-config.yaml. Three possible outcomes: ALLOW, WARN, BLOCK.
# wardex-config.yaml
release_gate:
enabled: true
risk_appetite: 0.20
warn_above: 0.12
mode: any # "any" blocks if any vuln exceeds threshold; "aggregate" uses sum
asset_context:
criticality: 0.8
internet_facing: true
requires_auth: true
compensating_controls:
- type: waf
effectiveness: 0.35The difference between ALLOW and BLOCK is not the CVE — it is the asset context. R lies in [0, 1.5]; each profile carries a distinct risk_appetite threshold (see data/calibration.json).
| CVE | CVSS | EPSS | [BANK] | [SAAS] | [INFRA] | [HOSP] |
|---|---|---|---|---|---|---|
| Log4Shell | 10.0 | 0.94 | 1.41 BLOCK |
0.75 BLOCK |
1.41 BLOCK |
1.13 BLOCK |
| xz backdoor | 10.0 | 0.86 | 1.29 BLOCK |
0.69 BLOCK |
1.29 BLOCK |
1.03 BLOCK |
| curl SOCKS5 | 9.8 | 0.26 | 0.38 BLOCK |
0.20 WARN |
0.38 BLOCK |
0.31 BLOCK |
| minimist | 9.8 | 0.01 | 0.01 ALLOW |
0.01 ALLOW |
0.01 ALLOW |
0.01 ALLOW |
Calibrated against 237 real CVEs with live EPSS from FIRST.org (data/dataset_2025-03-01.json):
| Profile | Appetite | BLOCK | ALLOW | % Block |
|---|---|---|---|---|
| Tier-1 Bank (DORA) | 0.5 | 176 | 57 | 74% |
| Hospital (HIPAA) | 0.8 | 168 | 63 | 71% |
| SaaS Start-up | 2.0 | 111 | 86 | 47% |
| Energy/Utilities (NIS2) | 0.3 | 180 | 53 | 76% |
When your scanner does not include EPSS, Wardex assumes EPSS 1.0 (worst case) and blocks until explicit validation:
wardex enrich epss wardex-vulns.yaml --output epss-enrich.yaml
wardex evaluate --epss-enrichment epss-enrich.yaml --evidence vulns.yaml controls.yamlEnrichment queries api.first.org and signs the result via HMAC-SHA256.
wardex convert grype results.json > vulns.yaml
wardex convert sbom sbom.xml > vulns.yaml# .github/workflows/wardex-gate.yml
jobs:
risk-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Wardex
run: go install github.com/had-nu/wardex/v2@latest
- name: Evaluate risk gate
run: |
wardex evaluate \
--config .wardex/config.yaml \
--evidence vulns.yaml \
controls.yaml
# Exit 0 = ALLOW, Exit 10 = BLOCK, Exit 11 = compliance gapdocker compose up -d # start PostgreSQL, MinIO, and Wardex API stub
docker compose down # stop everythingWardex ships a docker-compose.yml with PostgreSQL (audit store), MinIO (artefact bucket), and a Wardex API stub for local integration testing. See the compose file for service ports and configuration.
Wardex reads these environment variables at startup:
| Variable | Default | Description |
|---|---|---|
WARDEX_ACCEPT_SECRET |
— | HMAC-SHA256 key for signing acceptances and Art14 artefacts (min 32 chars) |
WARDEX_ACTOR |
cli |
Identity string recorded in audit entries |
WARDEX_SYSLOG_ENDPOINT |
— | tcp://syslog.example.com:514 — forward audit events to central syslog |
WARDEX_SYSLOG_PROTO |
tcp |
Syslog transport: tcp, udp, or tls |
WARDEX_SYSLOG_CERT |
— | TLS client cert path for tls proto |
WARDEX_SYSLOG_KEY |
— | TLS client key path for tls proto |
WARDEX_SYSLOG_CA |
— | Custom CA cert path for tls proto |
Syslog forwarding is opt-in. When WARDEX_SYSLOG_ENDPOINT is set, every gate decision, acceptance, and Art14 lifecycle event is also dispatched as a structured RFC 5424 message to the configured endpoint. The connection is established at startup and reconnected on failure with exponential backoff.
For environments that require certificate-based identity rather than shared secrets:
wardex pki init --org "Your Corp" --validity 3650d
wardex pki issue --name ci-agent --out ci-agent.wex
wardex config seal --keyring ci-agent.wex --input config.yaml --out config.wexstatePKI mode creates an Ed25519 CA and issues short-lived operator certificates. Sealed configs signed with PKI certificates carry the full X.509 chain, enabling automated expiry verification without an external trust store.
When the gate blocks and there is a business case for proceeding, Wardex formalises the exception with a named owner, justification, and TTL. Silent expirations and configuration drift are detected automatically.
# Request acceptance
wardex accept request \
--report report.json \
--cve CVE-2024-1234 \
--accepted-by sec-lead@company.com \
--justification "WAF mitigates the attack vector; patch scheduled for Q3" \
--expiry 90d
# Verify integrity of all active acceptances
wardex accept verify
# Export verification report as JSON artefact
wardex accept verify --output verification-report.json
# List acceptances and status
wardex accept list --activeAcceptances are signed with HMAC-SHA256 and recorded in an append-only log (JSONL). Wardex rejects acceptances that have expired, been tampered with, or whose wardex-config.yaml has drifted since signing.
For DORA compliance and non-repudiable chains of custody, Wardex allows sealing risk policies (wardex-config.yaml) into a signed cryptographic envelope (.wexstate).
- Strong identity: Ed25519 keys for Admins, CISOs, and Analysts.
- Sealed config: Risk policies cannot be altered in CI/CD without executive approval.
- Append-only trust store: Central record of authorised keys and revocations.
# Seal the policy (CISO action)
wardex config seal --keyring ciso.wex --input config.yaml --out config.wexstate
# Evaluate with mandatory seal verification
wardex evaluate --config config.wexstate --evidence vulns.yaml --strictSee the Governance Playbook for the full workflow.
import "github.com/had-nu/wardex/v2/pkg/sdk"
controls, _ := sdk.LoadControls("./controls.yaml")
result, _ := sdk.Analyze(controls, "iso27001")
fmt.Printf("Coverage: %.1f%%\n", result.Summary.GlobalCoverage)For the release gate:
import (
"github.com/had-nu/wardex/v2/pkg/model"
"github.com/had-nu/wardex/v2/pkg/releasegate"
)
gate := releasegate.Gate{
AssetContext: model.AssetContext{
Criticality: 0.9,
InternetFacing: true,
RequiresAuth: true,
},
CompensatingControls: []model.CompensatingControl{
{Type: "waf", Effectiveness: 0.35},
},
RiskAppetite: 0.20,
}
report := gate.Evaluate([]model.Vulnerability{
{CVEID: "CVE-2024-1234", CVSSBase: 9.1, EPSSScore: 0.84, Reachable: true},
})
fmt.Println(report.OverallDecision) // ALLOW | WARN | BLOCK- Architecture and internals
- Business context and the binary gate problem
- Playbook — use cases with full commands
- Governance — Trust Store & Sealed Config Playbook
- GitHub Actions integration
- Helm chart reference
- Exit codes
- Dev environment (docker-compose)
- CHANGELOG
- Contributing
Dual-licensed:
AGPL-3.0 (free): use in internal CI/CD pipelines or in open-source projects that make their source available.
Commercial licence (paid): embedding in proprietary products, SaaS platforms, or distribution without opening source. See the Commercial Terms or contact andre_ataide@proton.me.