veckl
Semantic diffs for STIG checklists. veckl parses DISA STIG checklist files β CKL (STIG Viewer 2) and CKLB (STIG Viewer 3), such as those produced by Evaluate-STIG β and tells you what actually changed between scan cycles:
π¨ 1 new CAT I open finding. 2 new open total, 2 remediated.
Instead of eyeballing two checklists side by side in STIG Viewer, or trying to read a raw diff of CKL XML, you get a review-ready report: new opens, remediations, status moves, added/removed checks, and benchmark version bumps β as markdown for humans (and PR comments) or JSON for machines.
Why
STIG checklists are compliance artifacts that change every scan cycle, but the file formats fight every tool teams already use for change review. CKL is a wall of XML; CKLB is deeply nested JSON; neither produces a readable git diff. So the "what changed since last cycle?" question gets answered by hand, badly, or not at all.
veckl makes checklists behave like code: keep them in git, and let CI tell reviewers what moved.
Install
go install github.com/hinsonct/veckl/cmd/veckl@latest
Or grab a prebuilt binary from releases. Single static binary β nothing to install in your airgap beyond the file itself.
Usage
# Human-readable markdown (default) β paste into a PR or pipeline summary
veckl diff last-cycle.ckl this-cycle.ckl
# Machine-readable JSON with a stable schema
veckl diff last-cycle.cklb this-cycle.cklb --format json
# CI gate: print the report, then exit 1 if anything newly opened
veckl diff last-cycle.cklb this-cycle.cklb --fail-on new-open,new-cat1
# Export a checklist as OSCAL assessment-results (JSON, OSCAL 1.1.3)
veckl export this-cycle.cklb > assessment-results.json
# Validate an Evaluate-STIG answer file against the latest checklist
veckl answers answerfiles/RHEL_9_STIG.xml this-cycle.cklb --fail-on orphaned,stale
CKL and CKLB are auto-detected from content, and Combined checklists (multiple STIGs per file) are supported. Comparing files from different hosts produces a prominent warning rather than a silent garbage diff.
What the diff reports
| Category |
Meaning |
| π΄ Regression |
any status β Open |
| β
Remediated |
Open β NotAFinding / Not_Applicable |
| π Status change |
other transitions (e.g. Not_Reviewed β NotAFinding) |
| β / β Added / removed |
checks that appeared or disappeared (usually a benchmark revision) |
| π Text change |
finding details / comments edited with no status change |
When the STIG benchmark version itself changed between cycles, veckl says so up front β churn from a STIG revision reads very differently than churn from your systems drifting.
CI gate
--fail-on turns the diff into a pipeline gate. The report always prints first; the gate only decides the exit code, so CI logs show exactly what tripped it.
| Condition |
Fails when |
new-open |
any finding newly opened this cycle (regression or added-as-Open) |
new-cat1 |
a CAT I finding newly opened this cycle |
any-open |
any finding is open in the new checklist |
any-cat1 |
any CAT I finding is open in the new checklist |
Exit codes: 0 clean, 1 a --fail-on condition tripped, 2 veckl itself failed (unparseable file, bad flag) β so pipelines can distinguish "compliance regressed" from "tool broke".
GitHub Actions
This repo ships a composite action that runs the diff, publishes the report to the job summary, and applies the gate:
- uses: hinsonct/veckl@v0
with:
old: checklists/last-cycle.cklb
new: checklists/this-cycle.cklb
fail-on: new-open,new-cat1
Outputs report (markdown) and json are available to later steps β e.g. to post the report as a PR comment.
GitLab CI
See examples/gitlab-ci.yml for a job that gates the pipeline and keeps the markdown report as an artifact.
OSCAL export
veckl export <checklist> converts a checklist into an OSCAL assessment-results document (JSON, OSCAL 1.1.3): each STIG becomes a result, each check becomes an observation plus a finding whose objective status is satisfied (Not a Finding / Not Applicable) or not-satisfied (Open / Not Reviewed), with the original status and severity preserved as props.
UUIDs are deterministic (UUIDv5 over the document content), so exporting the same checklist twice produces byte-identical output β OSCAL artifacts stay diffable in git just like the checklists they came from. Generated documents are validated against the official OSCAL JSON schema in this repo's test suite.
Answer-file validation
Evaluate-STIG answer files automate check responses across scan cycles, and they rot silently. veckl answers <answer-file> <checklist> flags:
- Orphaned entries β the check no longer exists in the benchmark (removed or renumbered by a STIG revision)
- Stale entries β the finding's current status matches none of the statuses the answer can produce, so the entry no longer fires (the system drifted, or the scan ran without the answer file)
Answer keys for other hosts are skipped, DEFAULT keys always apply, and comment-only entries are never flagged. --fail-on orphaned,stale gates CI with the same exit-code convention as diff.
Verifying releases
Release checksums are signed with cosign keyless signing using the release workflow's GitHub OIDC identity β no key to distribute, no key to leak. To verify a download (substitute the version):
VER=0.1.0
cosign verify-blob \
--certificate "veckl_${VER}_checksums.txt.pem" \
--signature "veckl_${VER}_checksums.txt.sig" \
--certificate-identity "https://github.com/hinsonct/veckl/.github/workflows/release.yml@refs/tags/v${VER}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"veckl_${VER}_checksums.txt"
sha256sum -c "veckl_${VER}_checksums.txt" --ignore-missing
The first command proves the checksum file was produced by this repository's release workflow for that tag; the second proves your archive matches it.
Airgapped delivery (Zarf)
Zarf users can package veckl for transfer across an airgap boundary. On a connected machine:
zarf package create packaging/zarf --set VERSION=0.1.0 --set ARCH=amd64 --confirm
Move the resulting zarf-package-veckl-*.tar.zst across the boundary, then on the inside:
zarf package deploy zarf-package-veckl-*.tar.zst --confirm
This installs the binary to /usr/local/bin/veckl. (Zarf is optional β veckl is a single static binary, so copying the file works too; the package just makes the delivery declarative and checksummed.)
- Evaluate-STIG is the intended producer: it scans your systems and emits CKL/CKLB; veckl diffs consecutive outputs.
- STIG Manager is a database-backed web application for managing STIG assessments across an organization. veckl is deliberately not that: it's a stateless CLI for git- and CI-centric workflows, and works fine alongside STIG Manager.
Roadmap
- CI gate:
--fail-on new-open,new-cat1 exit codes + a GitHub Action and GitLab CI examples
- OSCAL export: emit OSCAL
assessment-results (JSON, OSCAL 1.1.3), schema-validated in tests
- Evaluate-STIG answer-file validation: flag stale or orphaned answer-file entries
- Zarf package for airgapped delivery
- Signed releases (cosign)
Development
go test ./...
go build ./cmd/veckl
Test fixtures in testdata/ are fully synthetic: public DISA RHEL 9 STIG identifiers with fabricated hosts and results. No real scan data is (or ever will be) in this repository.
License
Apache-2.0