diff --git a/CHANGELOG.md b/CHANGELOG.md index cec63698..caf290e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,25 @@ The CLI is governed by GNU/POSIX conventions. Long-form flags are the canonical names; short forms are listed in `cmd/kensa/flags.go`. Compare any two releases at - (swap the tags for + (swap the tags for any pair). ## Unreleased +## v0.11.0 (2026-09-20) + +Two engine corrections lead this release, both to what Kensa reports rather +than to what it does on a host. Evidence records are signed over their final +contents, so `kensa verify` accepts records it used to reject. And a +remediation step that fails partway now reports the host as unconfirmed +instead of restored, which OpenWatch shows as "Partially applied" with an +inspection prompt where it used to show "Reverted, host unchanged". Expect +more of those prompts: a handler that failed without changing anything reports +the same way, because the engine has no evidence either way. The rest is two +CLI additions, a CLI change announced since v0.1, and a supply-chain +hardening pass over CI. The frozen `api/` package gains no field and loses +none; OpenWatch confirmed no consumer change is needed. + ### Changed - **CI builds specs with Specter v0.15.0**, up from v0.13.2, and verifies the @@ -179,6 +193,27 @@ any pair). fetches read a public repository anonymously, everything else is a local read, and the release upload authenticates with its own step-scoped token. +### Known limits + +- Two kinds of evidence from earlier versions fail verification, and neither + is repaired or re-signed, because rewriting them would mean signing evidence + after the fact. **Stored records** of a partially applied transaction with a + stranded step: those were persisted with a signature that no longer matched + their contents. **Returned envelopes** handed back to the caller after a + committed, rolled-back or staged result was demoted because its persistence + failed: the engine rewrote the decision inside a signed envelope. Those + envelopes were not stored by Kensa, since storing them is what had failed; + whether a caller kept one is not something Kensa can establish. A + persistence failure that did not demote the result left the signature + intact. Records on every other path are unaffected. +- The v1 evidence envelope serializes `rollback_results` but does not cover it + with the signature. Editing that field in a stored record does not + invalidate verification. Authenticating it changes the signed bytes and needs + a new schema version that a verifier can tell apart from v1; that work is + tracked and not in this release. +- Both engine corrections were verified offline, with isolated stores and real + signatures, not on a live host. + ## v0.10.0 (2026-08-10) ### Added diff --git a/README.md b/README.md index c5ff3d42..f1b31b51 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Requires Go 1.26+ and make. git clone git@github.com:Hanalyx/kensa.git cd kensa make build # builds all five binaries into bin/ -./bin/kensa --version # → kensa 0.10.0 (kensa) +./bin/kensa --version # → kensa 0.11.0 (kensa) ``` The five binaries: @@ -117,7 +117,7 @@ ldd bin/kensa # "not a dynamic executable" ## Status -`v0.10.0`, released and signed. The 0.x line is the pre-1.0 development phase. The +`v0.11.0`, released and signed. The 0.x line is the pre-1.0 development phase. The `api/` Go package is frozen under v1 semver for OpenWatch's consumption; the rest of the surface (CLI flags, rule schema additions, output formats) may change between MINOR versions with one release of deprecation warning. All 29 handlers diff --git a/VERSION b/VERSION index 78bc1abd..d9df1bbc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.0 +0.11.0 diff --git a/VERSIONING_PLAN.md b/VERSIONING_PLAN.md index beca141b..5f3e395c 100644 --- a/VERSIONING_PLAN.md +++ b/VERSIONING_PLAN.md @@ -226,6 +226,22 @@ Current phase. Indicates: CHANGELOG entry and one-version deprecation warning** - Breaking changes to `api/` require a major bump (`1.0.0` or later); the `api/` contract is the load-bearing commitment to OpenWatch +- **Pre-1.0 exception, founder-approved 2026-09-20, one change at a + time.** During 0.x, a *corrective* `api/` behavior change may ship in a + MINOR bump: one that moves no Go signature, adds no field and removes + none, and changes what an existing value means only to stop it stating + something false. It is not a general license to change value semantics. + Each use needs its own founder approval, and the release that carries it + must document three things: the compatibility assessment (which consumers + are known, what each sees, and what remains unassessed), the operator- + visible effect, and any migration a consumer must perform. Written + clearance from OpenWatch is required and is evidence for OpenWatch only; + it does not establish compatibility for any other consumer of `api/`. + The first use is v0.11.0, where a failed apply step reports + `rollback_failed` instead of `rolled_back` because `rolled_back` claimed + a restoration the engine had not performed. This is an exception granted + for the 0.x line, not a reading of the rule above; at 1.0 and later such + a change is MAJOR. - No long-term support commitment for 0.x lines ### Production Phase (1.x.x+) @@ -252,33 +268,60 @@ Indicates production-ready software: ### Tagging -Kensa releases are git tags of the form `v$(cat VERSION)`. The release -process is manual today; release automation (GoReleaser, RPM packaging, -Ed25519-signed checksums) is planned for v1.0. +Kensa releases are git tags of the form `v$(cat VERSION)`. Pushing a `v*` +tag triggers `.github/workflows/release.yml`, which runs GoReleaser per +`.goreleaser.yaml` and publishes the GitHub Release with its artifacts. +The tag push IS publication; there is no separate publish step and no way +to preview it, so everything below the tag line is a point of no return. ```bash # 1. Bump VERSION on a release branch -echo "0.2.0" > VERSION - -# 2. Update CHANGELOG.md: stamp ## Unreleased as ## v0.2.0 — -$EDITOR CHANGELOG.md - -# 3. Open a release PR -git checkout -b release/v0.2.0 -git add VERSION CHANGELOG.md -git commit -m "chore(release): v0.2.0" -git push -u origin release/v0.2.0 -gh pr create --title "chore(release): v0.2.0" --body "..." - -# 4. After CI green + PR merge, tag the merge commit and create the -# GitHub Release page from the CHANGELOG section. +echo "0.11.0" > VERSION + +# 2. Update CHANGELOG.md: stamp ## Unreleased as ## v0.11.0 () and +# keep an empty ## Unreleased above it. Update the README version lines. +$EDITOR CHANGELOG.md README.md + +# 3. Open a release PR; `make docs-check` verifies VERSION, CHANGELOG and +# README agree. Pull-request CI does NOT run the release snapshot or +# govulncheck jobs: both are gated to `schedule` and `workflow_dispatch` +# and show as skipped on the PR. +git checkout -b chore/release-v0.11.0 +git add VERSION CHANGELOG.md README.md +git commit -m "chore(release): v0.11.0" +git push -u origin chore/release-v0.11.0 +gh pr create --title "chore(release): v0.11.0" --body "..." + +# 4. Dispatch CI on the candidate branch and require it green on EVERY +# job, including "Release snapshot" (builds every artifact, no publish, +# no sign) and "Vulnerability Scan (govulncheck)". Record the run id and +# confirm its head SHA is the candidate. This is the packaging gate; a +# green pull-request run is not. +gh workflow run ci.yml --ref chore/release-v0.11.0 +gh run list --workflow ci.yml --branch chore/release-v0.11.0 --event workflow_dispatch --limit 1 + +# 5. After PR merge and founder release acceptance: tag the merge commit. +# The workflow refuses to run without GPG_PRIVATE_KEY, GPG_PASSPHRASE, +# COSIGN_PRIVATE_KEY and COSIGN_PASSWORD. Nothing checks that the tag +# matches VERSION; the tag name alone sets the published version. git checkout main && git pull --ff-only -git tag -a "v0.2.0" -m "Release v0.2.0 — Sentinel" -git push origin "v0.2.0" -gh release create v0.2.0 --title "v0.2.0 — Sentinel" \ - --notes-file <(sed -n '/^## v0.2.0/,/^## v[0-9]/p' CHANGELOG.md | sed '$d') +git tag -a "v0.11.0" -m "Release v0.11.0 — Sentinel" +git push origin "v0.11.0" + +# 6. GoReleaser has changelog generation disabled, so the release page is +# published with an EMPTY body. Set it from the CHANGELOG section after +# the workflow finishes, and check the extracted notes are not empty +# first (see the sed note above). +sed -n '/^## v0.11.0/,/^## v[0-9]/p' CHANGELOG.md | sed '$d' > /tmp/notes.md +test -s /tmp/notes.md && gh release edit v0.11.0 --notes-file /tmp/notes.md ``` +What a release publishes, all signed: rpm and deb for linux/amd64 and +linux/arm64, a noarch `kensa-rules` rpm and deb, binary tarballs, `with-rules` +air-gap tarballs, a CycloneDX SBOM, and a sha256 checksums file with a cosign +signature over it. The GPG signing key is the Hanalyx LLC key in `KEYS`; +the checksums signature uses the Kensa cosign key, also in `KEYS`. + Use `sed` for the range, not `awk`. An awk range whose end pattern also matches its start line opens and closes on that one line, so `awk '/^## v0.2.0/,/^## v[0-9]/'` emits only the heading and the trailing @@ -286,10 +329,12 @@ matches its start line opens and closes on that one line, so page with no body, published without an error. A `sed` range never terminates on its start line. Check the output is not empty before tagging. -The 0.x line ships source-only. OpenWatch and other Go consumers -import the `api/` package via `go get github.com/Hanalyx/kensa@v0.x.y` -against the tag. Operators build from source per `README.md` → -**Building from source**. +Go consumers such as OpenWatch import the `api/` package via +`go get github.com/Hanalyx/kensa@v0.x.y` against the tag, independent of +the packages. Operators install the signed rpm or deb; building from source +per `README.md` remains supported but is not the shipped path. This section +said the 0.x line ships source-only until v0.11.0, which was untrue from +v0.2.0 on. ### Commit Message Format diff --git a/cmd/kensa/coverage_finalization_test.go b/cmd/kensa/coverage_finalization_test.go index 9286afe4..3d0eeb40 100644 --- a/cmd/kensa/coverage_finalization_test.go +++ b/cmd/kensa/coverage_finalization_test.go @@ -632,25 +632,29 @@ func TestActiveDocsAgreeWithFinalContract(t *testing.T) { } }) - t.Run("changelog_and_version", func(t *testing.T) { + t.Run("changelog", func(t *testing.T) { c := read("CHANGELOG.md") - // Anchor on the heading at line start: the file's preamble mentions - // "## Unreleased" in prose, and matching that would slice the wrong - // region and pass or fail for the wrong reason. - const heading = "\n## Unreleased\n" - i := strings.Index(c, heading) - if i < 0 { - t.Fatal("CHANGELOG has no Unreleased heading") - } - unreleased := c[i+len(heading):] - if j := strings.Index(unreleased, "\n## "); j >= 0 { - unreleased = unreleased[:j] - } - if !strings.Contains(unreleased, "coverage") || !strings.Contains(unreleased, "--framework") { - t.Errorf("Unreleased does not record the finalization:\n%s", unreleased) - } - if v := strings.TrimSpace(read("VERSION")); v != "0.10.0" { - t.Errorf("VERSION = %q; this slice must not bump it", v) + // The finalization entry sits under Unreleased until the release + // that ships it stamps that section, which happened in v0.11.0. So + // the entry must appear above the v0.10.0 heading, whichever + // section it is in; pinning it to Unreleased, or pinning VERSION, + // would fail at every release. Anchor headings at line start: the + // preamble mentions "## Unreleased" in prose. + const entry = "**`kensa coverage` always reports framework control coverage.**" + const prior = "\n## v0.10.0 " + e := strings.Index(c, entry) + p := strings.Index(c, prior) + if e < 0 { + t.Fatal("CHANGELOG does not record the coverage finalization") + } + if p < 0 { + t.Fatal("CHANGELOG has no v0.10.0 heading to anchor on") + } + if e > p { + t.Errorf("the finalization entry sits below the v0.10.0 heading, in a release that predates it") + } + if !strings.Contains(c[e:p], "--framework") { + t.Error("the finalization entry does not name --framework") } }) diff --git a/docs/guide/09-reference.md b/docs/guide/09-reference.md index 3a3ddbb1..79cc984d 100644 --- a/docs/guide/09-reference.md +++ b/docs/guide/09-reference.md @@ -1,6 +1,6 @@ # 09 · Command reference -_Applies to: Kensa v0.10.0 plus Unreleased changes. Last updated 2026-09-08._ +_Applies to: Kensa v0.11.0. Last updated 2026-09-20._ This chapter documents every `kensa` command and flag. It is the exhaustive counterpart to the task-focused chapters: for *how* to scan diff --git a/specs/cli/coverage-command-finalization.spec.yaml b/specs/cli/coverage-command-finalization.spec.yaml index 18ba373f..0c52cbc1 100644 --- a/specs/cli/coverage-command-finalization.spec.yaml +++ b/specs/cli/coverage-command-finalization.spec.yaml @@ -338,9 +338,13 @@ spec: KENSA_NO_REPURPOSE_WARNINGS_active_entry: absent manpage_check: pass changelog: - Unreleased_records_finalization: true + # The entry lives under Unreleased until the release that + # ships it stamps that section; the finalization shipped + # in v0.11.0. "Unreleased_records_finalization: true" and + # "VERSION_changed: false" were PR-time guards written as + # permanent expectations, which no release could satisfy. + records_finalization_at_or_after: v0.11.0 historical_release_sections_changed: false - VERSION_changed: false specifications: old_rename_status: deprecated current_contract_points_to: cli-coverage-command-finalization @@ -348,8 +352,19 @@ spec: quiet_and_manpage_drafts_match_active_surface: true references_constraints: [C-07] priority: high + # Accepted 2026-09-08, then revised: the changelog expectations + # were PR-time guards written as permanent ones. They required the + # entry to sit under ## Unreleased and VERSION to be unchanged, + # which no release can satisfy, since releasing IS stamping that + # section and changing VERSION. The v0.11.0 release failed its + # unit job on exactly that. The revised expectation is that the + # entry appears at or after v0.11.0, the release that shipped the + # finalization, and the VERSION guard is gone. The amendment was + # accepted on 2026-09-22, which is the date below. Both dates are + # kept so the record shows the contract changed after its first + # approval rather than appearing to have been settled once. approval_gate: true - approval_date: "2026-09-08" + approval_date: "2026-09-22" - id: AC-09 description: |