Compare commits

...
38 Commits
Author SHA1 Message Date
james.maes 707bbd33f0 Merge pull request 'ci: docs checks and conformance tests on Gitea' (#57) from ci/gitea-lint-workflow into main 2026-11-08 15:01:59 +00:00
James MaesandClaude Opus 5.5 206f409f6f ci: pin the shared workflow to v1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:00:40 -06:00
James MaesandClaude Opus 5.5 c16db56477 ci: call the shared workflow from ci-workflows main
lint / lint (push) Successful in 24s
build / docs (push) Successful in 24s
lint / ansible-lint (push) Skipped
windows / windows (push) Successful in 22s
build / conformance (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:56:28 -06:00
James MaesandClaude Opus 5.5 7533c8888c ci: musl lychee build; name the repository for the static conformance check
lint / lint (push) Successful in 19s
build / docs (push) Successful in 19s
lint / ansible-lint (push) Skipped
windows / windows (push) Successful in 24s
build / conformance (push) Successful in 24s
The gnu lychee needs glibc 2.38, newer than the lint image's. The runner
checks out into a directory named after the runner user, so the static
check must be told the repository name (K22-REPO-42 looks it up in
MAINTAINERS.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:43:09 -06:00
James MaesandClaude Opus 5.5 2afa8928a5 ci: docs checks and conformance tests on Gitea
lint / lint (push) Failing after 17s
build / docs (push) Failing after 17s
lint / ansible-lint (push) Skipped
windows / windows (push) Failing after 19s
build / conformance (push) Failing after 19s
The repository's own gates had no runner since the GitHub-hosted
ci.yml cannot run on Gitea. Port them to the shared ci-workflows:
- lint.yaml: markdownlint (repo config), yamllint over every YAML
  file, and the offline lychee internal-link check (pinned, sha256);
- windows.yaml: the conformance checker self-test, fuzzing, package
  tests and the static self-conformance check.

.yamllint.yaml keeps every default rule except line length, which
K22-CODE-08 leaves unlimited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:37:22 -06:00
james.maes 0d635b5688 Merge pull request 'ci: remove workflows that call the retired CI system' (#56) from retire/old-ci into main 2026-11-07 13:30:28 +00:00
james.maesandClaude Opus 5.5 3fcf243a34 ci: remove workflows that call the retired CI system
Owner decision 2026-11-07: only KofTwentyTwo/ci-workflows remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:30:26 +00:00
forge-recovery 27161850ef ci: restore original Standards and Jarvis CI routes
KofTwentyTwo/ci-policy: ci / fork-guard (push) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (push) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (push) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (push) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (push) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (push) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (push) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (push) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (push) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (push) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (push) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (push) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (push) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (push) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (push) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (push) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (push) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (push) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (push) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (push) Skipped
KofTwentyTwo/ci-policy: Original Standards CI / docs (push) Failing after 8s
KofTwentyTwo/ci-policy: Original Standards CI / pr-dco (push) Skipped
KofTwentyTwo/ci-policy: Original Standards CI / pr-title (push) Skipped
KofTwentyTwo/ci-policy: Original Standards CI / verify (push) Failing after 11s
KofTwentyTwo/ci-policy: native / fork-deny (push) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (push) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (push) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (push) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (push) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (push) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (push) Skipped
KofTwentyTwo/ci-policy: Original Standards CI / secrets (push) Failing after 6s
KofTwentyTwo/ci-policy: native / build-windows-7 (push) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (push) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (push) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (push) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (push) Skipped
KofTwentyTwo/ci-policy: native / native-result (push) Successful in 5s
KofTwentyTwo/ci-policy: Original Standards CI / sca (push) Failing after 6s
KofTwentyTwo/ci-policy: Original Standards CI / workflows (push) Failing after 6s
KofTwentyTwo/ci-policy: Original Standards CI / conformance (push) Successful in 39s
KofTwentyTwo/ci-policy: Original Standards CI / windows-signing (push) Successful in 43s
KofTwentyTwo/ci-policy: Original Standards CI / gate (push) Failing after 10s
KofTwentyTwo/ci-policy: ci / standards-0 (push) Failing after 1m54s
KofTwentyTwo/ci-policy: ci / ci-result (push) Failing after 3s
KofTwentyTwo/ci-policy: native / build-macos-1 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (push) Skipped
KofTwentyTwo/ci-policy: Original Standards CI / analyze (actions) (push) Successful in 1m7s
KofTwentyTwo/ci-policy: Original Standards CI / analyze (javascript-typescript) (push) Successful in 1m39s
2026-11-04 12:00:27 +00:00
forge-recoveryandCodex 50379d2d2d ci: restore original Standards weekly and manual CI triggers
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Successful in 6s
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Successful in 9s
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Successful in 10s
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Successful in 9s
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Successful in 21s
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Successful in 3s
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
Co-Authored-By: Codex <noreply@openai.com>
2026-11-04 11:58:01 +00:00
james.maesandClaude Sonnet 5.5 a18179e90f docs(exceptions): record exact printing uri-js tool license
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Successful in 7s
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Successful in 21s
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Successful in 23s
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Successful in 12s
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Successful in 38s
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Successful in 8s
KofTwentyTwo/ci-policy: ci / fork-guard (push) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (push) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (push) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (push) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (push) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (push) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (push) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (push) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (push) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (push) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (push) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (push) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (push) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (push) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (push) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (push) Skipped
KofTwentyTwo/ci-policy: native / fork-deny (push) Skipped
KofTwentyTwo/ci-policy: native / dotnet-0 (push) Skipped
KofTwentyTwo/ci-policy: native / dotnet-1 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-4 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-5 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-6 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-7 (push) Skipped
KofTwentyTwo/ci-policy: native / swift-8 (push) Skipped
KofTwentyTwo/ci-policy: native / native-result (push) Successful in 5s
KofTwentyTwo/ci-policy: Shared source and config CI / verify (push) Successful in 15s
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (push) Successful in 15s
KofTwentyTwo/ci-policy: Shared source and config CI / gate (push) Successful in 10s
KofTwentyTwo/ci-policy: ci / source-config-13 (push) Successful in 32s
KofTwentyTwo/ci-policy: ci / ci-result (push) Successful in 4s
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-2 (push) Skipped
KofTwentyTwo/ci-policy: native / dotnet-3 (push) Skipped
Approve only the unchanged development-only uri-js 4.4.1 package used transitively by ESLint, with version, integrity and license-byte guards. Other license and vulnerability gates remain required.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-01 16:39:45 -06:00
james.maesandCodex fbfc8d2cf3 docs(exceptions): accept exact printing Windows SDK licenses
Record EX-0014 for the exact maintainer-approved fifteen-package Microsoft
Windows SDK closure in QRun-IO/qqq-print-agent. Preserve versions, artifact
hashes, notices, redistribution conditions and all required security gates.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-11-01 03:50:16 -06:00
james.maesandCodex 7d758b1344 fix(ci): restore CodeQL coverage and record H2 license exception (#54)
Record EX-0013 only for QRun-IO/qbit-printing's unmodified H2 2.4.240 binary, with reviewed SHA-256, existing security controls and expiry 2027-04-30.

Restore normal required CI and preserve Actions plus JavaScript/TypeScript security-extended coverage in the advanced workflow. Default setup was explicitly disabled to resolve its upload conflict. All required checks and both advanced analyses passed on b8e4debe64040a346c5a0815174a68edc94fed98.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-31 03:39:29 -05:00
koftwentytwo-standards-releases[bot] 70b555bd56 chore(main): release 1.2.1 (#51)
Release proposal generated from reviewed changes on main. Review the
version, changes, and validation before merging.
---


## 1.2.1 (2026-10-13)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

## What's Changed
### Changes
* fix(releases): record MSI checklist preflight recovery by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/49


**Full Changelog**:
https://github.com/KofTwentyTwo/standards/compare/v1.2.0...v1.2.1

---
Generated by [Release
Please](https://github.com/googleapis/release-please) using this
repository's release configuration.

Co-authored-by: koftwentytwo-standards-releases[bot] <340613191+koftwentytwo-standards-releases[bot]@users.noreply.github.com>
2026-10-13 08:29:45 -05:00
james.maesandCodex GPT-6 db417d1525 fix(releases): record MSI checklist preflight recovery (#49)
The1.2.0 tag failed preflight because I omitted the required MINOR
checklist annotation. Record an honest forward recovery: preserve that
signed failed tag, prepare1.2.1 from the same MSI implementation, attach
real conformance/scanning/checklist evidence and include its URL in the
new signed annotation. Do not retag or bypass validation.

Only documentation changes. Actual full conformance exits0 with no FAIL;
the only open code-scanning item is a medium Scorecard Fuzzing process
finding, and no HIGH/CRITICAL Dependabot alerts are open. Current
exceptions are recorded. The existing optional MSI code and all gates
are unchanged. Owner authorized release and no further human-review
requests. Substantial AI assistance: Codex GPT-6. Refs #47 and
KofTwentyTwo/MarkdownViewr.Windows#36.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex GPT-6 <noreply@openai.com>
2026-10-13 08:23:32 -05:00
koftwentytwo-standards-releases[bot] 0ef3d319b8 chore(main): release 1.2.0 (#48)
Release proposal generated from reviewed changes on main. Review the
version, changes, and validation before merging.
---


## 1.2.0 (2026-10-13)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

## What's Changed
### Changes
* feat(releases): publish signed per-user MSI installers by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/47


**Full Changelog**:
https://github.com/KofTwentyTwo/standards/compare/v1.1.2...v1.2.0

---
Generated by [Release
Please](https://github.com/googleapis/release-please) using this
repository's release configuration.

Co-authored-by: koftwentytwo-standards-releases[bot] <340613191+koftwentytwo-standards-releases[bot]@users.noreply.github.com>
2026-10-13 08:10:06 -05:00
james.maesandCodex GPT-6 7584fa19cc feat(releases): publish signed per-user MSI installers (#47)
Provide optional per-user MSI alongside existing EXE/ZIP assets using
pinned Velopack1.2.161. Keep the existing signing hook, preserve
third-party signatures, verify the administratively extracted MSI
payload through the existing signed-archive verifier, require native MSI
install/window launch/uninstall before publication, and include MSI in
SBOM subjects/checksums/provenance. The default remains EXE-only for
existing callers.

Validation: actionlint and configured zizmor medium gate pass; all40
signing regressions pass with zero skips. A real native MSI fixture
compiles and the actual GitHub-wrapped extraction body preserves all6
signed input application binaries byte-for-byte. Native MSI ProductCode
lookup succeeds. The first local administrative target exceeded Windows
Installer's path limit; a short normal temporary root succeeds, matching
hosted runner paths. This fixture is unsigned and not published;
production MSI signing/installation is required before claiming
delivery.

The maintainer explicitly renewed EXE+MSI delivery and instructed
completion without further human-review requests. Existing publication
authorization applies. No Azure identity, scope or credential changes.
Substantial AI assistance: Codex GPT-6. Refs
KofTwentyTwo/MarkdownViewr.Windows#36 and #33.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex GPT-6 <noreply@openai.com>
2026-10-13 08:05:38 -05:00
koftwentytwo-standards-releases[bot] 083d90326a chore(main): release 1.1.2 (#46)
Release proposal generated from reviewed changes on main. Review the
version, changes, and validation before merging.
---


## 1.1.2 (2026-10-13)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

## What's Changed
### Changes
* fix(releases): scope bootstrap parameters under Actions pwsh by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/45


**Full Changelog**:
https://github.com/KofTwentyTwo/standards/compare/v1.1.1...v1.1.2

---
Generated by [Release
Please](https://github.com/googleapis/release-please) using this
repository's release configuration.

Co-authored-by: koftwentytwo-standards-releases[bot] <340613191+koftwentytwo-standards-releases[bot]@users.noreply.github.com>
2026-10-13 06:10:59 -05:00
james.maesandCodex GPT-6 261a0055ae fix(releases): scope bootstrap parameters under Actions pwsh (#45)
Both1.0.1 production runs reached the fixed builder after full
acceptance. The inline dependency bootstrap failed with `param is not
recognized` because GitHub's default pwsh shell inserts
`$ErrorActionPreference = 'stop'` before the run body. The previous
standalone-script test did not model that wrapper; this was my
implementation error.

Invoke the existing parameterized bootstrap inside a script block and
pass its arguments through. Preserve every package
URL/version/hash/signature check, destination guard, official action
input and Azure trust scope. Update tests to execute the actual run body
after GitHub's preamble.

Validation: added the preamble to the existing real-body test first and
reproduced all five bootstrap cases failing with the exact hosted param
error in2.26sec; after the fix40 signing tests pass with zero skips. The
actual corrected wrapped body also downloads/verifies Microsoft's real
pinned package and starts its CLI. Configured script analysis,
actionlint, zizmor medium gate and diff checks pass. No production
signing/publication success claimed.

Refs #43 and KofTwentyTwo/MarkdownViewr.Windows#33. Substantial AI
assistance: Codex GPT-6. Required genuine maintainer review before
merge; app release authorization already exists.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex GPT-6 <noreply@openai.com>
2026-10-13 06:08:28 -05:00
koftwentytwo-standards-releases[bot] 96cbdf208a chore(main): release 1.1.1 (#42)
Release proposal generated from reviewed changes on main. Review the
version, changes, and validation before merging.
---


## 1.1.1 (2026-10-12)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

## What's Changed
### Changes
* docs(inventory): register MarkdownViewr and the Windows starter by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/36
* fix(releases): bootstrap verified Artifact Signing dependency by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/44


**Full Changelog**:
https://github.com/KofTwentyTwo/standards/compare/v1.1.0...v1.1.1

---
Generated by [Release
Please](https://github.com/googleapis/release-please) using this
repository's release configuration.

Co-authored-by: koftwentytwo-standards-releases[bot] <340613191+koftwentytwo-standards-releases[bot]@users.noreply.github.com>
2026-10-12 18:24:28 -05:00
james.maesandCodex GPT-6 b3e4b4989c fix(releases): bootstrap verified Artifact Signing dependency (#44)
The MarkdownViewr dev release reaches Azure OIDC successfully but the
official Artifact Signing action fails bootstrapping its pinned Sign
CLI: `--add-source` is incompatible with the caller's NuGet package
source mapping.

Prestage the exact action dependency from nuget.org with a fixed SHA256
and `dotnet nuget verify --all`. The official action then finds the
genuine package in its normal dependency directory. Caller NuGet policy,
OIDC trust, signing inputs, ownership catalog and final
signature/distributed-byte gates stay intact. Existing dependency
directories are preserved and rejected rather than replaced.

Fixes #43.

Validation: reproduced the hosted error locally; executed the actual new
workflow step with Microsoft's real signed package and launched its CLI;
all 40 signing regression tests pass (zero skips); full configured
PowerShell analysis, actionlint and zizmor pass. Five added tests cover
valid staging, changed download, invalid signature, existing directory
and missing executable. No production signing or publication claimed
yet.

Substantial AI assistance: implemented with Codex GPT-6. Maintainer
review required by K22-AI-01 before merge.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex GPT-6 <noreply@openai.com>
2026-10-12 18:22:02 -05:00
james.maesandCodex 089a94ac3c docs: register MarkdownViewr and Windows starter as Products
MarkdownViewr.Windows and windows-app-template are public Windows
products but are absent from the required repository inventory. Add both
as Product tier, with adoption-in-progress status and their actual
purpose.

This resolves the missing inventory proposal for K22-REPO-42 without
asserting conformance, signed release readiness or completed maintainer
review. Existing people, access, repository rows and policies remain
unchanged. Inventory acceptance requires this PR to be reviewed and
merged.

Validation: markdownlint-cli2 v0.23.3 reports zero issues for
MAINTAINERS.md; git diff --check passes; commit is signed (G) and DCO
signed off. The new links point to the existing public repositories.
Required hosted documentation/conformance checks and maintainer review
remain pending.

Related release tracking:
https://github.com/KofTwentyTwo/MarkdownViewr.Windows/issues/10. Shared
defaults: AppKit PR#29 and windows-app-template PR#4. Substantial AI
assistance: Codex (GPT-6).

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-10-12 11:59:29 -05:00
koftwentytwo-standards-releases[bot] 93b1d6163a chore(main): release 1.1.0
Release proposal generated from reviewed changes on main. Review the
version, changes, and validation before merging.
---


## 1.1.0 (2026-10-12)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

## What's Changed
### Changes
* docs(exceptions): close EX-0002 for gclo by @KofTwentyTwo in
https://github.com/KofTwentyTwo/standards/pull/33
* docs(exceptions): add EX-0009 to EX-0012 from the repository audit by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/37
* feat(releases): verify Windows Artifact Signing before publication by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/31
* fix(signing): pin reviewed helpers reachable from main by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/39
* fix(ci): isolate racing signing analyzer rules by @KofTwentyTwo in
https://github.com/KofTwentyTwo/standards/pull/41


**Full Changelog**:
https://github.com/KofTwentyTwo/standards/compare/v1.0.0...v1.1.0

---
Generated by [Release
Please](https://github.com/googleapis/release-please) using this
repository's release configuration.

Co-authored-by: koftwentytwo-standards-releases[bot] <340613191+koftwentytwo-standards-releases[bot]@users.noreply.github.com>
2026-10-12 11:08:32 -05:00
james.maesandCodex 7c44ceaa7c fix(ci): isolate racing signing analyzer rules
The Windows signing gate intermittently crashes inside PSScriptAnalyzer
1.25.0 while its rules share lazy PowerShell command metadata. This
blocks the standards 1.1.0 release and MarkdownViewr's production
signing rollout. The unchanged gate reproduced the
`CommandInfo.get_Parameters` failure locally on the fourth cold process;
the first two-pass candidate also failed in
`CommandInfo.ResolveParameter`.

Run `PSUseCorrectCasing` and `PSReservedCmdletChar` separately, followed
by every remaining configured rule. The latter shares
`Helper.GetExportedFunction` with the comment-help rule, so separating
casing alone is insufficient for this exported signing module. All three
original source files, pinned analyzer/Pester versions, settings, fatal
findings/exceptions, 35 signing tests and ten-minute job timeout are
retained. No rules or checks are disabled and no retry conceals
failures.

Three negative fixtures require detection of incorrect command casing, a
reserved character in an exported cmdlet name, and `Write-Host`. These
validate each pass before analyzing the real signing files. Full
exception details remain visible if the analyzer fails again.

Validation:

- Exact workflow analyzer body passed 20 predetermined cold PowerShell
processes, each covering all three files and all three negative
fixtures. Original race reproduced; the rejected two-pass candidate's
failure is retained.
- All 35 signing/Velopack tests passed with zero skips.
- `actionlint`, actual authenticated online `zizmor --min-severity
medium`, and `git diff --check` passed; existing suppressions unchanged.
- Hosted checks pending at creation. Maintainer review of this exact
change is required before merge under K22-AI-01; earlier review of the
signing implementation/reference correction is not reused for this new
behavior.

Related: PowerShell/PSScriptAnalyzer#1708,
KofTwentyTwo/MarkdownViewr.Windows#5, and #40. Substantial AI
assistance: Codex (GPT-6).

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-10-12 11:05:12 -05:00
james.maesandCodex 21df02deb9 fix(signing): pin reviewed helpers reachable from main
Repin six helper references to signed main after the reviewed signing implementation was squash-merged. The referenced composite action/module are byte-identical to the previously reviewed code.

Actual online security scan reproduces six HIGH impostor-commit findings before and passes after; actionlint and every required hosted check pass. No helper behavior, permissions, gate or suppression changes. Maintainer confirmed review/resolved findings for PR #39 and release publication is authorized.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-12 10:26:18 -05:00
james.maesandCodex f896215ec9 feat(releases): verify Windows Artifact Signing before publication
Sign explicit owned Windows binaries, preserve third-party bytes/signatures, and verify final NuGet/Velopack distribution before publication. Protected release signing uses Azure OIDC, SHA256 and RFC3161 timestamps.

Validation: exact e3aaed4 CI38263239957 passes all required gates; 35 local signing regressions, conformance self-test/full API preflight and document/workflow/security lint pass. Real local representative six-binary and final installer/portable/update signing integration also passes; protected CI OIDC remains to be verified by the consumer release.

Maintainer confirmed review of exact e3aaed4 and resolution of all findings in this session, and authorized release publication.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-12 09:53:15 -05:00
james.maesandClaude Opus 5.5 c95e9e298c docs(exceptions): add EX-0009 to EX-0012 from the repository audit (#37)
Adds four exception-register entries for standing deviations found by
the 2026-10-12 repository audit:

- **EX-0009**: GitFlow with merge commits (K22-REPO-20 linear history,
K22-REPO-22 squash only) for Website-Backend, Website-Frontend, Munitor
and cluster-gitops. Munitor's release and deploy pipeline depends on
develop/main ancestry. cluster-gitops image bumps are now GitHub-signed
GraphQL commits with no ruleset bypass actor.
- **EX-0010**: CircleCI (Munitor orb) instead of the GitHub Actions
reusable-workflow gates (K22-CI-01, 10-24, 30) for Website-Backend,
Website-Frontend and Munitor. The CircleCI checks are required by
rulesets.
- **EX-0011**: 2-space formatting (K22-CODE-TS-01, K22-CODE-SH-02) in
Website-Frontend (Prettier + eslint-config-next) and Munitor (Bash). A
reformat would trigger releases; the repositories' `.editorconfig` cites
this ID.
- **EX-0012**: Praetor commits git-crypt-encrypted local OpenTofu state
and tfvars (K22-CODE-TF-04/05). This is the bootstrap/recovery path for
the infrastructure that would host a remote backend.

No exception is needed for the munitor-ci bot's unsigned pushes, because
that is fixed: Munitor now commits through `createCommitOnBranch`, so
the commits are signed.

AI disclosure: drafted by Claude Code (Claude Opus 5.5) at the
maintainer's direction.

Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:08:56 -05:00
james.maesandClaude Fable 5.1 1cc9d01fa3 docs(exceptions): close EX-0002 for gclo (#33)
gclo now ships Authenticode-signed releases through the shared workflow
(sign: true; Azure Artifact Signing over OIDC; v1.0.2-beta.3,
v1.1.0-beta.1 and v1.1.0 verified with signtool verify /pa /all, 289
files, 0 failures). EX-0002 is recorded as closed for gclo on 2026-10-11
and narrowed to Windows apps that have not turned signing on yet (AppKit
samples, future apps).

Relates to KofTwentyTwo/gclo#57 and standards#5.

Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 08:01:14 -05:00
koftwentytwo-standards-releases[bot]andCodex 7c69368443 chore(main): release 1.0.0 (#20)
Publish the stable standards baseline with mandatory applicable badge participation
and parser fuzzing, the passing assessment, professional release text, scoped
frontend audit exception, Windows signing correction and readiness documentation.

Release checklist: https://github.com/KofTwentyTwo/standards/issues/30

Validated full conformance, Markdown and links, package regressions, required PR
gates and both 10000-case fuzz campaigns. Existing Windows consumer limitations
remain documented in issue #22 and docs/releases.md.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-10 16:22:50 -05:00
james.maesandClaude Fable 5.1 40ecebae5c ci(release-velopack): sign only unsigned files; verify before packing (#29)
## What

First real `sign: true` run (gclo `v1.0.2-beta.2`, run 38080813985):
OIDC login worked, signing started, and the step died after 320 s
because it signs **every** exe/dll under `publish` (~285 in a
self-contained publish, 239 already signed by Microsoft/.NET) and
`azure/artifact-signing-action` stops at its 300 s default `timeout`.

- **Select the binaries to sign**: only files whose
`Get-AuthenticodeSignature` status is not `Valid` go to the action's
`files` input; fails if the main exe (or the CLI exe) is not among them.
Third-party signatures are preserved, nothing is signed twice. `timeout:
1800`.
- **vpk**: its built-in `--azureTrustedSignFile` path hung >10 min on
nine files locally (it skips signed files itself, so the count was never
the issue). vpk now calls a `--signTemplate` wrapper (written into
`RUNNER_TEMP`) that uses the SignTool and the
`Microsoft.ArtifactSigning.Client` dlib the action installed, with the
Azure CLI credential, and skips already-signed files.
- **Verify before packaging further**: `signtool verify /pa /all` on
Setup.exe and every exe/dll inside the portable zip; nothing unsigned
reaches the draft release (K22-REL-05).

Closes #26.

## How it was tested

Reproduced the package job locally with gclo's real publish output
against the live `kof22signing` account (`releases` profile): 48 of 287
binaries selected and signed in 192 s, all verify (Valid, timestamped,
CN=James Maes); `vpk pack` with the wrapper: 9 files + Setup.exe signed,
36 s; Setup.exe, Update.exe, gclo.exe and gclo_ExecutionStub.exe valid;
282 DLLs in the portable zip, 0 unsigned (45 ours, the rest Microsoft's
untouched); CLI exe valid. `actionlint` clean. First CI run will be gclo
`v1.0.2-beta.3` after re-pinning.

Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 16:07:10 -05:00
james.maesandGPT-6 5c21fc7771 docs(exceptions): scope temporary frontend audit exception (#27)
Website-Frontend CI currently fails on GHSA-vfj7-8cjw-p6xm in
development-only braces 3.0.3. No patched package release exists. Record
EX-0008 for this advisory and its propagated findings only, expiring
November 10, 2026, with a review on October 17.

The implementation retains the full audit, rejects all production
high/critical findings and all critical findings, checks exact locked
development paths/version, fails on audit errors or expiry, and requires
reassessment for any newer braces release. Lint, tests, build, secret
scanning, SAST and Trivy remain enabled. No global scanner suppression
is added.

Validation: Markdown lint and offline link/fragment checks pass. The
consumer guard passes against the current website lockfile and
regression cases for acceptance, expiry, unrelated/mixed advisories,
runtime paths, critical findings, new releases and audit errors. This
register change is the approval record required by the exception policy;
consumer release remains gated by its normal CI.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: GPT-6 <codex@openai.com>
2026-10-10 15:09:28 -05:00
james.maesandCodex 83134656fc docs(assurance): record OpenSSF passing badge (#25)
The README and assurance records still say enrollment is blocked, even
though OpenSSF project 15366 now identifies KofTwentyTwo/standards and
displays a passing badge. Add the provider's live badge and project URL,
record the completed maintainer self-assessment, and close the
enrollment-only exception EX-0007 while preserving its history.

Set the next assessment review to 2027-01-08 or before the next
MINOR/MAJOR release, whichever occurs first. Retain the unmet suggested
coverage and warning criteria and the release smoke-test follow-up in
#22. Update Scorecard dispositions without claiming an unobserved score
change.

Validation: public assessment identity and passing status verified;
Markdown lint passed for all 69 documents; offline links and anchors
passed with zero errors; full repository conformance passed with 32
PASS, 5 REVIEW, 3 existing WARN, 1 N/A, and no FAIL/UNKNOWN; git diff
--check passed.

AI assistance: Codex (GPT-6) prepared and validated this documentation
change. No standards requirement, workflow, root AGENTS.md, release tag,
or published asset changes.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-10-10 14:56:22 -05:00
james.maesandOpenAI GPT-6 Codex 11fe60b146 fix(releases): allow native formatting in generated changelog (#24)
Scope native spacing, bare-link and repeated-heading exceptions to the generated
changelog. Keep normal Markdown rules for other documentation.

Refs: #21
Validation: all nine required checks passed in CI run 38074899308.
Recorded AI review on PR #24 at source fa15d5cd2a.

Co-Authored-By: OpenAI GPT-6 Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-10 13:15:22 -05:00
james.maesandOpenAI GPT-6 Codex 435049d688 fix(releases): use professional repository and automation text (#23)
Configure professional Release Please wording and native release-note categories,
retain complete package notes, and reject emoji in PR titles and descriptions.
Update contributor and agent guidance and remove the decorative IDE symbol.

Fixes: #21
Validation: all nine required PR checks passed in CI run 38074586698.
Recorded AI review: https://github.com/KofTwentyTwo/standards/pull/23#pullrequestreview-5480239702

Co-Authored-By: OpenAI GPT-6 Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-10 13:10:10 -05:00
james.maesandOpenAI GPT-6 Codex f7262f97cb feat(assurance)!: require applicable fuzzing and badge participation
Add mandatory parser fuzzing (K22-TEST-25) and public Product Best Practices participation (K22-REPO-12), with scope records, criterion evidence, and adoption templates. The existing required conformance job runs both PowerShell fuzz seeds on PR/main and longer campaigns on weekly schedules and release tags.

Fixes #18. Preserve Scorecard detection limits: native PowerShell property-based fuzzing is running, but is not a claim of a supported integration or a passing badge. EX-0007 covers standards enrollment only, reviewed 2026-10-17 and expiring 2026-11-10 UTC, while the provider blocks maintainer login.

BREAKING CHANGE: Public Product repositories must enroll and maintain an evidence-backed assessment; complex untrusted-input parsers must have required CI fuzz campaigns and documented scope. Publish these new MUSTs in a MAJOR release. Existing immutable v0.1.2 and reserved failed tags remain unchanged; this merge does not publish another release.

Validation: all nine required PR checks passed at aa40477a11, including both native fuzz seeds. Extended local campaigns passed 20000 cases and 320000 assertions. Checker/package regressions, Markdown/links, workflow lint/security, secret scans, configured analysis of new PowerShell code, and full authenticated conformance passed.

AI review: https://github.com/KofTwentyTwo/standards/pull/19#pullrequestreview-5480144854. No blocking code findings remain. Substantial AI assistance: OpenAI GPT-6 Codex, under explicit maintainer implementation and merge authorization. No independent human review or completed badge assessment is claimed.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: OpenAI GPT-6 Codex <noreply@openai.com>
2026-10-10 12:38:20 -05:00
koftwentytwo-standards-releases[bot]andOpenAI GPT-6 Codex fdf5ed0296 chore(main): release 0.1.2 (#16)
First published immutable standards bundle containing policies, profiles, architectures, configs, workflows, templates, and tooling. Version and manifest are 0.1.2; changelog describes the complete snapshot and the corrected secret contract. v0.1.0 and v0.1.1 remain unpublished and reserved.

Release checklist: https://github.com/KofTwentyTwo/standards/issues/17

Validated package regression tests, Markdown lint, recorded AI review, and required PR gates. Substantial AI assistance: OpenAI GPT-6 Codex under maintainer authorization.

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: OpenAI GPT-6 Codex <noreply@openai.com>
2026-10-10 09:46:58 -05:00
james.maesandOpenAI GPT-6 Codex 08b5ac84a7 fix(releases): declare the protected environment secret contract (#15)
Declare the one named App key in the caller and required reusable workflow contract. Its value remains in the reviewer-protected tag-only environment; nested CI receives no secrets.

Both failed versions remain reserved. All required PR checks, actionlint, zizmor, Markdown lint, and full conformance passed. Substantial AI assistance: OpenAI GPT-6 Codex.

Release-As: 0.1.2
Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: OpenAI GPT-6 Codex <noreply@openai.com>
2026-10-10 09:42:34 -05:00
koftwentytwo-standards-releases[bot]andCodex ff31f36ddb chore(main): release 0.1.1 (#11)
🤖 I have created a release *beep* *boop* ---   ## [0.1.1](https://github.com/KofTwentyTwo/standards/compare/v0.1.0...v0.1.1) (2026-10-10)   ### Bug Fixes  * **releases:** carry PR read permission through release gates ([#13](https://github.com/KofTwentyTwo/standards/issues/13)) ([e375cf3](https://github.com/KofTwentyTwo/standards/commit/e375cf3b07f6b209324fe9e0be77844bbd0397a2))  --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).   ## First published standards bundle  This 0.1.1 release contains the full standards snapshot. The 0.1.0 tag remains reserved after its workflow startup failure; no release assets were published for it. Recovery #13 passed real GitHub workflow validation. Checklist #14 tracks source conformance, signatures, hashes, provenance, and immutable publication. AI assistance/review: Codex (GPT-6).  

Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-10 09:29:03 -05:00
james.maesandCodex e375cf3b07 fix(releases): carry PR read permission through release gates (#13)
## What

Grant the release gates the PR read permission their nested CI caller requires. Preserve optional PATCH checklist links. Closes #12.

## Why

GitHub rejected the first tag before any job ran because reusable-workflow permissions cannot increase at a lower level, even for skipped PR jobs. Keep v0.1.0 unchanged; publish the first 0.1 bundle as v0.1.1.

## Validation

Actionlint and zizmor pass. Dispatch the actual release workflow on this branch to check GitHub's complete call-chain validation without publishing (the top-level tag condition skips branch publication). Normal PR gates and reviewed version metadata remain required before tagging.

Commits are signed/DCO signed off. No new dependency or secret. Substantial AI assistance: Codex (GPT-6). Release-As: 0.1.1.


Release-As: 0.1.1
Signed-off-by: James Maes <james@kof22.com>
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-10-10 09:24:29 -05:00
46 changed files with 2595 additions and 97 deletions

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
# Gitea CI for the standards repository. The .github workflows are the reusable
# GitHub gates this repository publishes; Gitea does not run them.
name: build
on:
push:
jobs:
docs:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/lint.yaml@v1
with:
markdownlint: '**/*.md'
yamllint: |
**/*.yml
**/*.yaml
# Internal links and anchors between documents (offline: deterministic). The
# static musl build: the image's glibc is older than the gnu build needs.
commands: |
LYCHEE_VERSION=0.24.2
LYCHEE_SHA256=73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5
archive=lychee-x86_64-unknown-linux-musl.tar.gz
mkdir -p /tmp/lychee
curl -fsSLo "/tmp/lychee/$archive" "https://github.com/lycheeverse/lychee/releases/download/lychee-v$LYCHEE_VERSION/$archive"
(cd /tmp/lychee && echo "$LYCHEE_SHA256 $archive" | sha256sum --check --strict && tar -xzf "$archive")
/tmp/lychee/lychee-x86_64-unknown-linux-musl/lychee --offline --include-fragments --no-progress './**/*.md'
conformance:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/windows.yaml@v1
with:
commands: |
pwsh -NoProfile -File ./tools/Test-RepoConformance.ps1 -SelfTest
pwsh -NoProfile -File ./tools/Test-RepoConformance.Fuzz.ps1 -Seed 22023 -Iterations 1000
pwsh -NoProfile -File ./tools/Test-RepoConformance.Fuzz.ps1 -Seed 99173 -Iterations 1000
pwsh -NoProfile -File ./tools/Test-StandardsPackage.ps1
pwsh -NoProfile -File ./tools/Test-RepoConformance.ps1 -Repository KofTwentyTwo/standards -LocalPath . -StaticOnly
@@ -0,0 +1,70 @@
name: Windows signing verification
description: Prepare explicit owned-file catalogs and verify final Authenticode signatures and preserved binary bytes.
inputs:
operation:
description: prepare, verify, or verify-distribution.
required: true
root:
description: Root directory for the files to sign.
default: .
signing-files:
description: Newline-separated required file names or relative path patterns. No blanket dependency signing.
default: ""
preserve-other-binaries:
description: Record and check binaries outside the signing catalog.
default: "false"
expected-subject:
description: Verified certificate subject read from the deployed Azure profile.
default: ""
manifest:
description: Manifest created by prepare; empty uses RUNNER_TEMP/k22-signing-manifest.json.
default: ""
directory:
description: Directory of final packaged assets to verify.
default: ""
outputs:
catalog:
description: File catalog consumed by azure/artifact-signing-action.
value: ${{ steps.run.outputs.catalog }}
manifest:
description: Owned files and third-party preservation manifest.
value: ${{ steps.run.outputs.manifest }}
runs:
using: composite
steps:
- id: run
shell: pwsh
env:
K22_SIGNING_OPERATION: ${{ inputs.operation }}
K22_SIGNING_ROOT: ${{ inputs.root }}
K22_SIGNING_FILES: ${{ inputs.signing-files }}
K22_SIGNING_PRESERVE: ${{ inputs.preserve-other-binaries }}
K22_SIGNING_SUBJECT: ${{ inputs.expected-subject }}
K22_SIGNING_MANIFEST: ${{ inputs.manifest }}
K22_SIGNING_DIRECTORY: ${{ inputs.directory }}
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $env:GITHUB_ACTION_PATH '../../../tools/WindowsSigning.psm1') -Force
$manifest = $env:K22_SIGNING_MANIFEST
if (-not $manifest) { $manifest = Join-Path $env:RUNNER_TEMP 'k22-signing-manifest.json' }
$catalog = Join-Path $env:RUNNER_TEMP 'k22-signing-catalog.txt'
switch ($env:K22_SIGNING_OPERATION)
{
'prepare' {
$patterns = @($env:K22_SIGNING_FILES -split "`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if ($patterns.Count -eq 0) { throw 'Explicit owned signing-files are required.' }
if ([string]::IsNullOrWhiteSpace($env:K22_SIGNING_SUBJECT)) { throw 'The verified publisher subject is required before selecting signing files.' }
New-WindowsSigningCatalog -Root $env:K22_SIGNING_ROOT -Patterns $patterns -CatalogPath $catalog -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT -PreserveOtherBinaries:($env:K22_SIGNING_PRESERVE -eq 'true')
}
'verify' {
Assert-WindowsSigningManifest -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT
}
'verify-distribution' {
Assert-WindowsSigningManifest -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT
Assert-WindowsDistribution -Directory $env:K22_SIGNING_DIRECTORY -ManifestPath $manifest -ExpectedSubject $env:K22_SIGNING_SUBJECT
}
default { throw "Unsupported signing operation: $env:K22_SIGNING_OPERATION." }
}
"catalog=$catalog" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"manifest=$manifest" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+12
View File
@@ -0,0 +1,12 @@
# Plain-language categories for native GitHub release notes.
changelog:
exclude:
labels:
- "autorelease: pending"
- "autorelease: tagged"
- "autorelease: failed"
categories:
- title: Breaking changes
labels: [breaking]
- title: Changes
labels: ["*"]
+80 -2
View File
@@ -38,7 +38,7 @@ jobs:
contents: read
security-events: write # upload SARIF results to code scanning
with:
languages: '["actions"]'
languages: '["actions", "javascript-typescript"]'
docs:
name: docs
@@ -93,6 +93,16 @@ jobs:
shell: pwsh
run: ./tools/Test-RepoConformance.ps1 -SelfTest
- name: Conformance parser fuzzing
shell: pwsh
env:
CI_EVENT_NAME: ${{ github.event_name }}
CI_REF_TYPE: ${{ github.ref_type }}
run: |
$iterations = if ($env:CI_EVENT_NAME -eq 'schedule' -or $env:CI_REF_TYPE -eq 'tag') { 10000 } else { 1000 }
./tools/Test-RepoConformance.Fuzz.ps1 -Seed 22023 -Iterations $iterations
./tools/Test-RepoConformance.Fuzz.ps1 -Seed 99173 -Iterations $iterations
- name: Standards package tests
shell: pwsh
run: ./tools/Test-StandardsPackage.ps1
@@ -102,9 +112,60 @@ jobs:
shell: pwsh
run: ./tools/Test-RepoConformance.ps1 -LocalPath . -StaticOnly
windows-signing:
name: windows-signing
runs-on: windows-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Lint and test Windows signing gates
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Install-Module PSScriptAnalyzer -RequiredVersion 1.25.0 -Scope CurrentUser -Force
Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
Import-Module PSScriptAnalyzer -RequiredVersion 1.25.0
Import-Module Pester -RequiredVersion 5.7.1
# Keep every configured rule; isolate concurrent command-metadata consumers.
# https://github.com/PowerShell/PSScriptAnalyzer/issues/1708
$settings = 'configs/shell/PSScriptAnalyzerSettings.psd1'
$casingFixture = @(Invoke-ScriptAnalyzer -ScriptDefinition "get-content -literalpath 'fixture.txt'" -Settings $settings -IncludeRule PSUseCorrectCasing)
if ('PSUseCorrectCasing' -notin $casingFixture.RuleName) { throw 'The casing pass did not detect incorrect casing.' }
$reservedFixture = @(Invoke-ScriptAnalyzer -ScriptDefinition 'function Get-Fixture? { [CmdletBinding()] param() }; Export-ModuleMember -Function Get-Fixture?' -Settings $settings -IncludeRule PSReservedCmdletChar)
if ('PSReservedCmdletChar' -notin $reservedFixture.RuleName) { throw 'The reserved-character pass did not detect the exported function.' }
$remainingFixture = @(Invoke-ScriptAnalyzer -ScriptDefinition "Write-Host 'fixture'" -Settings $settings -ExcludeRule PSUseCorrectCasing, PSReservedCmdletChar)
if ('PSAvoidUsingWriteHost' -notin $remainingFixture.RuleName) { throw 'The remaining-rule pass did not detect Write-Host.' }
foreach ($path in @('tools/WindowsSigning.psm1', 'tools/WindowsSigning.Tests.ps1', 'tools/VelopackSigning.Tests.ps1'))
{
Write-Output "Analyzing $path"
try
{
$findings = @(
Invoke-ScriptAnalyzer -Path $path -Settings $settings -IncludeRule PSUseCorrectCasing
Invoke-ScriptAnalyzer -Path $path -Settings $settings -IncludeRule PSReservedCmdletChar
Invoke-ScriptAnalyzer -Path $path -Settings $settings -ExcludeRule PSUseCorrectCasing, PSReservedCmdletChar
)
}
catch
{
Write-Output $_.Exception.ToString()
throw
}
if ($findings.Count -gt 0) { $findings | Format-Table; throw "Script analysis failed: $path" }
}
$configuration = New-PesterConfiguration
$configuration.Run.Path = @('tools/WindowsSigning.Tests.ps1', 'tools/VelopackSigning.Tests.ps1')
$configuration.Run.Exit = $true
Invoke-Pester -Configuration $configuration
release-please:
name: release-please
needs: [security, codeql, docs, conformance]
needs: [security, codeql, docs, conformance, windows-signing]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.RELEASE_APP_CLIENT_ID != ''
runs-on: ubuntu-24.04
timeout-minutes: 10
@@ -125,7 +186,24 @@ jobs:
permission-contents: write
permission-pull-requests: write
# Version files merge before publication; wait for the publisher's label.
- name: Check for unpublished merged releases
id: pending
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
pending="$(gh api --paginate "repos/$GH_REPO/pulls?state=closed&per_page=100" --jq '.[] | select(.merged_at != null and any(.labels[]; .name == "autorelease: pending")) | .number')"
if [[ -n "$pending" ]]; then
echo 'can-propose=false' >> "$GITHUB_OUTPUT"
echo "Waiting for publication of merged release PR(s): $pending"
else
echo 'can-propose=true' >> "$GITHUB_OUTPUT"
fi
- name: Propose a semantic release
if: steps.pending.outputs.can-propose == 'true'
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ steps.app.outputs.token }}
+10
View File
@@ -52,6 +52,7 @@ jobs:
- name: PR title is a Conventional Commit header
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# The title becomes the squash commit subject on main (K22-REPO-22), so it
# must be a valid Conventional Commits 1.0 header of at most 100 characters.
@@ -65,6 +66,15 @@ jobs:
printf 'Title was: %s\n' "$PR_TITLE"
exit 1
fi
node - <<'NODE'
const emoji = /\p{Emoji_Presentation}|[\u2600-\u27BF]|\p{Extended_Pictographic}\uFE0F|[#*0-9]\uFE0F?\u20E3/u;
for (const [field, text] of [['title', process.env.PR_TITLE], ['description', process.env.PR_BODY || '']]) {
if (emoji.test(text)) {
console.error(`::error title=Unprofessional pull request::The ${field} contains an emoji; use professional text.`);
process.exit(1);
}
}
NODE
echo "PR title OK"
dco:
+84
View File
@@ -64,6 +64,14 @@ on:
description: Newline-separated test projects to run with coverage before packing.
type: string
default: ""
sign:
description: Authenticode-sign owned Windows binaries before packing, using Azure OIDC in the protected release environment.
type: boolean
default: false
signing-files:
description: Newline-separated required owned binary paths relative to the checkout; required when sign is true.
type: string
default: ""
outputs:
version:
description: The released version (tag without the leading v).
@@ -79,8 +87,10 @@ jobs:
name: build
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 60
environment: release
permissions:
contents: read
id-token: write # Azure OIDC is available only in the protected release environment.
outputs:
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.version.outputs.prerelease }}
@@ -186,12 +196,86 @@ jobs:
}
if ($failed) { exit 1 }
- name: Prepare owned binary signing catalog
if: inputs.sign
id: signing
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: prepare
signing-files: ${{ inputs.signing-files }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
- name: Require verified Azure signing configuration
if: inputs.sign
shell: pwsh
env:
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
ARTIFACT_SIGNING_ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
ARTIFACT_SIGNING_ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
ARTIFACT_SIGNING_PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
ARTIFACT_SIGNING_SUBJECT: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
run: |
foreach ($name in @('AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_SUBSCRIPTION_ID', 'ARTIFACT_SIGNING_ENDPOINT', 'ARTIFACT_SIGNING_ACCOUNT', 'ARTIFACT_SIGNING_PROFILE', 'ARTIFACT_SIGNING_SUBJECT'))
{
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { throw "Missing verified signing variable: $name" }
}
- name: Azure login (OIDC)
if: inputs.sign
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- name: Sign owned binaries before packing
if: inputs.sign
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
endpoint: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
files-catalog: ${{ steps.signing.outputs.catalog }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
cache-dependencies: false
exclude-environment-credential: true
exclude-workload-identity-credential: true
exclude-managed-identity-credential: true
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Verify signed binaries
if: inputs.sign
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: verify
manifest: ${{ steps.signing.outputs.manifest }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
- name: Pack
shell: pwsh
env:
K22_VERSION: ${{ steps.version.outputs.version }}
run: dotnet pack $env:K22_SOLUTION -c Release "-p:Platform=$env:K22_PLATFORM" "-p:Version=$env:K22_VERSION" --no-build -o artifacts/nuget
- name: Verify the exact binaries shipped in NuGet packages
if: inputs.sign
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: verify-distribution
directory: artifacts/nuget
manifest: ${{ steps.signing.outputs.manifest }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
# Maps each package to the project folder whose packages.lock.json describes its
# dependencies, for the SBOM job. Package ids come from the produced file names.
- name: List packages
+10 -3
View File
@@ -2,6 +2,10 @@ name: release-standards
on:
workflow_call:
secrets:
RELEASE_APP_PRIVATE_KEY:
description: App key resolved from the protected release environment.
required: true
permissions: {}
@@ -51,11 +55,13 @@ jobs:
}
echo "pr=$pr" >> "$GITHUB_OUTPUT"
# Preflight is approved before tagging; shipping checks follow publication.
checklist=''
annotation="$(git for-each-ref --format='%(contents)' "refs/tags/$TAG")"
checklist="$(printf '%s\n' "$annotation" | sed -n 's/^Release checklist: //p' | head -n 1)"
if [[ "$version" == *.0 ]]; then
annotation="$(git for-each-ref --format='%(contents)' "refs/tags/$TAG")"
[[ -n "$checklist" ]]
fi
if [[ -n "$checklist" ]]; then
prefix="https://github.com/$GH_REPO/issues/"
checklist="$(printf '%s\n' "$annotation" | grep -F 'Release checklist: ' | head -n 1 | sed 's/^Release checklist: //')"
[[ "$checklist" == "$prefix"* ]]
issue="${checklist#"$prefix"}"
[[ "$issue" =~ ^[1-9][0-9]*$ ]]
@@ -70,6 +76,7 @@ jobs:
uses: ./.github/workflows/ci.yml
permissions:
contents: read
pull-requests: read # required by ci.yml's nested PR caller even on tag events
security-events: write
build:
+263 -19
View File
@@ -26,7 +26,7 @@
# app unpackaged and self-contained, and the optional CLI as a single file.
# package (Windows, environment `release`): optionally Authenticode-sign the
# published binaries with Azure Trusted Signing over OIDC (input `sign`,
# off until exception EX-0002 closes), download the previous release on the
# enabled after repository signing verification), download the previous release on the
# same channel as the delta baseline, `vpk pack` (which signs Setup.exe with
# the same identity when `sign` is on), and zip the CLI. Only this build's
# Velopack assets (from assets.<channel>.json) are kept, with a
@@ -100,7 +100,16 @@ on:
Authenticode-sign binaries and Setup.exe with Azure Trusted Signing (OIDC). Requires the
variables AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, TRUSTED_SIGNING_ENDPOINT,
TRUSTED_SIGNING_ACCOUNT, and TRUSTED_SIGNING_PROFILE in the caller's `release` environment.
Off until exception EX-0002 (signing identity validation) closes.
ARTIFACT_SIGNING_SUBJECT must contain the verified publisher subject. Legacy TRUSTED_SIGNING_*
names remain accepted; ARTIFACT_SIGNING_* names take precedence.
type: boolean
default: false
signing-files:
description: Newline-separated required owned file names or publish-relative patterns, including the optional CLI; required when sign is true.
type: string
default: ""
msi:
description: Generate a per-user MSI alongside Setup.exe and portable ZIP; requires the same signing and verification.
type: boolean
default: false
outputs:
@@ -325,9 +334,34 @@ jobs:
name: publish-cli
path: publish/cli
# Signing is OFF by default: exception EX-0002 records that the Azure Trusted
# Signing identity is still being validated. Until then, integrity and origin rest
# on SHA256SUMS and the build-provenance attestations (K22-REL-04).
- name: Prepare owned binary signing catalog
if: inputs.sign
id: signing
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: prepare
root: publish
signing-files: ${{ inputs.signing-files }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
preserve-other-binaries: "true"
- name: Require verified Azure signing configuration
if: inputs.sign
shell: pwsh
env:
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
ARTIFACT_SIGNING_ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
ARTIFACT_SIGNING_ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
ARTIFACT_SIGNING_PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
ARTIFACT_SIGNING_SUBJECT: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
run: |
foreach ($name in @('AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_SUBSCRIPTION_ID', 'ARTIFACT_SIGNING_ENDPOINT', 'ARTIFACT_SIGNING_ACCOUNT', 'ARTIFACT_SIGNING_PROFILE', 'ARTIFACT_SIGNING_SUBJECT'))
{
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { throw "Missing verified signing variable: $name" }
}
- name: Azure login (OIDC)
if: inputs.sign
uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0
@@ -336,22 +370,52 @@ jobs:
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
# The pinned official action installs this tool with --add-source, which
# conflicts with NuGet package source mapping. Stage its verified package
# in the action's own dependency directory without changing caller policy.
- name: Prepare pinned Artifact Signing CLI dependency
if: inputs.sign
shell: pwsh
run: |
& {
param(
[string]$PackageDirectory = $env:RUNNER_TEMP,
[string]$InstallRoot = $env:LOCALAPPDATA
)
$ErrorActionPreference = 'Stop'
$version = '0.9.1-beta.26227.3'
$package = Join-Path $PackageDirectory "sign.$version.nupkg"
$destination = Join-Path $InstallRoot "ArtifactSigning/sign/sign.$version"
if (Test-Path -LiteralPath $destination) { throw 'Artifact Signing CLI directory already exists; use a fresh release runner.' }
Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/sign/$version/sign.$version.nupkg" -OutFile $package
if ((Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash -ne '34FD0D4AEABDBC363A48883881865B4DD65E6C11BA916028082FA23F3E1B1BA1')
{
throw 'Artifact Signing CLI package checksum mismatch.'
}
dotnet nuget verify $package --all
if ($LASTEXITCODE -ne 0) { throw 'Artifact Signing CLI NuGet signature verification failed.' }
[IO.Compression.ZipFile]::ExtractToDirectory($package, $destination)
if (-not (Test-Path -LiteralPath (Join-Path $destination 'tools/net8.0/any/sign.dll') -PathType Leaf))
{
throw 'Artifact Signing CLI package is missing its executable.'
}
} @args
# Signs the app's own executables and DLLs (and the CLI) before packing, so the
# binaries inside the installer, the portable zip, and the update packages are
# all signed (K22-REL-05).
- name: Sign published binaries (Azure Trusted Signing)
- name: Sign only owned published binaries (Azure Artifact Signing)
if: inputs.sign
uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
endpoint: ${{ vars.TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ vars.TRUSTED_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ vars.TRUSTED_SIGNING_PROFILE }}
files-folder: ${{ github.workspace }}\publish
files-folder-filter: exe,dll
files-folder-recurse: true
endpoint: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
files-catalog: ${{ steps.signing.outputs.catalog }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
timeout: 1800
cache-dependencies: false
exclude-environment-credential: true
exclude-workload-identity-credential: true
@@ -359,25 +423,60 @@ jobs:
exclude-shared-token-cache-credential: true
exclude-visual-studio-credential: true
exclude-visual-studio-code-credential: true
exclude-azure-cli-credential: false
exclude-azure-powershell-credential: true
exclude-azure-developer-cli-credential: true
exclude-interactive-browser-credential: true
- name: Verify signed binaries and preserved dependencies
if: inputs.sign
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: verify
manifest: ${{ steps.signing.outputs.manifest }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
# vpk signs what it adds (Setup.exe and the update stub) with the same identity,
# reading this metadata file and the Azure CLI login above.
- name: Write signing metadata for vpk
if: inputs.sign
shell: pwsh
env:
TRUSTED_SIGNING_ENDPOINT: ${{ vars.TRUSTED_SIGNING_ENDPOINT }}
TRUSTED_SIGNING_ACCOUNT: ${{ vars.TRUSTED_SIGNING_ACCOUNT }}
TRUSTED_SIGNING_PROFILE: ${{ vars.TRUSTED_SIGNING_PROFILE }}
TRUSTED_SIGNING_ENDPOINT: ${{ vars.ARTIFACT_SIGNING_ENDPOINT || vars.TRUSTED_SIGNING_ENDPOINT }}
TRUSTED_SIGNING_ACCOUNT: ${{ vars.ARTIFACT_SIGNING_ACCOUNT || vars.TRUSTED_SIGNING_ACCOUNT }}
TRUSTED_SIGNING_PROFILE: ${{ vars.ARTIFACT_SIGNING_PROFILE || vars.TRUSTED_SIGNING_PROFILE }}
run: |
@{
Endpoint = $env:TRUSTED_SIGNING_ENDPOINT
CodeSigningAccountName = $env:TRUSTED_SIGNING_ACCOUNT
CertificateProfileName = $env:TRUSTED_SIGNING_PROFILE
ExcludeCredentials = @('EnvironmentCredential', 'WorkloadIdentityCredential', 'ManagedIdentityCredential', 'SharedTokenCacheCredential', 'VisualStudioCredential', 'VisualStudioCodeCredential', 'AzurePowerShellCredential', 'AzureDeveloperCliCredential', 'InteractiveBrowserCredential')
} | ConvertTo-Json | Set-Content -LiteralPath "$env:RUNNER_TEMP\trusted-signing.json" -Encoding utf8
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe |
Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName -Descending | Select-Object -First 1
$dlib = Get-ChildItem "$env:LOCALAPPDATA\ArtifactSigning" -Recurse -Filter Azure.CodeSigning.Dlib.dll |
Where-Object { $_.FullName -match '\\x64\\' } | Sort-Object FullName -Descending | Select-Object -First 1
if (-not $signtool -or -not $dlib) { throw 'Installed x64 SignTool or Artifact Signing dlib was not found.' }
foreach ($setting in @{
K22_VPK_SIGN_TOOL = $signtool.FullName
K22_VPK_SIGN_DLIB = $dlib.FullName
K22_VPK_SIGN_METADATA = "$env:RUNNER_TEMP\trusted-signing.json"
}.GetEnumerator())
{
"$($setting.Key)=$($setting.Value)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
}
@'
param([Parameter(Mandatory)][string] $File)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$signature = Get-AuthenticodeSignature -LiteralPath $File
if ($signature.Status -eq 'Valid') { exit 0 }
if ($signature.Status -ne 'NotSigned') { throw "Refusing to replace an invalid signature on $File." }
& $env:K22_VPK_SIGN_TOOL sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 /dlib $env:K22_VPK_SIGN_DLIB /dmdf $env:K22_VPK_SIGN_METADATA $File
if ($LASTEXITCODE -ne 0) { throw "Artifact Signing failed on $File." }
& $env:K22_VPK_SIGN_TOOL verify /pa /all /v $File
if ($LASTEXITCODE -ne 0) { throw "Signature verification failed on $File." }
'@ | Set-Content -LiteralPath "$env:RUNNER_TEMP\sign-one.ps1" -Encoding utf8NoBOM
# The vpk version matches the Velopack NuGet package the apps reference (AppKit).
- name: Install vpk
@@ -399,10 +498,20 @@ jobs:
- name: Velopack pack
shell: pwsh
env:
VPK_MSI: ${{ inputs.msi }}
run: |
$sign = @()
if ($env:K22_SIGN -eq 'true') { $sign = @('--azureTrustedSignFile', "$env:RUNNER_TEMP\trusted-signing.json") }
vpk pack --packId $env:K22_APP_ID --packVersion $env:K22_VERSION --packDir publish/app --mainExe $env:K22_MAIN_EXE --channel $env:K22_CHANNEL --icon $env:K22_ICON --outputDir Releases @sign
if ($env:K22_SIGN -eq 'true')
{
# App/CLI binaries are already signed. Only Velopack's branded helpers
# may be signed inside pack; preserve every other dependency signature.
$exclude = '(?i)[\\/](?!(?:Squirrel\.exe|.*_ExecutionStub\.exe)$)[^\\/]+$'
$command = "pwsh -NoProfile -NonInteractive -File `"$env:RUNNER_TEMP\sign-one.ps1`" `"{{file}}`""
$sign = @('--signTemplate', $command, '--signExclude', $exclude)
}
$env:VELOPACK_TEMP = Join-Path $env:RUNNER_TEMP 'k22-velopack'
vpk pack --runtime $env:K22_RUNTIME --packId $env:K22_APP_ID --packVersion $env:K22_VERSION --packDir publish/app --mainExe $env:K22_MAIN_EXE --channel $env:K22_CHANNEL --icon $env:K22_ICON --outputDir Releases --instLocation PerUser @sign
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# Keeps only what this build produced, as `vpk upload` does: the files listed in
@@ -431,11 +540,139 @@ jobs:
Write-Output "Assets: $($names -join ', '); feed: $feedName; setup: $setup"
"setup=$setup" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
- name: Extract required MSI payload
if: inputs.msi
id: msi
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$installers = @(Get-ChildItem -LiteralPath release -Filter '*.msi' -File)
if ($installers.Count -ne 1) { throw 'Exactly one MSI installer is required.' }
$root = Join-Path $env:RUNNER_TEMP ('k22-msi-' + [guid]::NewGuid().ToString('N'))
"root=$root" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
$image = Join-Path $root 'image'
$assets = Join-Path $root 'assets'
New-Item -ItemType Directory -Path $image, $assets -Force | Out-Null
$start = [Diagnostics.ProcessStartInfo]::new('msiexec.exe')
$start.UseShellExecute = $false
$start.CreateNoWindow = $true
foreach ($argument in @('/a', $installers[0].FullName, '/qn', "TARGETDIR=$image", '/l*v', (Join-Path $root 'extract.log')))
{
$start.ArgumentList.Add($argument)
}
$process = [Diagnostics.Process]::Start($start)
try
{
if (-not $process.WaitForExit(180000)) { $process.Kill(); throw 'MSI extraction timed out.' }
if ($process.ExitCode -ne 0) { throw "MSI extraction failed: $($process.ExitCode)." }
}
finally { $process.Dispose() }
$manifests = @(Get-ChildItem -LiteralPath $image -Recurse -File -Filter sq.version)
if ($manifests.Count -ne 1 -or $manifests[0].Directory.Name -ne 'current') { throw 'Expected one installed MSI application payload.' }
$payload = $manifests[0].Directory.Parent.FullName
[IO.Compression.ZipFile]::CreateFromDirectory($payload, (Join-Path $assets 'msi-Portable.zip'))
"directory=$assets" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
# Reuse the existing archive verifier for MSI's administratively extracted
# payload: every owned/third-party byte and generated launcher is checked.
- name: Verify exact signed MSI payload
if: inputs.msi && inputs.sign
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: verify-distribution
directory: ${{ steps.msi.outputs.directory }}
manifest: ${{ steps.signing.outputs.manifest }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
- name: Verify MSI installs, launches and uninstalls
if: inputs.msi
shell: pwsh
env:
MSI_ROOT: ${{ steps.msi.outputs.root }}
run: |
$ErrorActionPreference = 'Stop'
$msi = @(Get-ChildItem -LiteralPath release -Filter '*.msi' -File)
if ($msi.Count -ne 1) { throw 'Exactly one MSI installer is required.' }
$installer = New-Object -ComObject WindowsInstaller.Installer
$database = $installer.OpenDatabase($msi[0].FullName, 0)
$view = $database.OpenView('SELECT `Value` FROM `Property` WHERE `Property` = ''ProductCode''')
$view.Execute()
$product = $view.Fetch().StringData(1)
if ($product -notmatch '^\{[0-9A-Fa-f-]{36}\}$') { throw 'Invalid MSI product identity.' }
$target = Join-Path $env:MSI_ROOT 'installed'
$application = $null
try
{
$install = Start-Process msiexec.exe -ArgumentList @('/i', ('"' + $msi[0].FullName + '"'), '/qn', ('VELOPACK_INSTALLDIR="' + $target + '"'), '/l*v', ('"' + (Join-Path $env:MSI_ROOT 'install.log') + '"')) -WindowStyle Hidden -PassThru
if (-not $install.WaitForExit(180000)) { $install.Kill(); throw 'MSI installation timed out.' }
if ($install.ExitCode -ne 0) { throw "MSI installation failed: $($install.ExitCode)." }
$executable = Join-Path $target "current/$env:K22_MAIN_EXE"
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) { throw 'MSI did not install the application.' }
$application = Start-Process -FilePath $executable -PassThru
$deadline = [DateTime]::UtcNow.AddSeconds(30)
do
{
$application.Refresh()
if ($application.HasExited) { throw 'MSI-installed application exited before opening its window.' }
if ($application.MainWindowHandle -ne [IntPtr]::Zero) { break }
Start-Sleep -Milliseconds 200
} while ([DateTime]::UtcNow -lt $deadline)
if ($application.MainWindowHandle -eq [IntPtr]::Zero) { throw 'MSI-installed application did not open a window.' }
if (-not $application.CloseMainWindow() -or -not $application.WaitForExit(10000)) { $application.Kill(); throw 'MSI-installed application did not close normally.' }
}
finally
{
if ($null -ne $application -and -not $application.HasExited) { $application.Kill() }
$uninstall = Start-Process msiexec.exe -ArgumentList @('/x', $product, '/qn', '/l*v', ('"' + (Join-Path $env:MSI_ROOT 'uninstall.log') + '"')) -WindowStyle Hidden -PassThru
if (-not $uninstall.WaitForExit(180000)) { $uninstall.Kill(); throw 'MSI uninstall timed out.' }
if ($uninstall.ExitCode -notin 0, 1605) { throw "MSI uninstall failed: $($uninstall.ExitCode)." }
}
if (Test-Path -LiteralPath (Join-Path $target "current/$env:K22_MAIN_EXE")) { throw 'MSI uninstall left the application installed.' }
Write-Output "MSI $product installed, opened its window, closed and uninstalled successfully."
- name: Remove temporary MSI extraction
if: always() && inputs.msi
shell: pwsh
env:
MSI_ROOT: ${{ steps.msi.outputs.root }}
run: |
if ([string]::IsNullOrWhiteSpace($env:MSI_ROOT)) { exit 0 }
$root = [IO.Path]::GetFullPath($env:MSI_ROOT)
$temporaryRoot = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath($env:RUNNER_TEMP))
if ([IO.Path]::GetDirectoryName($root) -ne $temporaryRoot -or [IO.Path]::GetFileName($root) -notmatch '^k22-msi-[0-9a-f]{32}$') { throw 'Unexpected MSI scratch directory.' }
if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
- name: Zip CLI
if: inputs.cli-project != ''
shell: pwsh
run: Compress-Archive -Path publish/cli/* -DestinationPath "release/$env:K22_APP_ID-cli-$env:K22_RUNTIME-$env:K22_VERSION.zip"
- name: Verify final installer, portable, full update and CLI assets
if: inputs.sign
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: verify-distribution
directory: release
manifest: ${{ steps.signing.outputs.manifest }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
# Verification precedes upload, SBOM attestations, hashes and provenance.
- name: Upload signed app output for its SBOM
if: inputs.sign
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signed-publish-app
path: publish/app/
if-no-files-found: error
- name: Upload signed CLI output for its SBOM
if: inputs.sign && inputs.cli-project != ''
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: signed-publish-cli
path: publish/cli/
if-no-files-found: error
- name: Upload release files
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
@@ -459,14 +696,14 @@ jobs:
- name: Download published app
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: publish-app
name: ${{ inputs.sign && 'signed-publish-app' || 'publish-app' }}
path: publish/app
- name: Download published CLI
if: inputs.cli-project != ''
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: publish-cli
name: ${{ inputs.sign && 'signed-publish-cli' || 'publish-cli' }}
path: publish/cli
- name: Download release files
@@ -519,6 +756,13 @@ jobs:
subject-path: release/*-cli-*.zip
sbom-path: sbom/${{ inputs.app-id }}-cli-${{ needs.build.outputs.version }}.cdx.json
- name: Attest MSI SBOM
if: inputs.msi
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: release/*.msi
sbom-path: sbom/${{ inputs.app-id }}-${{ needs.build.outputs.version }}.cdx.json
- name: Upload SBOMs
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
+3
View File
@@ -15,6 +15,9 @@ jobs:
release:
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/release-standards.yml
# Required even when the value exists only in the called job's environment.
secrets:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
permissions:
contents: write
issues: read
+1 -1
View File
@@ -1 +1 @@
{".":"0.1.0"}
{".":"1.2.1"}
+6
View File
@@ -0,0 +1,6 @@
# yamllint configuration for the standards repository: every default rule except
# line length, which K22-CODE-08 leaves unlimited (pinned action SHAs and URLs
# run long).
extends: default
rules:
line-length: disable
+102 -3
View File
@@ -1,9 +1,108 @@
# Changelog
<!-- Generated by Release Please; permit its section spacing. -->
<!-- markdownlint-disable MD012 -->
<!-- Generated by Release Please and GitHub; permit native spacing, links and repeated categories. -->
<!-- markdownlint-disable MD012 MD022 MD024 MD032 MD034 -->
## 0.1.0 (2026-10-10)
## 1.2.1 (2026-10-13)
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### Changes
* fix(releases): record MSI checklist preflight recovery by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/49
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.2.0...v1.2.1
## 1.2.0 (2026-10-13)
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### Changes
* feat(releases): publish signed per-user MSI installers by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/47
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.2...v1.2.0
## 1.1.2 (2026-10-13)
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### Changes
* fix(releases): scope bootstrap parameters under Actions pwsh by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/45
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.1...v1.1.2
## 1.1.1 (2026-10-12)
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### Changes
* docs(inventory): register MarkdownViewr and the Windows starter by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/36
* fix(releases): bootstrap verified Artifact Signing dependency by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/44
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.0...v1.1.1
## 1.1.0 (2026-10-12)
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### Changes
* docs(exceptions): close EX-0002 for gclo by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/33
* docs(exceptions): add EX-0009 to EX-0012 from the repository audit by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/37
* feat(releases): verify Windows Artifact Signing before publication by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/31
* fix(signing): pin reviewed helpers reachable from main by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/39
* fix(ci): isolate racing signing analyzer rules by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/41
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.0.0...v1.1.0
## 1.0.0 (2026-10-10)
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### Breaking changes
* feat(assurance)!: require applicable fuzzing and badge participation by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/19
### Changes
* fix(releases): use professional repository and automation text by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/23
* fix(releases): allow native formatting in generated changelog by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/24
* docs(assurance): record OpenSSF passing badge by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/25
* docs(exceptions): scope temporary frontend audit exception by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/27
* ci(release-velopack): sign only unsigned files; verify before packing by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/29
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v0.1.2...v1.0.0
## [0.1.2](https://github.com/KofTwentyTwo/standards/compare/v0.1.1...v0.1.2) (2026-10-10)
First published standards bundle: policies, language profiles, architectures,
configs, reusable workflows, templates, and tooling. The earlier `0.1.0` and
`0.1.1` versions were never published; their signed tags remain reserved after
the workflow failures documented below.
### Bug Fixes
* **releases:** declare the protected environment secret contract ([#15](https://github.com/KofTwentyTwo/standards/issues/15)) ([08b5ac8](https://github.com/KofTwentyTwo/standards/commit/08b5ac84a7b4d0a06d0b4c5f3ba9e643799084ad))
## 0.1.1 (2026-10-10) — unpublished
The bundle passed CI, checksums, and all five provenance checks, but publication
stopped before draft creation because the reusable caller omitted its environment
secret contract. This signed tag and the `0.1.0` baseline remain reserved.
### Bug Fixes
* **releases:** carry PR read permission through release gates ([#13](https://github.com/KofTwentyTwo/standards/issues/13)) ([e375cf3](https://github.com/KofTwentyTwo/standards/commit/e375cf3b07f6b209324fe9e0be77844bbd0397a2))
## 0.1.0 (2026-10-10) — unpublished
### Features
+6
View File
@@ -22,6 +22,8 @@ themselves: everything below comes from the [SDLC policy](policies/sdlc.md).
## What a good change looks like
- Use professional wording in code comments, documentation, commit/PR messages,
release notes and automation output. No emojis, joke copy or bot sound effects.
- A new or changed requirement follows the format in
[policies/README.md](policies/README.md#requirement-ids): an ID that has never been used,
a level, the rule, *Why*, and *Verified by*. A requirement nobody can verify is not
@@ -41,6 +43,7 @@ Documentation and package checks:
npx markdownlint-cli2 "**/*.md"
lychee --offline --include-fragments --no-progress './**/*.md'
pwsh tools/Test-RepoConformance.ps1 -SelfTest
pwsh tools/Test-RepoConformance.Fuzz.ps1 -Seed 22023 -Iterations 1000
pwsh tools/Test-StandardsPackage.ps1
pwsh tools/Test-RepoConformance.ps1 -LocalPath . -StaticOnly
```
@@ -59,6 +62,9 @@ pwsh tools/Test-RepoConformance.ps1 -Repository KofTwentyTwo/standards -LocalPat
```
The same checks run in CI on every pull request and must pass before merging.
Parser changes must pass both CI seeds and retain any discovered failure as a
deterministic regression test. See [fuzzing](docs/security/fuzzing.md) for extended
campaigns and [badge assessment](docs/security/best-practices.md) for enrollment evidence.
## License of contributions
+2
View File
@@ -24,6 +24,8 @@ conformance version in its README once it passes the conformance checker.
| --- | --- | --- | --- |
| [standards](https://github.com/KofTwentyTwo/standards) | These policies, standards, configs, and reusable workflows | Product | Not yet: rulesets and CI being added |
| [AppKit](https://github.com/KofTwentyTwo/AppKit) | Shared foundation packages for Windows desktop apps | Product | Adoption in progress |
| [MarkdownViewr.Windows](https://github.com/KofTwentyTwo/MarkdownViewr.Windows) | Windows Markdown reader based on Darin Kelkhoff's macOS markdownViewr and AppKit | Product | Adoption in progress: release acceptance, signing and maintainer review pending |
| [windows-app-template](https://github.com/KofTwentyTwo/windows-app-template) | Reusable AppKit Windows app starter with optional CLI and stable/dev release workflows | Product | Adoption in progress: shared defaults and release controls in review |
| [gclo](https://github.com/KofTwentyTwo/gclo) | Clone and update every repository of a GitHub organization (Windows app and CLI) | Product | Adoption in progress: standards gates, governance files, and the Kingsrook layout are in review (gclo #62); settings and exceptions pending |
### Awaiting classification
+23 -13
View File
@@ -7,23 +7,23 @@ reusable GitHub Actions workflows, repository templates, and a conformance check
[![Docs: CC BY 4.0](https://img.shields.io/badge/docs-CC%20BY%204.0-lightgrey)](LICENSE-docs)
[![Code: MIT](https://img.shields.io/badge/code-MIT-green)](LICENSE)
[![Status: draft](https://img.shields.io/badge/status-draft%20(pre--1.0)-orange)](#status)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15366/badge)](https://www.bestpractices.dev/projects/15366)
[![Status: versioned](https://img.shields.io/badge/status-versioned-blue)](#status)
## Status
> **Draft standards, before `v1.0.0`.** The requirements are written and the
> enforcement tooling exists, but no KofTwentyTwo repository has been verified against
> them yet; adoption starts with this repository, then
> [AppKit](https://github.com/KofTwentyTwo/AppKit) and
> [gclo](https://github.com/KofTwentyTwo/gclo). The compliance crosswalks therefore list
> controls as **Defined**, not met, and the response times in the security program are
> targets until `v1.0.0`. See [how these standards work](policies/README.md).
> **Versioned standards with repository-specific adoption.** Immutable releases
> identify each published baseline. This repository has completed repository and CI
> conformance checks; other repositories record their own checks and exceptions.
> Compliance crosswalks describe requirements and evidence for individual controls.
> See [how these standards work](policies/README.md) and the
> [release checklists](https://github.com/KofTwentyTwo/standards/issues?q=is%3Aissue+%22Release+v%22).
---
**Tier:** Product ([scope](policies/README.md#scope)). **Conformance:** this repository is
the first to adopt the standards; it will state *Conforms to KofTwentyTwo standards
v1.0* once the checker passes on it.
**Tier:** Product ([scope](policies/README.md#scope)). **Conformance:** repository and
CI checks reviewed on 2026-10-10: zero FAIL or UNKNOWN, with manual review items
resolved in the release checklist and approved [exceptions](exceptions/register.md).
## How to use
@@ -33,6 +33,11 @@ v1.0* once the checker passes on it.
the [reusable workflows](#use-the-reusable-workflows), and keep it green with the
[conformance checker](#check-a-repositorys-conformance).
Security assurance is recorded in the [Best Practices assessment](docs/security/best-practices.md)
and [parser fuzzing guide](docs/security/fuzzing.md). The repository earned the
[OpenSSF Best Practices passing badge](https://www.bestpractices.dev/projects/15366)
on 2026-10-10 through a maintainer self-assessment.
## Contents
- [How to use](#how-to-use)
@@ -197,13 +202,18 @@ IDE does on save is what CI checks.
`protect-release-tags` rulesets, squash-only merging, the security features
(private vulnerability reporting, secret scanning with push protection, Dependabot,
code scanning, immutable releases), and read-only Actions tokens.
6. **Check.** Run the conformance checker and fix every FAIL:
6. **Assurance.** Complete `docs/security/assurance.json` and its evidence templates.
Public Product repositories register their own Best Practices assessment and
link the actual badge. Identify complex untrusted-input parsers; add their fuzz
commands to a required PR/main CI job and weekly/release campaigns, or document
why none apply. Review scope decisions; copied placeholders do not conform.
7. **Check.** Run the conformance checker and fix every FAIL:
```powershell
pwsh ./tools/Test-RepoConformance.ps1 -Repository KofTwentyTwo/<repo> -LocalPath ../<repo>
```
7. **Declare it.** Add *"Conforms to KofTwentyTwo standards vX.Y"* to the README and
8. **Declare it.** Add *"Conforms to KofTwentyTwo standards vX.Y"* to the README and
list the repository in [MAINTAINERS.md](MAINTAINERS.md#repositories).
### An existing repository
+94
View File
@@ -0,0 +1,94 @@
# Windows release signing
The reusable NuGet and Velopack builders support Azure Artifact Signing over GitHub
OIDC. Reuse an existing validated signing account and active PublicTrust profile;
retrieve the actual tenant GUID, regional account URI, profile name and certificate
subject from Azure before setting variables. Account/domain names and subscription
IDs are not tenant GUIDs. Do not export a private key or use an administrator's
password in CI.
## Trust and configuration
Configure a reviewed Entra application/service principal per repository, or reuse a
suitable existing CI identity while preserving other repositories' credentials.
Use the issuer `https://token.actions.githubusercontent.com`, audience
`api://AzureADTokenExchange` and subject `repo:OWNER/REPOSITORY:environment:release`.
Protect the `release` environment against untrusted refs. Assign the built-in
**Artifact Signing Certificate Profile Signer** at the existing certificate profile
scope; Azure Owner/Contributor is not a signing permission.
Set these environment variables using verified deployed values:
| Variable | Value |
| --- | --- |
| `AZURE_TENANT_ID` | Actual tenant GUID |
| `AZURE_CLIENT_ID` | CI application client ID |
| `AZURE_SUBSCRIPTION_ID` | Signing subscription ID |
| `ARTIFACT_SIGNING_ENDPOINT` | Account's regional HTTPS URI |
| `ARTIFACT_SIGNING_ACCOUNT` | Existing account resource name |
| `ARTIFACT_SIGNING_PROFILE` | Existing active profile name |
| `ARTIFACT_SIGNING_SUBJECT` | Profile certificate's verified subject |
Callers can retain legacy `TRUSTED_SIGNING_ENDPOINT`, `_ACCOUNT`, `_PROFILE` and
`_SUBJECT` names; the new names take precedence. The verified subject is required.
Both builders use official full-SHA-pinned `azure/login` v3 and
`azure/artifact-signing-action` v2, Windows runners and `AzureCliCredential` only.
Signing jobs run in `release` with `contents: read` and `id-token: write`. No
pull-request signing path exists. SHA256 file digests and RFC3161 SHA256 timestamps
at `http://timestamp.acs.microsoft.com` are mandatory when signing is enabled.
## Caller inputs and artifact order
Set `sign: true` and a newline-separated `signing-files` list. Every pattern must
match; missing binaries fail rather than quietly omitting a signature.
Owned and preserved `.exe`/`.dll` headers must parse before a catalog is written;
hash preservation cannot prove an already corrupt source is executable. MSI/MSIX
use their package/signature validation paths rather than PE parsing. For NuGet,
use exact checkout-relative built DLL paths. For Velopack, use publish-relative
paths such as `app/MyApp.exe`, `app/MyApp.dll`, `app/MyApp.Core.dll` and
`cli/myapp.exe`. Include all owned modules. Exclude third-party dependencies.
NuGet signs before `pack --no-build`, then checks the DLLs inside the final packages
against their signed originals. Velopack signs owned published binaries, preserves
other binary hashes, and uses vpk's signing hook only for its branded Squirrel and
execution stubs plus the final Setup/MSI. Final checks validate trust, expected
publisher, timestamps and SHA256 for the shipped binaries, require unchanged
dependency bytes, and verify Setup's embedded full package matches the verified
full update package. Culture-directory satellite assemblies retain their separate
paths. Verification precedes upload, SBOM attestations, checksums and provenance.
Signing or verification failure stops publication when `sign` is true. The default
remains false for compatibility; each owner must enable required signing in their
caller or block publication during migration. NuGet/ZIP/delta containers are not
Authenticode targets. Test reconstructed delta updates on installed channel builds
before claiming upgrade coverage. MSIX presets must use the verified subject as
their manifest Publisher.
## Local tools and proof
Use Microsoft's [current integration instructions](https://learn.microsoft.com/azure/artifact-signing/how-to-signing-integrations)
for Artifact Signing Client Tools, matching-architecture SignTool and
`Azure.CodeSigning.Dlib.dll`, .NET 8 and the Visual C++ runtime. Metadata fields are
`Endpoint`, `CodeSigningAccountName` and `CertificateProfileName`. Authenticate with
an identity that has the profile signer role and sign with `/fd SHA256`,
`/tr http://timestamp.acs.microsoft.com`, `/td SHA256`, `/dlib` and `/dmdf`.
Run `signtool verify /pa /all /v` on final installers and extracted shipped binaries.
Record publisher, trust, timestamp, exact artifact hashes and the CI run. An active
profile, a mocked test or a signed Microsoft tool does not prove an application's
signing works. Retain any signing exception until real end-to-end evidence exists.
## Prepared rollout
This implementation is prepared against published standards v1.0.0. Preserve that
release's metadata, policy changes and signing evidence. Its released Velopack
path lacks the explicit owned-file catalog and its NuGet path lacks these signing
inputs, so it is not sufficient for an AppKit caller that requires them.
The prepared workflows reference helper snapshot `4a7f028976163cb80730126eda1b029005803325`,
which includes source PE parsing and final missing-dependency checks. These references remain
unreleased placeholders until the reviewed helper commit is available on GitHub
and the completed builder is published with its proper version comment. Verify
that exact helper revision, not an older catalog implementation. Keep consumer
publication blocked until the reviewed builder is published, pinned and proven
with representative signed artifacts. Local tests do not authorize rollout.
+3 -2
View File
@@ -38,7 +38,7 @@ this crosswalk is updated when it becomes final.
| **PW.5** Create source code by adhering to secure coding practices | [Coding standards](../standards/coding/README.md), K22-AI-30 |
| **PW.6** Configure build processes to improve executable security | Language profiles (warnings as errors, analyzers, deterministic builds), K22-CI-30 |
| **PW.7** Review and/or analyze human-readable code | K22-SDLC-12, K22-CI-22 |
| **PW.8** Test executable code | [Testing standard](../standards/testing.md) |
| **PW.8** Test executable code | [Testing standard](../standards/testing.md), including K22-TEST-25 parser fuzzing where applicable |
| **PW.9** Configure software to have secure settings by default | K22-SEC-70, K22-SEC-71 |
| **RV.1** Identify and confirm vulnerabilities on an ongoing basis | K22-CI-21, K22-CI-22 (weekly scans), K22-DEP-40, K22-SEC-40 |
| **RV.2** Assess, prioritize, and remediate vulnerabilities | K22-SEC-41, K22-SEC-42 |
@@ -72,4 +72,5 @@ Scorecard checks and the requirement that drives each:
| Vulnerabilities | K22-SEC-41 |
| Binary-Artifacts | K22-REPO-05 |
| Packaging | K22-REL-11 |
| Fuzzing | K22-TEST-23 (where applicable) |
| Fuzzing | K22-TEST-25 (mandatory where applicable); K22-TEST-23 negative-test guidance. Detection supports selected integrations and can miss native harnesses |
| CII-Best-Practices | K22-REPO-12 (public Product enrollment and maintained assessment); actual badge level is assigned by the provider |
+1 -1
View File
@@ -22,7 +22,7 @@ picks them up on open: *Settings → Editor → Code Style* shows **Project** se
## Per IDE (all projects)
1. **Code style:** *Settings → Editor → Code Style → ⚙ (Show Scheme Actions) → Import
1. **Code style:** *Settings → Editor → Code Style → Show Scheme Actions → Import
Scheme → IntelliJ IDEA code style XML* → choose `Kingsrook_Code_Style.xml`.
2. **Reformat on save:** *Settings → Tools → Actions on Save* → enable **Reformat code**
and **Optimize imports**.
+18
View File
@@ -0,0 +1,18 @@
# MSI builder release recovery
The signed v1.2.0 tag is reserved by a preflight failure: its annotation omitted
the required MINOR-release checklist. No1.2.0 assets were published. Preserve
that tag and recover through a new1.2.1 release proposal and signed tag.
The optional MSI implementation is unchanged from merged PR47. Hosted checks
and40 signing regressions pass; native MSI compilation and the actual wrapped
extraction body preserve all6 signed fixture application binaries. Production
MSI signing, installation and distributed-payload verification remain mandatory.
Before tagging the recovery, complete and link the release checklist, retain full
conformance/scanning evidence and verify main/release-proposal gates. Include the
checklist URL in the signed annotation even for this PATCH recovery. Publish only
after its protected deployment and verify all actual bundle hashes/provenance.
This preserves existing EXE callers, signing identity/profile/trust, branch/tag
protections and public1.1.2 assets. No account onboarding is repeated.
+40 -10
View File
@@ -24,10 +24,19 @@ against it before packaging.
Use `feat` for new guidance and `!` for new obligations or removed allowances as
defined in [the versioning policy](../policies/README.md#versioning-of-the-standards).
Infrastructure changes appear in release notes and can produce PATCH releases;
`test`, `style`, and `chore` alone do not produce release notes. Review the proposed
Infrastructure changes can produce PATCH releases;
`test`, `style`, and `chore` alone do not trigger a version proposal. Review the proposed
version and notes before merging. A `breaking` label alone does not change the version.
PR headers and footers are explicitly configured in `release-please-config.json`.
Native GitHub release notes use the plain-language categories in `.github/release.yml`,
keeping breaking changes visible without decorative symbols. Versions still come
from Conventional Commits; note entries link to reviewed PRs. Maintainers review
breaking-change descriptions before publication. Repository text and automation
output use professional wording, without emojis, joke copy or bot sound effects.
Only generated `CHANGELOG.md` permits native spacing, bare links and repeated
category headings; other documentation retains the normal Markdown rules.
## One-time maintainer setup
1. Keep **Settings → General → Releases → Enable release immutability** enabled.
@@ -46,7 +55,9 @@ version and notes before merging. A `breaking` label alone does not change the v
`release` with a maintainer reviewer and deployment access for `v*` tags only.
Store `RELEASE_APP_PRIVATE_KEY` separately in these **environment secrets**;
never as a repository secret or committed file. Environments in the reusable
publisher resolve their own secrets; caller workflows do not pass the key.
publisher resolve their own secrets, but the caller must explicitly pass the
secret name and the reusable workflow must declare it. The named contract does
not expose the tag-only value to caller jobs or nested CI gates.
Keep `protect-main` and `protect-release-tags` intact; the App does not create
tags or bypass review. Allow the SHA-pinned Release Please action if repository
action restrictions require an explicit allowlist entry.
@@ -71,16 +82,18 @@ and Release Please actions are pinned and tracked by Dependabot. See the
```bash
git fetch origin main
git tag -s v0.1.0 <release-pr-merge-sha> \
git tag -s v0.1.2 <release-pr-merge-sha> \
-m "Release checklist: https://github.com/KofTwentyTwo/standards/issues/123"
git push origin v0.1.0
git push origin v0.1.2
```
4. The tag workflow validates ancestry, version files, the pending release PR, and
the checklist; reruns all repository CI gates; builds and attests the assets;
and waits for the `release` environment approval. Publication creates a draft with
all assets and notes, then publishes it. The release PR is marked
`autorelease: tagged` so the next version can be proposed.
`autorelease: tagged` so the next version can be proposed. Version proposals wait
while a merged release PR has `autorelease: pending`; a failed, permanently
retired version uses `autorelease: failed` instead.
These bundles ship no compiled software, so the compiled-artifact SBOM requirement
does not apply. The included workflows/configs retain their upstream pins; source
@@ -93,10 +106,10 @@ Every release includes `KofTwentyTwo-standards-<version>.zip`, `.tar.gz`,
and license split. Download assets from a specific version, then verify:
```bash
gh release download v0.1.0 --repo KofTwentyTwo/standards --dir standards-0.1.0
cd standards-0.1.0
gh release download v0.1.2 --repo KofTwentyTwo/standards --dir standards-0.1.2
cd standards-0.1.2
sha256sum --check SHA256SUMS
gh attestation verify KofTwentyTwo-standards-0.1.0.zip --repo KofTwentyTwo/standards
gh attestation verify KofTwentyTwo-standards-0.1.2.zip --repo KofTwentyTwo/standards
```
On Windows, compare `Get-FileHash -Algorithm SHA256` to the corresponding entry in
@@ -106,7 +119,7 @@ version comment; do not use a moving branch or tag as a workflow pin.
To reproduce from a checkout containing the release commit:
```powershell
pwsh tools/Build-StandardsPackage.ps1 -Version 0.1.0 -Ref v0.1.0 -OutputPath artifacts/rebuild-0.1.0
pwsh tools/Build-StandardsPackage.ps1 -Version 0.1.2 -Ref v0.1.2 -OutputPath artifacts/rebuild-0.1.2
pwsh tools/Test-StandardsPackage.ps1
```
@@ -116,6 +129,12 @@ same Git/archive tool versions. The builder refuses an existing output directory
## Failed releases
The initial `v0.1.0` tag was reserved by a workflow-startup failure; no bundle was
published for it. The `v0.1.1` publisher then stopped before draft creation because
the caller omitted the environment secret contract. Both tags remain reserved;
`v0.1.2` is the first published bundle. Keep these tags unchanged; use a new
version when a tagged workflow needs a source fix.
Never replace published assets or move/reuse a version tag. Fix a published error
with a new release. A failed upload or publication leaves a draft; inspect its run
and assets, then remove **only the unpublished draft** before rerunning the tag's
@@ -126,3 +145,14 @@ from `autorelease: pending` to `autorelease: tagged`; do not rebuild the release
References: [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases),
[enabling immutability](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes),
and [Release Please token behavior](https://github.com/googleapis/release-please-action#other-actions-on-release-please-prs).
## Known limitations
[Issue #22](https://github.com/KofTwentyTwo/standards/issues/22) tracks Windows App SDK
publish resources and the missing shared launch-smoke gate in `release-velopack.yml`.
Windows consumers must keep `WindowsAppSDKSelfContained` and `WindowsPackageType`
consistent between build and publish, or clean intermediate output before publish.
Run smoke tests against the published executable and test the installer on a clean
machine before a stable application release. This remains a consumer-side check
until the shared workflow implements it. The standards bundle contains source and
configuration; it does not contain a compiled Windows application.
+17
View File
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"badge": {
"applicability": "required",
"reason": "Public open-source Product repository distributing standards and executable tooling.",
"projectUrl": "https://www.bestpractices.dev/projects/15366",
"evidence": "docs/security/best-practices.md"
},
"fuzzing": {
"applicability": "required",
"reason": "The conformance checker parses contributor-controlled workflows and exception records; unsafe acceptance can hide failed requirements.",
"targets": ["tools/Test-RepoConformance.ps1"],
"command": "./tools/Test-RepoConformance.Fuzz.ps1",
"workflow": ".github/workflows/ci.yml",
"evidence": "docs/security/fuzzing.md"
}
}
+73
View File
@@ -0,0 +1,73 @@
# OpenSSF Best Practices evidence
## Enrollment and review
**Repository:** <https://github.com/KofTwentyTwo/standards>. **Owner:** @KofTwentyTwo.
**Assessment:** [project 15366](https://www.bestpractices.dev/en/projects/15366/passing).
**Level:** Passing. **Reviewed and earned:** 2026-10-10.
**Next review:** 2027-01-08, or before the next MINOR or MAJOR release, whichever
occurs first. Reassess at least every 90 days.
The public assessment identifies this repository and displays the passing badge.
The maintainer completed the self-assessment, including personal attestations.
This badge records voluntary maintainer self-certification.
The enrollment-only exception [EX-0007](../../exceptions/register.md#ex-0007) is
closed with the actual README badge and project URL recorded in
[assurance.json](assurance.json).
## Criterion evidence matrix
IDs below omit the provider's `0.` level prefix. **Evidence** means source exists;
**Review** marks ongoing operational evidence or human judgment to revalidate;
**Proposed N/A** preserves applicability reasoning for the next review. These are
local evidence notes; the linked public assessment holds the submitted answers.
| Criteria | Evidence and ongoing review | State |
| --- | --- | --- |
| `description_good`, `documentation_basics` | [README](../../README.md) describes standards, tooling and adoption | Evidence |
| `interact`, `discussion`, `report_process`, `report_tracker`, `report_archive` | Public [issues](https://github.com/KofTwentyTwo/standards/issues), templates and [CONTRIBUTING](../../CONTRIBUTING.md) | Evidence |
| `contribution`, `contribution_requirements` | CONTRIBUTING documents proposals, signed commits, DCO, review and gates | Evidence |
| `floss_license`, `floss_license_osi`, `license_location` | Root [MIT](../../LICENSE) for code, configs and templates; [CC BY 4.0](../../LICENSE-docs) for prose; attributed Apache-2.0 material in [NOTICE](../../NOTICE) | Evidence |
| `documentation_interface` | [Tool help and local commands](../../CONTRIBUTING.md#running-the-checks-locally), [release instructions](../releases.md); owner checks shipped tools' help completeness | Review |
| `sites_https`, `delivery_mitm` | GitHub repository and release delivery use HTTPS; [verification](../../standards/releases.md#verifying-a-release) checks provenance | Evidence |
| `english` | Repository documentation and contribution guidance are in English | Evidence |
| `maintained`, `report_responses`, `enhancement_responses` | Recent PRs/issues exist; a new project has limited response history. Assess actual responsiveness, not a policy promise | Review |
| `repo_public`, `repo_track`, `repo_interim`, `repo_distributed` | Public Git repository retains reviewed changes; signed history and protected main | Evidence |
| `version_unique`, `version_semver`, `version_tags`, `release_notes` | Immutable [v0.1.2](https://github.com/KofTwentyTwo/standards/releases/tag/v0.1.2), committed version and [changelog](../../CHANGELOG.md); failed unpublished tags are reserved | Evidence |
| `release_notes_vulns` | [Release standard](../../standards/releases.md) requires security fix notes; no published vulnerability fix history to attest to yet | Review |
| `vulnerability_report_process`, `vulnerability_report_private` | [SECURITY](../../SECURITY.md) links private GitHub reporting and supported versions | Evidence |
| `vulnerability_report_response` | Maintainer confirmed actual response compliance on 2026-10-10 in the public assessment; revalidate report history, not just SECURITY targets | Review |
| `build`, `build_common_tools`, `build_floss_tools` | [Bundle builder](../../tools/Build-StandardsPackage.ps1) uses Git archives and PowerShell; prerequisites and release steps are documented | Evidence |
| `test`, `test_invocation`, `test_policy`, `tests_are_added`, `tests_documented_added` | CONTRIBUTING requires behavioral regression tests and documents checker, package and fuzz commands | Evidence |
| `test_continuous_integration` | [CI](../../.github/workflows/ci.yml) runs tests on PR/main, schedules and tag publication gates | Evidence |
| `test_most` | Parser properties, helper self-tests and package tests exist; measured whole-tool coverage is not established | Suggested: Unmet |
| `warnings`, `warnings_fixed` | Markdown/workflow gates reject findings; revalidate applicable PowerShell warning handling | Review |
| `warnings_strict` | Maximal PowerShell warning/linter coverage across shipped tooling is not established by Actions CodeQL | Suggested: Unmet |
| `know_secure_design`, `know_common_errors` | Primary maintainer personally attested on 2026-10-10; supporting [threat model](threat-model.md), [security policy](../../policies/security-program.md) and parser regressions remain review evidence | Review |
| `crypto_published`, `crypto_call`, `crypto_floss`, `crypto_keylength`, `crypto_working`, `crypto_weaknesses` | No custom product cryptography. GitHub TLS, SSH signing and attestations are delegated mechanisms; assess whether delegation meets each criterion or permits N/A | Review |
| `crypto_pfs` | No application server or product-controlled TLS configuration; document the hosted-service boundary if N/A is permitted | Proposed N/A |
| `crypto_password_storage` | No application passwords, password database or credential-storage implementation | Proposed N/A |
| `crypto_random` | The seeded fuzz PRNG generates only synthetic tests, never security keys/nonces. Cryptographic randomness is delegated to signing/hosting providers | Review |
| `delivery_unsigned` | Release assets have checksums, verified source, build provenance and GitHub immutable-release attestation; see [verification commands](../releases.md) | Evidence |
| `vulnerabilities_fixed_60_days`, `vulnerabilities_critical_fixed` | [Security program](../../policies/security-program.md#vulnerability-management) defines deadlines; maintainer must check current findings and known unpatched issues before answering | Review |
| `no_leaked_credentials` | Required [gitleaks and secret scans](../../.github/workflows/security.yml); scans support, but cannot prove, a universal absence claim | Review |
| `static_analysis`, `static_analysis_common_vulnerabilities`, `static_analysis_often` | [CodeQL Actions](../../.github/workflows/codeql.yml), zizmor and actionlint in required security gates; document PowerShell analysis coverage accurately | Evidence |
| `static_analysis_fixed` | Gates reject applicable findings; review unresolved alerts and [Scorecard dispositions](scorecard-dispositions.md) before claiming all important findings fixed | Review |
| `dynamic_analysis`, `dynamic_analysis_enable_assertions` | Executed [fuzz properties](fuzzing.md), deterministic helper and isolated package tests; assertions fail the job | Evidence |
| `dynamic_analysis_unsafe` | Shipped tooling is managed PowerShell, not unsafe native-language implementation; owner validates applicability | Proposed N/A |
| `dynamic_analysis_fixed` | Generated-input findings gained deterministic regressions; review all future campaign failures before assessment updates | Review |
## Maintaining the assessment
1. Review the public answers against current criteria and repository evidence by the
next review date and before each MINOR or MAJOR release.
2. Reconfirm human knowledge, unresolved findings and actual response history with
the maintainer; update answers when evidence or applicability changes.
3. Keep the provider's live badge, project URL and this review record synchronized.
Downstream repositories need their own assessment and badge.
4. Improve the unmet suggested criteria `test_most` and `warnings_strict`; passing
does not establish whole-tool coverage or maximal warning coverage. Track the
shared release smoke-test gap in [issue #22](https://github.com/KofTwentyTwo/standards/issues/22).
Downstream repositories use the [assessment template](../../templates/repo/docs/security/best-practices.md)
and their own evidence and project ID; EX-0007 does not apply to them.
+50
View File
@@ -0,0 +1,50 @@
# Conformance parser fuzzing
## Scope and properties
K22-TEST-25 applies to shipped tooling even though most repository content is prose.
`tools/Test-RepoConformance.Fuzz.ps1` generates synthetic workflow and exception input
for the pure helpers in `tools/Test-RepoConformance.ps1`. It varies quotes, indentation,
comments, LF/CRLF, scalar indicators, action references and attacker expressions.
Independent construction supplies the expected security decision: only full SHA
pins pass; credential persistence and untrusted expressions are detected; expressions
in environment fields remain outside script bodies; exceptions match their exact
scope and must be open and unexpired. Bounded malformed ASCII inputs also exercise
parser robustness. These properties test unsafe acceptance, not only crashes.
This is native generated-input fuzzing, not coverage-guided instrumentation or a
complete YAML validator. Zizmor remains the authoritative workflow security gate.
Scorecard currently detects selected integrations; it may continue reporting zero
for this PowerShell harness. Do not claim OSS-Fuzz or ClusterFuzzLite enrollment.
## Running and replaying
PowerShell 7.4 or later is the only dependency; no network or profile is used.
```powershell
pwsh -NoProfile -File tools/Test-RepoConformance.Fuzz.ps1 -Seed 22023 -Iterations 1000
pwsh -NoProfile -File tools/Test-RepoConformance.Fuzz.ps1 -Seed 99173 -Iterations 1000
# Weekly and pre-MINOR/MAJOR campaigns: run both seeds at 10000 iterations.
pwsh -NoProfile -File tools/Test-RepoConformance.Fuzz.ps1 -Seed 22023 -Iterations 10000
pwsh -NoProfile -File tools/Test-RepoConformance.Fuzz.ps1 -Seed 99173 -Iterations 10000
```
Failures print synthetic input, property, iteration and seed as JSON. Replay with
that seed and at least that iteration count against the same checkout and PowerShell
runtime. Minimize the input into a named `-SelfTest` regression, then fix the parser
and rerun both seeds. Report suspected vulnerabilities through [SECURITY.md](../../SECURITY.md).
Inputs are bounded to small workflow fragments and fewer than 128 random ASCII
characters; iterations are limited to 100000. Generated scripts are never executed.
The harness does not write files, launch tools, or request repository credentials.
The required `ci / conformance` job runs both seeds on PRs and `main`, and 10000
iterations per seed on weekly schedules and release-tag validation, within its
10-minute timeout. Future failing seeds must be retained alongside regression cases.
## Review record
- **Owner:** @KofTwentyTwo; **reviewed:** 2026-10-10; **next review:** 2027-01-08.
- Seed 22023 exposed quoted-checkout and explicit-indentation block-scalar gaps;
both now have deterministic self-test regressions. This is not a coverage claim.
- Reassess targets when parser boundaries change; generated data alone does not
establish completeness or substitute for ordinary package and checker tests.
+8 -2
View File
@@ -14,8 +14,14 @@ changes or predict future maintenance.
Keep the accepted risk visible; dismissal is not a claim that the missing second
maintainer exists. Do not disable Scorecard or weaken protections. Reopen and
reassess these dispositions when their assumptions change. The LOW best-practices
badge and MEDIUM fuzzing recommendations remain open for follow-up.
reassess these dispositions when their assumptions change. The
[badge assessment](best-practices.md) records the passing badge for
[project 15366](https://www.bestpractices.dev/projects/15366), earned on 2026-10-10;
EX-0007 is closed. Let the next Scorecard scan reassess the badge finding rather
than claiming a new score before it runs. The MEDIUM fuzzing recommendation remains
visible: [native parser fuzzing](fuzzing.md) supplies generated security properties and
regressions. Scorecard may not recognize this PowerShell harness; do not label a
score of zero as proof that tests did not run or claim a supported integration.
No OpenVEX statement is fabricated for these process findings: no vulnerable shipped
component or exploit was identified. Any actual vulnerability present but not
+34 -1
View File
@@ -1,6 +1,6 @@
# Threat model: standards distribution
- **Version reviewed:** v0.1.0 release-bundle implementation
- **Version reviewed:** v1.0.0 standards baseline
- **Last updated:** 2026-10-10
- **Status:** Implementation reviewed; provisioning is verified in the release checklist.
@@ -71,6 +71,39 @@ trust boundaries change.
## Review log
- **Prepared AppKit signing extension, 2026-10-10:** Explicit owned-file selection
excludes third-party binaries from re-signing, retains their hashes and requires
their presence in final archives. Catalog preparation parses owned/preserved
`.exe`/`.dll` headers before writes; already corrupt dependencies cannot pass
solely because hashes are unchanged. Final owned binaries and installers require
publisher, trust, SHA256 and timestamp checks before upload or publication. The
SignTool packaging wrapper preserves valid signatures and refuses invalid ones.
NuGet signs before packing and checks embedded DLL bytes. Existing protected
release-environment OIDC boundaries remain; real AppKit identity/profile access
and signatures are still unproved. The helper reference and consumer rollout
remain pending review/publication; this entry is not deployment evidence.
- **2026-10-10, v1.0.0:** Reviewed changes since v0.1.2: mandatory applicable badge
participation and parser fuzzing, completed passing self-assessment, professional
release text, scoped frontend audit exception, and unsigned-file-only Windows
signing with verification before packing. Distribution credentials, channels,
environment restrictions and provenance boundaries remain unchanged. Badge
metadata adds a public provider link; fuzzing executes synthetic local inputs.
Signing continues to use the Windows consumer's existing Azure OIDC boundary.
The most likely release interruption remains a failed gate or unpublished draft;
recovery preserves tags and immutable assets. The highest-impact distribution
threat remains a compromised credential or pinned Action, constrained by the
existing scoped tokens, protected environments and required checks. Windows
publish/resource and launch-smoke limitations remain tracked in
[issue #22](https://github.com/KofTwentyTwo/standards/issues/22); consumer workarounds
and rehearsal expectations are recorded in [release guidance](../releases.md#known-limitations).
- **2026-10-10, v0.1.2:** Reviewed the explicit reusable-workflow secret contract
after the unpublished v0.1.1 publisher failed. Only the named App key is passed;
its value remains restricted to the reviewer-protected tag environment. Nested
CI receives no secrets, and permissions, interfaces, and recovery boundaries
remain unchanged. Failed version tags stay reserved.
- **2026-10-10, v0.1.0:** Reviewed source-only packaging, release-PR validation,
preflight approval, App token scopes, environment restrictions, provenance, and
immutable publication. Main and tag protections remain in place. Most likely
+262 -1
View File
@@ -6,11 +6,20 @@ Open and closed exceptions to the KofTwentyTwo standards. The process is in
| ID | Status | Requirements | Scope | Expires |
| --- | --- | --- | --- | --- |
| [EX-0001](#ex-0001) | Open | K22-REPO-20 (approval count), OSPS-QA-07.01 | All repositories | When a second maintainer joins; reviewed 2027-10-09 |
| [EX-0002](#ex-0002) | Open | K22-REL-05 (Authenticode signing of Windows binaries) | Windows desktop apps (gclo, AppKit samples, future apps) | When the Azure Trusted Signing identity is validated, at the latest 2027-04-09 |
| [EX-0002](#ex-0002) | Open (closed for gclo 2026-10-11) | K22-REL-05 (Authenticode signing of Windows binaries) | Windows desktop apps not yet on `sign: true` (AppKit samples, future apps) | When each app turns on `sign: true` in the shared release workflow, at the latest 2027-04-09 |
| [EX-0003](#ex-0003) | Open | K22-DEP-02 (license allowlist) | gclo | Reviewed yearly, next on 2027-10-09 |
| [EX-0004](#ex-0004) | Open | K22-CI-30, K22-REL-04 (shared release workflow) | gclo | When the shared reusable release workflow ships, at the latest 2027-04-09 |
| [EX-0005](#ex-0005) | Open | K22-AI-10 (agents via PR only), K22-AI-11 (in part: unconfirmed pushes) | second-brain, nix, Praetor, Jellyfin | Reviewed yearly, next on 2027-10-10 |
| [EX-0006](#ex-0006) | Open | K22-CI-16 (tag-only environment secrets) | standards | 2027-04-10; review 2027-01-10 |
| [EX-0007](#ex-0007) | Closed | K22-REPO-12 (enrollment) | KofTwentyTwo/standards | Closed 2026-10-10 |
| [EX-0008](#ex-0008) | Open | K22-CI-21 (HIGH dependency finding) | Website-Frontend, development-only braces 3.0.3 | 2026-11-10; review 2026-10-17 |
| [EX-0009](#ex-0009) | Open | K22-REPO-20 (linear history), K22-REPO-22 (squash only) | Website-Backend, Website-Frontend, Munitor, cluster-gitops | Reviewed yearly, next on 2027-10-12 |
| [EX-0010](#ex-0010) | Open | K22-CI-01, K22-CI-10 to K22-CI-24, K22-CI-30 (GitHub Actions gates and reusable workflows) | Website-Backend, Website-Frontend, Munitor | 2027-04-12; review 2027-01-12 |
| [EX-0011](#ex-0011) | Open | K22-CODE-TS-01, K22-CODE-SH-02 (Kingsrook 3-space layout) | Website-Frontend (TypeScript), Munitor (shell) | 2027-04-12; review 2027-01-12 |
| [EX-0012](#ex-0012) | Open | K22-CODE-TF-04 (state never committed), K22-CODE-TF-05 (no secrets in `.tfvars`) | Praetor | Reviewed yearly, next on 2027-10-12 |
| [EX-0013](#ex-0013) | Open | K22-DEP-02 automated license classification | QRun-IO/qbit-printing, unmodified H2 2.4.240 only | 2027-04-30 or any H2 version/license change, whichever occurs first |
| [EX-0014](#ex-0014) | Proposed, awaiting approval | K22-DEP-02 (Microsoft SDK terms and classification) | QRun-IO/qqq-print-agent, exact 15-package Windows SDK inventory below | 2027-04-30 or any reviewed version/license/scope change, whichever occurs first |
| [EX-0015](#ex-0015) | Proposed, awaiting approval | K22-DEP-02 (build-tool license classification) | QRun-IO/qqq-print-protocol and QRun-IO/qqq-print-agent, development-only `uri-js` 4.4.1 only | 2027-04-30 or any `uri-js` version/license/scope change, whichever occurs first |
## EX-0001
@@ -59,6 +68,13 @@ Open and closed exceptions to the KofTwentyTwo standards. The process is in
EX-0004); releases before that have neither.
- **Expires:** when the signing identity is validated and the shared release workflow
signs Windows binaries; at the latest 2027-04-09.
- **Closed for gclo:** 2026-10-11. The signing identity (Azure Artifact Signing account
`kof22signing`, Public Trust profile, publisher "James Maes") is validated and the
shared workflow signs on `sign: true` (standards#28/#29): gclo v1.0.2-beta.3,
v1.1.0-beta.1 and v1.1.0 ship Setup.exe, Update.exe and every exe/dll
Authenticode-signed (SHA-256, RFC 3161), verified with `signtool verify /pa /all` by
gclo's `verify-signatures` job and by hand (289 files, 0 failures). The exception
stays open only for Windows apps that have not turned `sign: true` on yet.
- **Owner:** the maintainer.
## EX-0003
@@ -169,3 +185,248 @@ Open and closed exceptions to the KofTwentyTwo standards. The process is in
- **Expires:** 2027-04-10, or earlier when version proposals use credential federation.
- **Owner:** the maintainer.
- **Review date:** 2027-01-10.
## EX-0007
**Best Practices enrollment temporarily blocked by provider anti-spam**
- **Requirements:** `K22-REPO-12` enrollment and the actual README badge only.
- **Scope:** `KofTwentyTwo/standards`; no downstream repository is covered.
- **Reason:** on 2026-10-10 the maintainer could not sign in because of the provider's
anti-spam restriction and explicitly deferred enrollment. No project ID existed
when this exception was approved.
- **Risk:** consumers could not inspect a registered provider assessment during the
enrollment deferral.
- **Compensating controls (while open):** publish the [criterion evidence](../docs/security/best-practices.md)
and pending answers; retain existing security/PR gates and parser fuzzing. Do not
claim participation or a passing badge. Retry normal login once the restriction
clears, register the exact repository URL, and close this exception through a PR.
- **Expires:** 2026-11-10 (UTC), or successful enrollment and README badge publication,
whichever occurs first. **Review:** 2026-10-17.
- **Owner:** @KofTwentyTwo. Approval is recorded by the normal exception-register PR;
this entry does not authorize bypassing CI or publishing an unreviewed assessment.
- **Closed:** 2026-10-10. [Project 15366](https://www.bestpractices.dev/projects/15366)
identifies this repository and has earned the passing badge. The actual linked
badge is in [README](../README.md), the project URL is in
[assurance.json](../docs/security/assurance.json), and the
[assessment record](../docs/security/best-practices.md) sets the next review date.
## EX-0008
**Temporary development dependency audit exception for Website-Frontend**
- **Requirements:** `K22-CI-21` blocks HIGH and CRITICAL findings in the resolved
dependency graph.
- **Scope:** `KofTwentyTwo/Website-Frontend` only; development-only `braces` 3.0.3,
[GHSA-vfj7-8cjw-p6xm](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm)
/ CVE-2026-93687 and npm's propagated findings for that same advisory. No other
advisory, runtime dependency or CRITICAL finding is covered.
- **Reason:** the Next ESLint tooling depends on the affected package and no patched
npm release exists as of 2026-10-10. npm suggests an unrelated parent downgrade;
that does not establish a patched braces release.
- **Risk:** a deliberately deeply nested caller-supplied brace pattern can exhaust
the stack and stop a lint/build process. Website requests and article Markdown do
not control the lint glob configuration. The dependency remains vulnerable.
- **Compensating controls:** the complete audit still runs. A separate production
audit must have zero HIGH/CRITICAL findings. The exception checker validates the
advisory ID, package, locked version, development-only package paths and expiry;
rejects other HIGH/CRITICAL findings, malformed reports and audit errors; and
requires reassessment if the registry reports a new braces release. Lint, tests,
build, secret scanning, SAST and Trivy remain enabled. No global scanner exclusion
is permitted. Website-Frontend configuration cites this ID.
- **Expires:** 2026-11-10 at 00:00 UTC, or a patched upstream release, whichever
occurs first. A newer upstream release blocks acceptance pending reassessment.
- **Owner:** James Maes, repository maintainer.
- **Review date:** 2026-10-17. Remove the consumer exception and close this entry once
the affected dependency is replaced or patched.
## EX-0009
**GitFlow with merge commits for Munitor-released and GitOps repositories**
- **Requirements:** `K22-REPO-20` (linear history on `main`) and `K22-REPO-22`
(squash-only merges).
- **Scope:** `KofTwentyTwo/Website-Backend`, `KofTwentyTwo/Website-Frontend`,
`KofTwentyTwo/Munitor` (`develop` and `main`, `feature/*` and `hotfix/*` branches)
and `KofTwentyTwo/cluster-gitops` (`main`, merge commits).
- **Reason:** these repositories run GitFlow with merge commits. In Website-Backend
and Website-Frontend, `feature/*` and `hotfix/*` branches (and `develop`
promotions) merge to `main`, which releases and deploys through Munitor, and a
`main` to `develop` back-merge follows. Munitor itself merges `feature/*` into
`develop`, `release/X.Y.Z` into `main`, back-merges `main` into `develop`, and a
`vX.Y.Z` tag publishes the orb. Squash
or rebase merges break the ancestry between `develop` and `main` that the pipeline
relies on. cluster-gitops keeps merge commits so each change set applied by ArgoCD
stays one reviewable unit next to the automated image-bump commits.
- **Risk:** non-linear history is harder to bisect and to read, and a merge commit can
carry several commits that were never reviewed individually.
- **Compensating controls:** every human change to the Website repositories and
Munitor reaches `main` through a pull request; repository rulesets require signed
commits and the CircleCI checks on `main` and `develop`. cluster-gitops (Internal
tier, no CI) requires signed commits and blocks deletion and force pushes on `main`;
the Munitor deploy step writes image bumps to it directly as GitHub-signed commits
(GraphQL `createCommitOnBranch`), with no bypass actor; release tags stay protected
(`K22-REPO-21`); merged branches are deleted.
- **Expires:** reviewed yearly, next on 2027-10-12, or earlier if Munitor gains a
squash-compatible release mode.
- **Owner:** the maintainer.
## EX-0010
**CircleCI instead of the GitHub Actions reusable workflows**
- **Requirements:** `K22-CI-01` (required checks run by the reusable workflows),
`K22-CI-10` to `K22-CI-24` as written for GitHub Actions, and `K22-CI-30`
(releases built by the shared reusable release workflow).
- **Scope:** `KofTwentyTwo/Website-Backend`, `KofTwentyTwo/Website-Frontend` and
`KofTwentyTwo/Munitor` (the CircleCI orb that builds, releases and deploys them).
- **Reason:** these repositories build, publish container images and deploy to the
k8s-galaxy cluster through the Munitor CircleCI orb, which predates the reusable
GitHub Actions workflows and holds the deploy integration (image publishing,
cluster-gitops version bumps). Porting the pipeline is a separate project.
- **Risk:** the gates are implemented per orb rather than by the audited reusable
workflows, so a gate can be missing or weaker than the standard (for example
CodeQL, zizmor, Scorecard and SLSA provenance have no CircleCI equivalent here).
- **Compensating controls:** the CircleCI checks are required by the repository
rulesets (Website: `build-and-test`, `secrets-scan`, `sast-scan`; Munitor:
`orb-tools/lint`, `pack`, `review`, `test`); the orb runs the test suite, gitleaks and Trivy where configured;
GitHub-side controls still apply (secret scanning with push protection, Dependabot
alerts and security updates, code scanning default setup where available); orbs are
pinned to exact versions; signed commits are required.
- **Expires:** 2027-04-12, or when the repositories move to the reusable workflows;
review 2027-01-12.
- **Owner:** the maintainer.
## EX-0011
**Website-Frontend and Munitor keep 2-space formatting**
- **Requirements:** `K22-CODE-TS-01` (Kingsrook layout through the KofTwentyTwo ESLint
configuration: 3-space indent, Allman braces) and `K22-CODE-SH-02` (3-space shell
indentation).
- **Scope:** `KofTwentyTwo/Website-Frontend` TypeScript, JavaScript, CSS and JSON
sources; `KofTwentyTwo/Munitor` Bash scripts and templates.
- **Reason:** the Next.js code base was written in the framework's default 2-space
layout and is formatted by Prettier 3 (with `prettier-plugin-tailwindcss`) plus
ESLint 9 with `eslint-config-next`, which the TypeScript profile does not use.
Munitor's 100+ orb scripts and templates use 2-space indentation. Reformatting
either is a large whitespace-only diff that triggers releases (a Website deploy or
a new orb version for every consumer) and obscures history.
- **Risk:** inconsistent layout across KofTwentyTwo TypeScript code; no functional
risk.
- **Compensating controls:** `.editorconfig` comes from the standard, with only the
indent size overridden for these file types and a comment citing this exception;
the existing lint, type and orb checks still gate CI.
- **Expires:** 2027-04-12, or when the code base is reformatted and adopts
`configs/typescript/eslint.config.mjs` (Website-Frontend) and the orb scripts are
reformatted with shfmt (Munitor); review 2027-01-12.
- **Owner:** the maintainer.
## EX-0012
**Praetor commits git-crypt-encrypted OpenTofu state and variable files**
- **Requirements:** `K22-CODE-TF-04` (state is never committed; remote state with
locking and encryption) and `K22-CODE-TF-05` (no secrets in `.tfvars` files).
- **Scope:** `KofTwentyTwo/Praetor` (Internal tier, private): the local-backend roots
under `terraform/` and `unifi/` (17 `*.tfstate*` files) and their 10 `*.tfvars`
files.
- **Reason:** Praetor provisions the homelab that would host any remote state backend
(the cluster, the NAS and Infisical are themselves managed here), so a remote backend
creates a bootstrap dependency on the infrastructure it builds. Committed, encrypted
local state is the recovery path when that infrastructure is down.
- **Risk:** state and variable files contain credentials; anyone holding the git-crypt
key or a decrypted clone can read them. Local state has no locking, so two
concurrent applies could corrupt it.
- **Compensating controls:** `.gitattributes` encrypts every `*.tfstate*`, `*.tfvars*`,
kubeconfig, talosconfig and `*secret*` path with git-crypt (GPG key held by the
maintainer); `git-crypt status` reports no unencrypted secret files; the repository
is private; applies are human-gated by the maintainer in session (`EX-0005`), which
serializes them; secrets are moving into Infisical with `sensitive = true` outputs.
- **Expires:** reviewed yearly, next on 2027-10-12, or when the roots move to an
encrypted remote backend.
- **Owner:** the maintainer.
## EX-0013
**H2 2.4.240 license metadata classification in QQQ Printing**
- **Requirements:** `K22-DEP-02` permits MPL-2.0 when used unmodified. This is an exception to the automated dependency-review classification for one already-permitted dependency, not permission to introduce an incompatible license or skip vulnerability review. The pinned `pr.yml` input requires this register entry for a package-specific license exception.
- **Scope:** `QRun-IO/qbit-printing` only; Maven package `com.h2database:h2`, unmodified version **2.4.240**, runtime JAR SHA-256 `29b70e427cc1c40cdc376283adbb0cc62853073797bb5fe5761f81fe73d57ce0`. No other package, version, repository or modified H2 binary is covered.
- **Reason:** GitHub dependency review reports `LicenseRef-scancode-unknown AND MPL-2.0`. The tagged upstream license expressly offers MPL-2.0 **or** EPL-1.0; this runtime uses the MPL-2.0 option. Published 2.3.232 and 2.4.240 POMs declare the same terms. Upgrading to 2.4.240 fixes confirmed close-time compaction data loss; reverting to the older driver is not an acceptable metadata workaround.
- **Risk:** The scanner cannot accurately classify this artifact's license. A package-level exclusion could otherwise hide a later license change because the pinned action ignores PURL versions. The reviewed binary remains unmodified and retains its license/attribution; this record does not waive the applicable redistribution terms.
- **Compensating controls:** The caller passes only `pkg:maven/com.h2database/h2` to `allow-dependencies-licenses`; every other package retains its existing license checks. The required `security / sca` Maven inventory guard rejects missing H2, any version other than 2.4.240, and multiple H2 versions, then verifies the complete selected runtime graph against scanner inventories. The runtime payload verifies every copied JAR's SHA-256 and contains exactly one H2 driver. The POM pins the reviewed version. `license-check: true`, `vulnerability-check: true`, low-or-higher dependency advisory review, Trivy severity gates, OSV malicious-package checks and all existing required checks remain enabled. H2 attribution and corresponding source access are documented in NOTICE and the repository license review. Any H2 upgrade requires renewed license review and guard/record updates before release.
- **Expires:** 2027-04-30, or an H2 version/license change or correction of GitHub's metadata classification, whichever occurs first. Remove or renew through the normal standards PR process; do not broaden the exclusion silently.
- **Owner:** the maintainer of `QRun-IO/qbit-printing`.
- **Review date:** 2026-10-30.
- **Evidence:** [H2 tagged license](https://github.com/h2database/h2database/blob/version-2.4.240/LICENSE.txt), [tagged license FAQ](https://github.com/h2database/h2database/blob/version-2.4.240/h2/src/docsrc/html/license.html), [published 2.4.240 POM](https://repo.maven.apache.org/maven2/com/h2database/h2/2.4.240/h2-2.4.240.pom), [published 2.3.232 POM](https://repo.maven.apache.org/maven2/com/h2database/h2/2.3.232/h2-2.3.232.pom), [failed dependency-review run](https://github.com/QRun-IO/qbit-printing/actions/runs/41160240342), [upstream compaction fix](https://github.com/h2database/h2database/pull/4249), [pinned workflow input](https://github.com/KofTwentyTwo/standards/blob/fdf5ed029603c938615a32b1beca3628b8787945/.github/workflows/pr.yml), [pinned version-ignoring matcher](https://github.com/actions/dependency-review-action/blob/a1d282b36b6f3519aa1f3fc636f609c47dddb294/src/purl.ts).
## EX-0014
**Exact Microsoft Windows SDK dependencies in QQQ Print Agent**
- **Status:** Open upon merge of this record through the normal protected PR process. The maintainer accepted the complete 15-package scope and its Microsoft custom terms on 2026-11-01; the earlier two-package proposal is superseded.
- **Requirements:** `K22-DEP-02`, for the Microsoft terms outside the standard license allowlist and their automated classification. This is not merely a metadata correction for an already-permitted SPDX license. No vulnerability, provenance, signing, attestation or branch-protection requirement is waived.
- **Scope:** `QRun-IO/qqq-print-agent` only, unmodified Windows x64 use of the exact package/version/raw-NUPKG-SHA-256 tuples below, including the source-linked native test host. BuildTools and MSIX tooling remain build-only. The implicit SDK reference pack also supplies reviewed runtime projections. No other repository, package, version, modified binary, standalone SDK offering, optional execution provider/model or new AI feature is covered. The finite table is authoritative; there is no Microsoft package-family wildcard.
- **Reason:** The existing WinUI application and its native regression host require this established SDK dependency set. Evaluated NuGet dependency submission now exposes the actual versions and non-SPDX Microsoft license terms. Removing declarations or returning to incomplete `>=0` metadata would hide dependencies rather than resolve the policy issue. AppKit's MIT dependencies retain ordinary license review and are not exempted.
- **Risk:** Microsoft's usage, redistribution, data/privacy/export and third-party obligations remain applicable. A package-name license exception could hide later version/license changes because the pinned action ignores PURL version constraints. Approval does not relicense Microsoft code as MIT or certify that every redistribution obligation has been satisfied.
- **Compensating controls:** The caller may list only the 15 exact package names below as `pkg:nuget/<name>` in `allow-dependencies-licenses`, after this record is approved. The required `Test-WindowsSdkInventory.ps1` guard checks exact versions, committed NuGet content hashes, raw package SHA-256, license/notice entry hashes and the corresponding explicit manifest. Raw signed-archive SHA-512 and NuGet lock `contentHash` are distinct and are checked separately. Fourteen packages are checked in app/test-host locks; `Microsoft.Windows.SDK.NET.Ref` is checked through evaluated SDK assets/download dependencies, not a fabricated lock entry. Preserve the five distinct license documents and applicable third-party notices. Verify BuildTools/MSIX nonlegal files are excluded and only expected runtime/projection output is distributed. Maintain explicit third-party terms and distributor/end-user protection obligations; generic MIT attribution alone is not sufficient. `license-check: true`, `vulnerability-check: true`, low-or-higher advisory failure, SCA/malware checks, locked restores, signed/DCO commits, required PR gates and release attestations remain enabled. Any reviewed version/license/payload-scope change requires renewed approval before the exclusion can be used.
- **Expires:** 2027-04-30, or any reviewed package version/license/scope change, whichever occurs first. A classifier improvement alone does not make proprietary terms permissively licensed; remove or renew the record through the normal standards PR process.
- **Owner:** the maintainer of `QRun-IO/qqq-print-agent`.
- **Review date:** 2026-11-01.
### Exact package scope
| NuGet package | Version | Raw NUPKG SHA-256 |
| --- | --- | --- |
| [Microsoft.Windows.AI.MachineLearning](https://www.nuget.org/packages/Microsoft.Windows.AI.MachineLearning/2.1.74) | 2.1.74 | `691165FA3C07A04B752CBF4A07E93ED13A418E9DEA1EE89EB163D2225E2BA3AF` |
| [Microsoft.Windows.SDK.BuildTools](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools/10.0.28000.2705) | 10.0.28000.2705 | `8BFDFB6CA2633F531CF80B5FA22512BA61A394D7988F0970DB83BAADC67929ED` |
| [Microsoft.Windows.SDK.BuildTools.MSIX](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools.MSIX/1.7.251221100) | 1.7.251221100 | `E61C560805029F5AE15433BE52E153F3862B6FAAF725292638A9ECE592F7A060` |
| [Microsoft.Windows.SDK.NET.Ref](https://www.nuget.org/packages/Microsoft.Windows.SDK.NET.Ref/10.0.19041.57) | 10.0.19041.57 | `90B18F2CF7D7A23F5B0BFE2B86D8DDA45881481F677CE36ECCAF6E9AB50B6B4B` |
| [Microsoft.WindowsAppSDK](https://www.nuget.org/packages/Microsoft.WindowsAppSDK/2.5.1) | 2.5.1 | `2E26DE1253E2E2A288A75387523E2F8B402C05F117D42013DF9875887A70933A` |
| [Microsoft.WindowsAppSDK.AI](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.AI/2.5.5) | 2.5.5 | `108143AD35B20C8DF7A885995CE4258E7C2E6BA74C9B60D2D819CBC46980B6FB` |
| [Microsoft.WindowsAppSDK.Base](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Base/2.0.4) | 2.0.4 | `E3E13478C4C80C59ED5F8F89542FE49A2985DAA484753E93A5858E90C2D46A4D` |
| [Microsoft.WindowsAppSDK.DWrite](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.DWrite/2.1.0) | 2.1.0 | `91746ABD6A780B84001F9C465507830D5A24C79912BC4AA0D35938C71C1C6B5F` |
| [Microsoft.WindowsAppSDK.Foundation](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Foundation/2.3.12) | 2.3.12 | `CE04D01D68CB16B5DC85A5C9EFB8162DBA876BC2E5B766B5968979899D4EFA9E` |
| [Microsoft.WindowsAppSDK.InteractiveExperiences](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.InteractiveExperiences/2.1.9) | 2.1.9 | `F9159D73E3E46717F159606E43B4ACF8336AAFF5BDD4D395FE3A93FDB41437AC` |
| [Microsoft.WindowsAppSDK.ML](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.ML/2.1.94) | 2.1.94 | `549E92CD397B9D90EA9F08334DB6C79DFA78FC49B47BD3D26A53EB1DF2F21191` |
| [Microsoft.WindowsAppSDK.Runtime](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Runtime/2.5.1) | 2.5.1 | `2FBD27012291BE50CAFED6942306964281D37167AF6A6AF3887E1163EC5CF063` |
| [Microsoft.WindowsAppSDK.Search](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Search/2.5.5) | 2.5.5 | `B5876FE0091A6A14C34E46B5BC898621E80B5ED598F45574B90DEB84BF33F023` |
| [Microsoft.WindowsAppSDK.Widgets](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.Widgets/2.0.5) | 2.0.5 | `7F1B585EF8F012784D2CB91B86E809814CECCAB74183A5979EF45DB141779CB5` |
| [Microsoft.WindowsAppSDK.WinUI](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.WinUI/2.3.9) | 2.3.9 | `D36145BD6C0F46FC04E07FB4AF365D093FB42C39B625948D0EAD07A0B26FCE96` |
The 14 ordinary packages have identical versions and NuGet content hashes in the production app and test host. `Microsoft.Windows.SDK.NET.Ref` is an implicit SDK download dependency; its reviewed `lib/net8.0/Microsoft.Windows.SDK.NET.dll` and `lib/net8.0/WinRT.Runtime.dll` are legitimate runtime projections, not build tools. Packaging verification found these bytes unchanged in the app, CLI and service; the package's `Microsoft.Windows.UI.Xaml.dll` was absent. The package manifest and payload guard retain these distinctions rather than excluding every reference-pack file indiscriminately.
### License evidence and conditions
| Exact document | Packages | License byte SHA-256 |
| --- | --- | --- |
| [Windows App SDK license.txt](https://www.nuget.org/packages/Microsoft.WindowsAppSDK/2.5.1/License) | WindowsAppSDK, AI, Base, DWrite, Foundation, InteractiveExperiences, Runtime, Search, Widgets, WinUI | `5B11E6347756E40FE0274BC08C97F89201B94F0D50181A09A00F1F4740840501` |
| [Windows App SDK and Windows ML license.txt](https://www.nuget.org/packages/Microsoft.WindowsAppSDK.ML/2.1.94/License) | WindowsAppSDK.ML 2.1.94 | `656AAB74C15AA9F9964BCDCC993EB2755CBDB4822D5E0E3BC61D2E281897F758` |
| [Windows ML Runtime license.txt](https://www.nuget.org/packages/Microsoft.Windows.AI.MachineLearning/2.1.74/License) | Windows.AI.MachineLearning 2.1.74 | `66395F8CB219087FAE2BD025010BD9076B736C14F03B48F20295471C0C376814` |
| [Packaged SDK sdk_license.txt](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools.MSIX/1.7.251221100/License) | SDK.BuildTools.MSIX 1.7.251221100 | `A7A5C7E7FF998558983D6ACA2702117C328AEB0C6404D298CB275F5623C6FD13` |
| [Official SDK license RTF](https://download.microsoft.com/download/0/F/F/0FF2B061-47DD-4F55-89B6-FD1D8C44F14D/sdk_license.rtf) | SDK.BuildTools and SDK.NET.Ref, whose nuspecs declare [WinSDKLicenseURL](https://aka.ms/WinSDKLicenseURL) | `DD07EB178E00C6BBA4148457FC00FF77CD4887EB521D504186FE59C9EC8BBE62` |
The 13 file-declared licenses were read from the exact package archives and their byte hashes checked. The two URL declarations resolved on 2026-10-31 to the official RTF above, labeled Windows 10 SDK / `WIN10SDK.RTM.AUG_2018_en-US`; that is the document supplied by these packages, not a guessed newer license. No full vendor-license copies are placed in this standards repository.
App SDK section 1 permits Windows development/testing; section 3 permits distribution of files placed with the application by the SDK, including self-contained deployment. Its requirements include significant application functionality, protective distributor/end-user terms, the stated indemnity obligations and unmodified-code exception, notices and restrictions on endorsement/standalone offerings. Both ML documents additionally retain their section 1(d) third-party-material/execution-provider terms. SDK sections 1(a)/1(c) cover development and build-server use; section 2 separately restricts redistributable/setup/utility code. These obligations and applicable third-party notices remain in force. The maintainer must fulfill the applicable redistribution/end-user protection requirements; this record is not a legal opinion or a waiver of vendor terms.
The paired guard passed 25 rejection tests and checked the actual package cache, 11 locks and 11 evaluated assets. An unexecuted local MSI (`F08FF820CB4F7EA3877554E27208441942C6AA4D8749AD5A0D852C21A14D814C`) was built and statically extracted: all 13 legal/manifest paths matched expected bytes, the two runtime projections matched their package bytes, and BuildTools/MSIX nonlegal payloads and other reference-pack bytes were excluded. This is technical packaging evidence, not a certification that every redistribution obligation is fulfilled or authorization to install/publish that local MSI.
Additional evidence: [resolved dependency-review failure](https://github.com/QRun-IO/qqq-print-agent/actions/runs/41259093183), [application change under review](https://github.com/QRun-IO/qqq-print-agent/pull/10), [pinned reusable workflow](https://github.com/KofTwentyTwo/standards/blob/fdf5ed029603c938615a32b1beca3628b8787945/.github/workflows/pr.yml), [version-ignoring package matcher](https://github.com/actions/dependency-review-action/blob/a1d282b36b6f3519aa1f3fc636f609c47dddb294/src/purl.ts). Publication and approval links must be added only when they exist.
## EX-0015
**uri-js 4.4.1 license classification in the QQQ Print build tooling**
- **Status:** Open upon merge of this record through the normal protected PR process.
- **Requirements:** `K22-DEP-02`. Build-only tools may use any OSI-approved license. GitHub dependency review classifies the npm package `uri-js` 4.4.1 as `BSD-2-Clause AND BSD-2-Clause-Views`, and SPDX lists `BSD-2-Clause-Views` as not separately OSI-approved. This is a classification exception for one build-only tool dependency, not permission to ship it and not permission to admit any other non-allowlisted license. No vulnerability, malware, provenance, signing or branch-protection requirement is waived.
- **Scope:** `QRun-IO/qqq-print-protocol` and `QRun-IO/qqq-print-agent` only; npm package `uri-js`, unmodified version **4.4.1**, lock integrity `sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==`, installed `LICENSE` SHA-256 `0AF366EFF4C01EC147C9C61EA9E8FFAD64A4294754C9D79355F3FD1B97CB2FB9`. It is a locked, `dev: true`, transitive dependency of the pinned ESLint 10.14.0 (through ajv 6), never a direct dependency and never a runtime dependency. `node_modules` is restored only in development and CI; the protocol release bundle names repository paths only and the agent installer is built from .NET projects. No other package, version, repository or modified copy is covered.
- **Reason:** the pinned ESLint requires ajv 6, which requires `uri-js`; ESLint is required by the TypeScript standard for these repositories' owned tooling. The installed `LICENSE` (Copyright 2011 Gary Court) is the BSD-2-Clause conditions and disclaimer plus one sentence stating that the software's views and conclusions are the authors' and are not official policies; SPDX describes this as identical to BSD-2-Clause plus that sentence. It adds no restriction on use, copying, modification or redistribution. Replacing the resolver with a non-compatible library, or removing the package from the lock without an upstream ESLint change, would change reviewed tool behavior rather than resolve a policy problem.
- **Risk:** the scanner and SPDX classification are not on the permissive allowlist; the package is old (OpenSSF Scorecard 2.4, below the threshold of 3, already reported as a warning). A package-name exclusion could hide a later version or license change because the pinned action ignores PURL versions. Dependency review reports no vulnerability for 4.4.1.
- **Compensating controls:** each caller passes only `pkg:npm/uri-js` to `allow-dependencies-licenses`; every other package keeps its license check. `license-check: true`, `vulnerability-check: true`, low-or-higher advisory failure, development/runtime/unknown scopes, SCA/malware, Scorecard reporting and all required checks remain enabled. The required script-quality job in each repository fails unless the lock contains exactly one `uri-js`, version 4.4.1 with the integrity above, `dev: true`, not a direct dependency, required only by development packages; the installed `LICENSE` bytes hash to the value above; and the caller lists exactly `pkg:npm/uri-js` and no other npm package URL. The guard has rejection tests for another version, integrity, scope, consumer, duplicate copy, changed license bytes and a broadened caller list. Any version, license or scope change requires renewed approval before the exclusion can be used.
- **Expires:** 2027-04-30, or any `uri-js` version, license or scope change, or an ESLint release that no longer requires it, whichever occurs first. Remove or renew through the normal standards PR process; do not broaden the exclusion silently.
- **Owner:** the maintainer of `QRun-IO/qqq-print-protocol` and `QRun-IO/qqq-print-agent`.
- **Review date:** 2027-01-31.
Evidence: failed `pr / dependency-review` runs on [qqq-print-protocol#16](https://github.com/QRun-IO/qqq-print-protocol/pull/16) and [qqq-print-agent#18](https://github.com/QRun-IO/qqq-print-agent/pull/18) (only this license finding remained), [SPDX BSD-2-Clause-Views](https://spdx.org/licenses/BSD-2-Clause-Views.html), [pinned reusable workflow](https://github.com/KofTwentyTwo/standards/blob/fdf5ed029603c938615a32b1beca3628b8787945/.github/workflows/pr.yml), [version-ignoring package matcher](https://github.com/actions/dependency-review-action/blob/a1d282b36b6f3519aa1f3fc636f609c47dddb294/src/purl.ts).
+7 -11
View File
@@ -1,16 +1,12 @@
# How these standards work
> **Status: draft, pre-v1.0.0.** The requirements are written but have not been
> released. Until the first release (`v1.0.0`):
>
> - the automated checks and conformance tooling are still being built, so no
> requirement is yet *Enforced* or *Verified* (see [compliance](../compliance/README.md));
> - no KofTwentyTwo repository claims conformance; adoption is in progress, starting
> with this repository, [AppKit](https://github.com/KofTwentyTwo/AppKit), and
> [gclo](https://github.com/KofTwentyTwo/gclo);
> - commitments to people outside the project (vulnerability response and fix times in
> the [security program](security-program.md#vulnerability-management)) are targets
> that the maintainer works to, and become binding commitments at `v1.0.0`.
> **Status: versioned baseline.** Published tags identify immutable requirements.
> Each repository adopts a specific version and records its own conformance evidence
> and approved exceptions. [Compliance](../compliance/README.md) labels describe the
> evidence for each mapped control; publication alone does not verify every consumer.
> External vulnerability response and fix deadlines in the
> [security program](security-program.md#vulnerability-management) become binding
> commitments with adoption of the stable `v1.0.0` baseline.
This repository is the single source of truth for how KofTwentyTwo builds, secures,
and ships software. It has three layers, each more specific than the last:
+4 -11
View File
@@ -2,18 +2,11 @@
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "simple",
"include-component-in-tag": false,
"bump-minor-pre-major": false,
"changelog-type": "github",
"pull-request-header": "Release proposal generated from reviewed changes on main. Review the version, changes, and validation before merging.",
"pull-request-footer": "Generated by [Release Please](https://github.com/googleapis/release-please) using this repository's release configuration.",
"initial-version": "0.1.0",
"changelog-sections": [
{ "type": "security", "section": "Security", "hidden": false },
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "perf", "section": "Performance", "hidden": false },
{ "type": "refactor", "section": "Refactoring", "hidden": false },
{ "type": "build", "section": "Build", "hidden": false },
{ "type": "ci", "section": "Automation", "hidden": false },
{ "type": "revert", "section": "Reverts", "hidden": false }
],
"packages": {
".": { "version-file": "version.txt" }
}
+4 -1
View File
@@ -117,7 +117,10 @@ the pull request description discloses it, per
**K22-AGENT-41 (SHOULD)** An agent reports in concise plain language: what changed,
why, what was verified, and what limitations remain. Document length scales with the
task; decoration (emojis, filler praise) is omitted unless asked for.
task. Repository text, including code comments, documentation, commits, PRs, release
notes and generated automation messages, uses professional wording. Omit emojis,
joke copy, filler praise and bot sound effects. Configure third-party automation to
follow the same rule; its default templates are not an exception.
*Why:* the report is the maintainer's primary review input. *Verified by:* review.
## Context economy
+14
View File
@@ -88,6 +88,20 @@ released software, links to the latest release and the verification instructions
[releases](releases.md#verifying-a-release).
*Why:* users can see the security posture before they install. *Verified by:* review.
**K22-REPO-12 (MUST)** Public, open-source Product repositories enroll in the
[OpenSSF Best Practices program](https://www.bestpractices.dev/en), link their actual
project badge from the README, and maintain an evidence-backed assessment in
`docs/security/best-practices.md`. Reassess at least every 90 days and before each
MINOR or MAJOR release. Record applicability, evidence path, and the actual project
URL in `docs/security/assurance.json` using the [starter record](../templates/repo/docs/security/assurance.json).
Private repositories record their scope and reason instead; Internal repositories
follow the tier rules above. Registration obstacles require a dated exception, not
a placeholder badge or a claim of passing. A passing badge is the adoption target;
enrollment alone does not demonstrate that every criterion is met.
*Why:* a public assessment makes security practice gaps visible and accountable.
*Verified by:* conformance checker (record, evidence and badge link); maintainer
review of the provider's project page, repository identity, and criterion answers.
## Branch and tag protection
Protection is configured with **repository rulesets**, never with legacy branch
+26
View File
@@ -80,3 +80,29 @@ release checklist.
applications expose a data-directory override (for example an environment variable)
that tests use.
*Why:* running tests must be safe on any machine. *Verified by:* review.
## Parser fuzzing
**K22-TEST-25 (MUST)** Repositories with complex parsers of untrusted input (including
workflow, configuration, protocol, archive, and document parsers) maintain fuzz
targets for those boundaries. Use coverage-guided fuzzing where supported; otherwise
use generated or mutated inputs with independent security properties. A crash-only
smoke test is insufficient when acceptance of unsafe input is the relevant failure.
Run bounded campaigns in a merge-gating CI job on every PR and push to `main`, and
longer campaigns at least weekly and before each MINOR or MAJOR release. Document
targets, input limits, invariants, a local replay command, seeds or corpus, CI job,
and failure handling in `docs/security/fuzzing.md`; declare the evidence, targets,
command and workflow in `docs/security/assurance.json`. Preserve minimized failures
as regression tests and fix or triage them before merging. Never execute generated
attacker scripts, use real secrets, or write to a user's profile.
Repositories without a complex untrusted-input parser document that decision and
its rationale in the same files; review applicability when input boundaries change
and at least every 90 days. Documentation-only status does not exempt shipped tooling.
*Why:* malformed inputs can cause unsafe acceptance as well as crashes; repeatable
fuzzing exercises combinations ordinary examples miss. *Verified by:* conformance
checker (scope record, target paths, command and CI reference); required CI execution
and review of properties, corpus and applicability. See the
[native PowerShell example](../docs/security/fuzzing.md). Scorecard recognition is
useful evidence, but is not a substitute for running appropriate targets.
+3
View File
@@ -1,5 +1,8 @@
# Contributing to {{PROJECT_NAME}}
Use professional wording in code comments, documentation, commit/PR messages,
release notes and automation output. No emojis, joke copy or bot sound effects.
Thanks for helping. This project follows the
[KofTwentyTwo standards](https://github.com/KofTwentyTwo/standards); this page is the
short version of what they ask of a contribution.
+5
View File
@@ -51,6 +51,11 @@ sha256sum --check SHA256SUMS
## Security
Report vulnerabilities privately; see [SECURITY.md](SECURITY.md).
Maintain the [Best Practices assessment](docs/security/best-practices.md) and
[parser fuzzing assessment](docs/security/fuzzing.md), with their actual scope and
configuration in [assurance.json](docs/security/assurance.json). After enrollment,
insert the provider's actual linked badge here; placeholders do not demonstrate
participation.
## Contributing
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"badge": {
"applicability": "required",
"reason": "Replace with this repository's public Product scope, or a reviewed private/Internal rationale.",
"projectUrl": null,
"evidence": "docs/security/best-practices.md"
},
"fuzzing": {
"applicability": "required",
"reason": "Replace with actual untrusted-input boundaries, or a reviewed not-applicable rationale.",
"targets": [],
"command": "",
"workflow": "",
"evidence": "docs/security/fuzzing.md"
}
}
@@ -0,0 +1,23 @@
# OpenSSF Best Practices assessment
## Project and ownership
For a public open-source Product repository, register its exact canonical URL at
<https://www.bestpractices.dev/en>. Replace `badge.projectUrl` in `assurance.json`
with the actual `https://www.bestpractices.dev/projects/<numeric-id>` URL and add the
provider's real linked badge to the README. Record the owner, review date and next
review within 90 days. Private/Internal scope decisions need a documented rationale.
## Criterion evidence
Use the current [passing criteria](https://www.bestpractices.dev/en/criteria/0).
For each answer, link actual source, policy, test or observed operational evidence.
Separate implemented practice from intentions, pending human attestations, and
justified not-applicable answers. Passing is the target, not an automatic claim
made by a copied template. Record remaining gaps with owners and follow-up dates.
## Maintenance
Reassess at least every 90 days and before each MINOR or MAJOR release. Keep the
provider's answers consistent with this evidence. A registration restriction needs
a dated approved exception; never use another repository's badge or invent an ID.
+29
View File
@@ -0,0 +1,29 @@
# Parser fuzzing assessment
## Applicability
Record the reviewed complex untrusted-input boundaries and their owner. If none
exist, explain why shipped tooling, importers, configuration and protocol handling
do not introduce one. Set `fuzzing.applicability` in `assurance.json` to `required`
or `not-applicable` with the actual rationale. Unfilled templates do not conform.
## Targets and properties
List each target, synthetic input limits, security invariants and corpus or seeds.
Use the language's native coverage-guided fuzzer where practical. Generated-input
property tests are an alternative when instrumentation is unavailable; assertions
must detect unsafe acceptance as well as crashes.
## Run and replay
Provide exact local commands, prerequisites, bounded PR/main campaign budgets,
weekly and pre-MINOR/MAJOR budgets, and the required CI job. Reference the target
paths, command and workflow in `assurance.json`; ensure the command actually runs
there rather than merely appearing in a comment. Explain how to replay and minimize
failures into regression tests. Never execute generated attacker scripts.
## Review
Name the owner, review date and next review (within 90 days). Reassess input
boundaries when they change. Do not claim a Scorecard-supported integration unless
it is genuinely configured and running; a score alone is not a test result.
+4
View File
@@ -35,6 +35,10 @@ jobs:
coverage-packages: "{{COVERAGE_PACKAGES}}"
test-projects: |
{{TEST_PROJECT}}
# For Windows libraries, enable only after verified Azure OIDC configuration:
# sign: true
# signing-files: |
# src/My.Library/bin/Release/net10.0/My.Library.dll
publish:
name: publish (nuget.org)
+13 -6
View File
@@ -8,11 +8,12 @@
# One-time owner setup:
# - Environment `release` (Settings -> Environments) with a deployment rule that allows
# only tags matching v*.
# - Code signing (when exception EX-0002 closes): add the environment variables
# AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, TRUSTED_SIGNING_ENDPOINT,
# TRUSTED_SIGNING_ACCOUNT, TRUSTED_SIGNING_PROFILE (a federated credential for this
# repository's `release` environment on the Azure app registration), then set
# `sign: true` below.
# - Code signing: verify the existing Artifact Signing profile, configure repository
# OIDC and its Certificate Profile Signer role, then add AZURE_CLIENT_ID,
# AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, ARTIFACT_SIGNING_ENDPOINT,
# ARTIFACT_SIGNING_ACCOUNT, ARTIFACT_SIGNING_PROFILE and ARTIFACT_SIGNING_SUBJECT
# to the protected release environment. Enable sign and enumerate owned files.
# See architecture/windows-release-signing.md; an Active profile alone is not proof.
# - winget (optional, stable releases only): the package must already exist in
# microsoft/winget-pkgs. Submit it once by hand with
# wingetcreate new <Setup.exe URL of the first stable release>
@@ -51,7 +52,13 @@ jobs:
test-projects: |
{{TEST_PROJECT}}
cli-project: "{{CLI_PROJECT}}" # remove for apps without a CLI
sign: false # exception EX-0002: true once the Azure Trusted Signing identity is validated
sign: false # enable only after CI authentication and real signed-artifact verification
# Required when sign is true; include all owned app/CLI binaries, not dependencies:
# signing-files: |
# app/MyApp.exe
# app/MyApp.dll
# app/MyApp.Core.dll
# cli/myapp.exe
# winget lives here, not in the shared workflow, because its token belongs to this
# repository's `release` environment. It runs only for stable releases, and only when
+3 -1
View File
@@ -44,7 +44,9 @@ if($committedVersion -ne $Version -or $releaseManifest['.'] -ne $Version)
$changelog = (git -C $repository show "${commit}:CHANGELOG.md") -join "`n"
$escapedVersion = [regex]::Escape($Version)
$section = [regex]::Match($changelog, "(?ms)^## (?:\[$escapedVersion\](?:\([^\r\n]*\))?|$escapedVersion)(?=\s|$)[^\r\n]*\r?\n.*?(?=^## |\z)")
# Native release-note categories also use H2; only a version heading ends a release.
$releaseHeading = '^## (?:\[\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\](?:\([^\r\n]*\))?|\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?=\s|$)'
$section = [regex]::Match($changelog, "(?ms)^## (?:\[$escapedVersion\](?:\([^\r\n]*\))?|$escapedVersion)(?=\s|$)[^\r\n]*\r?\n.*?(?=$releaseHeading|\z)")
if(-not $section.Success)
{
throw "CHANGELOG.md has no release notes for $Version at $commit."
+135
View File
@@ -0,0 +1,135 @@
#Requires -Version 7.4
<#
.SYNOPSIS
Runs bounded, reproducible property tests against the conformance parsers.
.DESCRIPTION
Constructs workflows with independently known security properties, varying quotes,
indentation, comments, line endings, block scalars, and attacker expressions. Inputs
are data only: no generated script is executed and no GitHub access is needed.
A failure prints the seed, iteration, property, and synthetic input for replay.
.PARAMETER Seed
Seed for a deterministic sequence of generated cases.
.PARAMETER Iterations
Number of generated cases; bounded for pull-request CI.
#>
[CmdletBinding()]
param(
[int] $Seed = 22023,
[ValidateRange(1, 100000)]
[int] $Iterations = 1000
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'Test-RepoConformance.ps1') -LoadHelpers
$random = [System.Random]::new($Seed)
$assertions = 0
$caseNumber = 0
<#
.SYNOPSIS
Fail with replay data when an independently generated security property is violated.
#>
function Assert-Property
{
param([bool] $Condition, [string] $Property, [string] $InputText)
if(-not $Condition)
{
$replay = [ordered]@{ seed = $Seed; iteration = $caseNumber; property = $Property; input = $InputText }
throw ($replay | ConvertTo-Json -Compress)
}
$script:assertions++
}
<#
.SYNOPSIS
Generate synthetic hexadecimal action refs using the injected deterministic PRNG.
#>
function Get-HexString
{
param([int] $Length)
$characters = for($position = 0; $position -lt $Length; $position++)
{
'0123456789abcdef'[$random.Next(16)]
}
return -join $characters
}
for($caseNumber = 1; $caseNumber -le $Iterations; $caseNumber++)
{
$newline = @("`n", "`r`n")[$random.Next(2)]
$indent = ' ' * $random.Next(2, 5)
$quote = @('', "'", '"')[$random.Next(3)]
$sha = Get-HexString 40
$uses = 'actions/checkout@' + $sha
$pinnedWorkflow = @(
'name: generated'
'on: pull_request'
'permissions: {}'
'jobs:'
($indent + 'check:')
($indent * 2 + 'runs-on: ubuntu-24.04')
($indent * 2 + 'steps:')
($indent * 3 + '- uses: ' + $quote + $uses + $quote + ' # pinned')
($indent * 3 + ' with:')
($indent * 3 + ' persist-credentials: false')
) -join $newline
$parsedUses = @(Get-WorkflowUses $pinnedWorkflow)
Assert-Property -Condition ($parsedUses.Count -eq 1) -Property 'one generated action is found' -InputText $pinnedWorkflow
Assert-Property -Condition ((Get-UsesPinState $parsedUses[0].Uses) -eq 'PINNED') -Property 'quoted full SHA remains pinned' -InputText $pinnedWorkflow
Assert-Property -Condition (@(Find-PersistedCheckout $pinnedWorkflow).Count -eq 0) -Property 'explicit credential disabling is found' -InputText $pinnedWorkflow
$unsafeCheckout = $pinnedWorkflow.Replace('persist-credentials: false', 'persist-credentials: true')
Assert-Property -Condition (@(Find-PersistedCheckout $unsafeCheckout).Count -eq 1) -Property 'quoted unsafe checkout is never missed' -InputText $unsafeCheckout
$badRef = @('v7', 'main', (Get-HexString 39), (Get-HexString 41))[$random.Next(4)]
Assert-Property -Condition ((Get-UsesPinState ('actions/checkout@' + $badRef)) -ne 'PINNED') -Property 'non-SHA refs never pass pinning' -InputText $badRef
$expression = @('github.event.pull_request.title', 'github.head_ref', 'inputs.payload')[$random.Next(3)]
$indicator = @('|', '|-', '|+', '>', '>-', '>+', '|2', '|2-', '>2+')[$random.Next(9)]
$expressionText = '${{ ' + $expression + ' }}'
$injectionWorkflow = @(
'permissions: {}'
'jobs:'
($indent + 'check:')
($indent * 2 + 'steps:')
($indent * 3 + '- run: ' + $indicator + ' # generated scalar')
($indent * 3 + ' echo "' + $expressionText + '"')
) -join $newline
$findings = @(Find-TemplateInjection $injectionWorkflow -IncludeInputs)
Assert-Property -Condition ($findings.Count -eq 1) -Property 'untrusted expression in a block scalar is found' -InputText $injectionWorkflow
Assert-Property -Condition ($findings[0].Expression -eq $expression) -Property 'finding preserves the controlled expression' -InputText $injectionWorkflow
$safeWorkflow = $injectionWorkflow.Replace($expressionText, '${{ github.sha }}')
Assert-Property -Condition (@(Find-TemplateInjection $safeWorkflow -IncludeInputs).Count -eq 0) -Property 'trusted expression is not reported' -InputText $safeWorkflow
$envWorkflow = @(
'jobs:'
($indent + 'check:')
($indent * 2 + 'steps:')
($indent * 3 + '- env:')
($indent * 3 + ' TITLE: ' + $expressionText)
($indent * 3 + ' run: echo "$TITLE"')
) -join $newline
Assert-Property -Condition (@(Find-TemplateInjection $envWorkflow -IncludeInputs).Count -eq 0) -Property 'expressions in env stay outside run bodies' -InputText $envWorkflow
$shortName = 'generated-' + $random.Next(100000).ToString([Globalization.CultureInfo]::InvariantCulture)
$requirement = 'K22-CI-' + $random.Next(10, 25).ToString('00', [Globalization.CultureInfo]::InvariantCulture)
$exception = '| [EX-0099](#ex-0099) | Open | ' + $requirement + ' | KofTwentyTwo/' + $shortName + ' | 2099-01-01 |'
$register = @(Read-ExceptionRegister $exception)
Assert-Property -Condition ($register.Count -eq 1) -Property 'generated exception row is parsed' -InputText $exception
Assert-Property -Condition ($null -ne (Find-Exception $register $requirement ('KofTwentyTwo/' + $shortName))) -Property 'exact exception scope matches' -InputText $exception
Assert-Property -Condition ($null -eq (Find-Exception $register $requirement ('KofTwentyTwo/' + $shortName + '-other'))) -Property 'partial repository names cannot gain an exception' -InputText $exception
Assert-Property -Condition ($null -eq (Find-Exception $register $requirement ('AnotherOwner/' + $shortName))) -Property 'explicit scope cannot authorize another owner' -InputText $exception
$closed = @(Read-ExceptionRegister $exception.Replace('| Open |', '| Closed |'))
Assert-Property -Condition ($null -eq (Find-Exception $closed $requirement ('KofTwentyTwo/' + $shortName))) -Property 'closed exceptions cannot authorize a failure' -InputText $exception
$expired = @(Read-ExceptionRegister $exception.Replace('2099-01-01', '2026-11-10'))
Assert-Property -Condition ($null -eq (Find-Exception $expired $requirement ('KofTwentyTwo/' + $shortName) -Today ([datetime] '2026-11-10'))) -Property 'expired exceptions cannot authorize a failure' -InputText $exception
$noise = -join (1..$random.Next(1, 128) | ForEach-Object { [char] $random.Next(32, 127) })
foreach($parser in 'Get-WorkflowUses', 'Get-TopLevelPermissions', 'Get-RunBlocks', 'Find-TemplateInjection', 'Find-PersistedCheckout', 'Read-ExceptionRegister')
{
& $parser $noise | Out-Null
}
Get-AssuranceCheck $noise @() @{} '' | Out-Null
$assertions++
}
Write-Output "PASS: conformance fuzz seed=$Seed iterations=$Iterations assertions=$assertions"
+133 -8
View File
@@ -4,8 +4,9 @@
Checks a repository against the KofTwentyTwo repository and CI/CD standards.
.DESCRIPTION
Evaluates every automatable requirement in standards/repository.md (K22-REPO-*)
and standards/ci-cd.md (K22-CI-*) and prints one row per requirement:
Evaluates automatable requirements in standards/repository.md (K22-REPO-*),
standards/ci-cd.md (K22-CI-*), and the K22-TEST-25 fuzzing scope record, printing
one row per requirement:
PASS the requirement is met
FAIL a MUST is not met (or a SHOULD, reported as WARN instead)
@@ -45,6 +46,9 @@
.PARAMETER SelfTest
Run the checker's own unit tests and exit.
.PARAMETER LoadHelpers
Load pure parser helpers for the fuzz harness without checking a repository.
.EXAMPLE
./tools/Test-RepoConformance.ps1 -Repository KofTwentyTwo/AppKit -LocalPath ../AppKit
@@ -58,7 +62,8 @@ param(
[switch] $StaticOnly,
[string] $StandardsPath = (Split-Path -Parent $PSScriptRoot),
[switch] $Json,
[switch] $SelfTest
[switch] $SelfTest,
[switch] $LoadHelpers
)
Set-StrictMode -Version Latest
@@ -72,7 +77,8 @@ $script:Levels = [ordered]@{
'K22-REPO-01' = 'MUST'; 'K22-REPO-02' = 'MUST'; 'K22-REPO-03' = 'MUST'
'K22-REPO-04' = 'MUST'; 'K22-REPO-05' = 'MUST'; 'K22-REPO-06' = 'MUST'
'K22-REPO-07' = 'MUST'; 'K22-REPO-08' = 'MUST'; 'K22-REPO-09' = 'MUST'
'K22-REPO-10' = 'SHOULD'; 'K22-REPO-11' = 'SHOULD'; 'K22-REPO-20' = 'MUST'
'K22-REPO-10' = 'SHOULD'; 'K22-REPO-11' = 'SHOULD'; 'K22-REPO-12' = 'MUST'
'K22-TEST-25' = 'MUST'; 'K22-REPO-20' = 'MUST'
'K22-REPO-21' = 'MUST'; 'K22-REPO-22' = 'MUST'; 'K22-REPO-30' = 'MUST'
'K22-REPO-31' = 'MUST'; 'K22-REPO-32' = 'SHOULD'; 'K22-REPO-40' = 'MUST'
'K22-REPO-41' = 'SHOULD'; 'K22-REPO-42' = 'MUST'
@@ -169,7 +175,7 @@ function Get-RunBlocks
{
$indent = $Matches[1].Length
$rest = $Matches[2]
if ($rest -and $rest -notmatch '^[|>][+-]?\s*$')
if ($rest -and $rest -notmatch '^[|>](?:[1-9][+-]?|[+-][1-9]?|[+-]?)\s*(?:#.*)?$')
{
[pscustomobject]@{ Line = $i + 1; Body = $rest }
continue
@@ -226,7 +232,7 @@ function Find-PersistedCheckout
$lines = $Text -split "`r?`n"
for ($i = 0; $i -lt $lines.Count; $i++)
{
if ($lines[$i] -match '^(\s*)(-\s+)?uses:\s*actions/checkout@')
if ($lines[$i] -match '^(\s*)(-\s+)?uses:\s*["'']?actions/checkout@')
{
$stepIndent = $Matches[1].Length
$ok = $false
@@ -263,6 +269,7 @@ function Read-ExceptionRegister
Requirements = @($qualifiers.Keys)
Qualifiers = $qualifiers
Scope = $cells[3]
Expires = if ($cells.Count -ge 5) { $cells[4] } else { '' }
}
}
}
@@ -270,10 +277,18 @@ function Read-ExceptionRegister
# Returns the open exception covering a requirement for a repository, or $null.
function Find-Exception
{
param([object[]] $Register, [string] $RequirementId, [string] $RepositoryName, [string[]] $Aspects = @())
param([object[]] $Register, [string] $RequirementId, [string] $RepositoryName, [string[]] $Aspects = @(), [datetime] $Today = [datetime]::UtcNow.Date)
foreach ($ex in $Register)
{
if ($ex.Status -notmatch '^Open$') { continue }
# Review dates are not expiry dates. Enforce an explicit ISO expiry or an
# "at the latest" deadline; condition-only expiry still needs human review.
if ($ex.Expires -match '(?i)(?:^|at (?:the )?latest\s+)(\d{4}-\d{2}-\d{2})')
{
$deadline = [datetime]::MinValue
if (-not [datetime]::TryParseExact($Matches[1], 'yyyy-MM-dd', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref] $deadline)) { continue }
if ($Today.Date -ge $deadline.Date) { continue }
}
if ($ex.Requirements -notcontains $RequirementId) { continue }
# A qualified exception covers a failure only when every failing aspect is
# the qualified one.
@@ -285,7 +300,8 @@ function Find-Exception
}
$short = if ($RepositoryName) { ($RepositoryName -split '/')[-1] } else { '' }
if ($ex.Scope -match '(?i)^all repositories' -or
($short -and $ex.Scope -match "(?i)(^|[\s/,(])$([regex]::Escape($short))($|[\s,)])"))
($RepositoryName -and $ex.Scope -match "(?i)(^|[\s,(])$([regex]::Escape($RepositoryName))($|[\s,)])") -or
($short -and $ex.Scope -match "(?i)(^|[\s,(])$([regex]::Escape($short))($|[\s,)])"))
{
return $ex
}
@@ -293,6 +309,78 @@ function Find-Exception
return $null
}
<#
.SYNOPSIS
Validate declared assurance evidence without executing commands or claiming certification.
#>
function Get-AssuranceCheck
{
param([string] $Text, [string[]] $Paths, [hashtable] $Workflows, [string] $Readme)
try
{
$record = ConvertFrom-Json -InputObject $Text -AsHashtable -ErrorAction Stop
if ($record -isnot [System.Collections.IDictionary] -or $record['schemaVersion'] -isnot [long] -or $record['schemaVersion'] -ne 1) { throw 'unsupported schema' }
}
catch
{
foreach ($id in 'K22-REPO-12', 'K22-TEST-25')
{
[pscustomobject]@{ Id = $id; Status = 'FAIL'; Detail = 'missing or invalid docs/security/assurance.json (schemaVersion 1)'; Aspects = @('record') }
}
return
}
foreach ($pair in @(@('badge', 'K22-REPO-12'), @('fuzzing', 'K22-TEST-25')))
{
$kind, $id = $pair
$entry = $record[$kind]
$status, $detail, $aspect = 'FAIL', 'invalid assurance scope, rationale or evidence path', 'record'
if ($entry -is [System.Collections.IDictionary] -and
$entry['applicability'] -in @('required', 'not-applicable') -and
$entry['reason'] -is [string] -and -not [string]::IsNullOrWhiteSpace($entry['reason']) -and
$entry['evidence'] -is [string] -and $entry['evidence'] -cmatch '^docs/security/[A-Za-z0-9._/-]+\.md$' -and
$entry['evidence'] -notmatch '(^|/)\.\.?(/|$)' -and $Paths -ccontains $entry['evidence'])
{
if ($entry['applicability'] -eq 'not-applicable')
{
$status, $detail = 'REVIEW', "$kind scope declared not applicable; verify rationale and input/public-Product scope in $($entry.evidence)"
}
elseif ($kind -eq 'badge')
{
$aspect = 'enrollment'
$detail = 'badge enrollment missing; register this repository or obtain a dated enrollment exception'
if ($entry['projectUrl'] -is [string] -and $entry['projectUrl'] -cmatch '^https://www\.bestpractices\.dev/(?:en/)?projects/([1-9][0-9]*)/?$')
{
$projectId = $Matches[1]
$badgePattern = '\[!\[[^\]]*\]\(https://www\.bestpractices\.dev/projects/' + $projectId + '/badge\)\]\(https://www\.bestpractices\.dev/(?:en/)?projects/' + $projectId + '/?\)'
if ($Readme -cmatch $badgePattern)
{
$status, $detail = 'REVIEW', "badge linked; verify $($entry.projectUrl) belongs to this repository, current answers and 90-day review in $($entry.evidence)"
}
else { $detail = 'README lacks the actual linked badge matching the declared project ID' }
}
}
else
{
$aspect = 'fuzzing'
$targets = @($entry['targets'])
$validTargets = $targets.Count -gt 0 -and @($targets | Where-Object {
$_ -isnot [string] -or $_ -match '(^[/\\]|^[A-Za-z]:|(^|[/\\])\.\.?([/\\]|$))' -or $Paths -cnotcontains $_
}).Count -eq 0
$validWorkflow = $entry['workflow'] -is [string] -and $entry['workflow'] -cmatch '^\.github/workflows/[^/]+\.ya?ml$' -and $Workflows.ContainsKey($entry['workflow'])
$validCommand = $entry['command'] -is [string] -and -not [string]::IsNullOrWhiteSpace($entry['command']) -and $entry['command'] -notmatch '[\r\n]'
$runs = if ($validWorkflow) { @(Get-RunBlocks $Workflows[$entry['workflow']] | ForEach-Object Body) -join "`n" } else { '' }
$commandInRun = $validCommand -and $runs -cmatch ('(?m)^\s*' + [regex]::Escape($entry['command'].Trim()) + '(?:\s|$)')
if ($validTargets -and $validWorkflow -and $commandInRun)
{
$status, $detail = 'REVIEW', "fuzz targets and run command found; verify required PR/main gate, weekly/release budgets, properties and replay in $($entry.evidence)"
}
else { $detail = 'declared fuzz targets must exist and the command must begin a run line in the declared workflow' }
}
}
[pscustomobject]@{ Id = $id; Status = $status; Detail = $detail; Aspects = @($aspect) }
}
}
# Lock files expected for each manifest found (K22-REPO-09).
$script:LockRules = @(
@{ Manifest = '(^|/)[^/]+\.(cs|fs|vb)proj$|(^|/)Directory\.Packages\.props$'; Locks = 'packages\.lock\.json$'; Name = 'NuGet (packages.lock.json)' }
@@ -395,6 +483,8 @@ jobs:
Assert ($inj[1].Expression -eq 'github.head_ref') 'injection: head_ref'
$persisted = @(Find-PersistedCheckout $wf)
Assert ($persisted.Count -eq 1 -and $persisted[0] -eq 9) "checkout: persisted ($($persisted -join ','))"
Assert (@(Find-PersistedCheckout "jobs:`n check:`n steps:`n - uses: 'actions/checkout@v7'`n with:`n persist-credentials: true").Count -eq 1) 'checkout: quoted unsafe action'
Assert (@(Find-TemplateInjection "jobs:`n check:`n steps:`n - run: |2 # explicit indentation`n echo `${{ github.head_ref }}").Count -eq 1) 'injection: block scalar indentation and comment'
$register = @"
| ID | Status | Requirements | Scope | Expires |
@@ -416,6 +506,24 @@ jobs:
Assert ($null -eq (Find-Exception $parsed 'K22-REPO-05' 'KofTwentyTwo/AppKit')) 'exception: closed'
Assert ((Find-Exception $parsed 'K22-CODE-CS-03' 'KofTwentyTwo/AppKit').Id -eq 'EX-0004') 'exception: short name scope'
Assert ($null -eq (Find-Exception $parsed 'K22-CODE-CS-03' 'KofTwentyTwo/AppKitExtras')) 'exception: no partial name match'
$expired = @(Read-ExceptionRegister '| [EX-0099](#ex-0099) | Open | K22-REPO-12 | standards | 2026-11-10; review 2026-10-17 |')
Assert ($null -ne (Find-Exception $expired 'K22-REPO-12' 'standards' -Today ([datetime] '2026-11-09'))) 'exception: valid before deadline'
Assert ($null -eq (Find-Exception $expired 'K22-REPO-12' 'standards' -Today ([datetime] '2026-11-10'))) 'exception: expiry cannot silently authorize failure'
$exactScope = @(Read-ExceptionRegister '| [EX-0099](#ex-0099) | Open | K22-REPO-12 | KofTwentyTwo/standards | 2099-01-01 |')
Assert ($null -eq (Find-Exception $exactScope 'K22-REPO-12' 'AnotherOwner/standards')) 'exception: explicit owner scope cannot match another owner'
Assert (@(Get-AssuranceCheck '{}' @() @{} '').Count -eq 2) 'assurance: missing schema returns both failures'
Assert (@(Get-AssuranceCheck '{"schemaVersion":true}' @() @{} '' | Where-Object Status -eq 'FAIL').Count -eq 2) 'assurance: boolean cannot masquerade as schema version 1'
Assert (@(Get-AssuranceCheck '{"schemaVersion":1,"badge":{},"fuzzing":{}}' @() @{} '' | Where-Object Status -eq 'FAIL').Count -eq 2) 'assurance: incomplete entries fail without crashing'
$assurance = '{"schemaVersion":1,"badge":{"applicability":"required","reason":"public Product","projectUrl":"https://www.bestpractices.dev/projects/123","evidence":"docs/security/best-practices.md"},"fuzzing":{"applicability":"required","reason":"parser","targets":["tools/parser.ps1"],"command":"./tools/fuzz.ps1","workflow":".github/workflows/ci.yml","evidence":"docs/security/fuzzing.md"}}'
$assurancePaths = @('tools/parser.ps1', 'docs/security/best-practices.md', 'docs/security/fuzzing.md')
$assuranceWorkflow = @{ '.github/workflows/ci.yml' = "jobs:`n tests:`n steps:`n - run: ./tools/fuzz.ps1 -Iterations 10" }
$assuranceReadme = '[![Best Practices](https://www.bestpractices.dev/projects/123/badge)](https://www.bestpractices.dev/projects/123)'
$assuranceRows = @(Get-AssuranceCheck $assurance $assurancePaths $assuranceWorkflow $assuranceReadme)
Assert (@($assuranceRows | Where-Object Status -ne 'REVIEW').Count -eq 0) 'assurance: evidence needs human verification, never fabricated certification'
Assert (@(Get-AssuranceCheck $assurance $assurancePaths $assuranceWorkflow '' | Where-Object { $_.Id -eq 'K22-REPO-12' -and $_.Status -eq 'FAIL' }).Count -eq 1) 'assurance: missing badge fails'
$assuranceWorkflow['.github/workflows/ci.yml'] = "jobs:`n tests:`n steps:`n - run: |`n # ./tools/fuzz.ps1"
Assert (@(Get-AssuranceCheck $assurance $assurancePaths $assuranceWorkflow $assuranceReadme | Where-Object { $_.Id -eq 'K22-TEST-25' -and $_.Status -eq 'FAIL' }).Count -eq 1) 'assurance: comment does not execute a fuzz command'
Assert (@(Get-AssuranceCheck ($assurance.Replace('tools/parser.ps1', '../parser.ps1')) $assurancePaths $assuranceWorkflow $assuranceReadme | Where-Object { $_.Id -eq 'K22-TEST-25' -and $_.Status -eq 'FAIL' }).Count -eq 1) 'assurance: traversal paths cannot satisfy targets'
Assert (@(Get-MissingLocks @('src/a/a.csproj', 'README.md')) -contains 'NuGet (packages.lock.json)') 'locks: missing nuget'
Assert (@(Get-MissingLocks @('src/a/a.csproj', 'src/a/packages.lock.json')).Count -eq 0) 'locks: nuget ok'
@@ -450,6 +558,7 @@ jobs:
exit 0
}
if ($LoadHelpers) { return }
if ($SelfTest) { Invoke-SelfTest }
# =================================================================================
@@ -534,6 +643,13 @@ function Find-FirstPath
}
$repoName = if ($Repository) { $Repository } else { Split-Path -Leaf $script:LocalRoot }
if (-not $Repository)
{
# Local static checks can identify exact exception scope without contacting
# GitHub. A clone with no canonical GitHub origin keeps its directory name.
$origin = & git -C $script:LocalRoot remote get-url origin 2>$null
if ($LASTEXITCODE -eq 0 -and $origin -match '^(?:https://github\.com/|git@github\.com:|ssh://git@(?:github\.com(?::22)?|ssh\.github\.com:443)/)([^/]+/[^/]+?)(?:\.git)?$') { $repoName = $Matches[1] }
}
# =================================================================================
# Static checks: required files (K22-REPO-01 .. 11)
@@ -662,6 +778,15 @@ $workflows = @{}
foreach ($p in $workflowPaths) { $workflows[$p] = Get-FileText $p }
$allWorkflowText = ($workflows.Values -join "`n")
# Assurance records fail closed when required evidence is absent. A positive static
# check is REVIEW: identity, applicability and actual CI execution need verification.
$assuranceReadme = if ($readmePath) { Get-FileText $readmePath } else { '' }
if ($assuranceReadme -match '(?im)\bTier(?:\*\*)?\s*[:|]\s*(?:\*\*)?\s*Internal\b') { $script:Levels['K22-REPO-12'] = 'SHOULD' }
foreach ($check in Get-AssuranceCheck (Get-FileText 'docs/security/assurance.json') $script:Paths $workflows $assuranceReadme)
{
Add-Result $check.Id $check.Status $check.Detail $check.Aspects
}
function Test-Calls
{
# True when any workflow calls the named standards reusable workflow (by remote
+17
View File
@@ -63,6 +63,14 @@ Future notes.
Current notes.
## Changes
Native category notes.
## Breaking changes
Current breaking details.
## 1.2.2
Older notes.
@@ -100,6 +108,7 @@ $manifest = Get-Content -LiteralPath (Join-Path $first "$prefix.manifest.json")
Test-Condition ($manifest.version -eq '1.2.3' -and $manifest.commit -eq (git -C $source rev-parse HEAD)) 'manifest identifies version and exact source commit'
$notes = Get-Content -LiteralPath (Join-Path $first "$prefix.notes.md") -Raw
Test-Condition ($notes -match 'Current notes' -and $notes -notmatch 'Older notes|Future notes' -and $notes.Contains($checklist)) 'notes contain only this release and its checklist'
Test-Condition ($notes -match 'Native category notes' -and $notes -match 'Current breaking details') 'notes preserve native H2 categories within a release'
foreach($file in (Get-ChildItem -LiteralPath $first -File))
{
Test-Condition ((Get-FileHash -LiteralPath $file.FullName).Hash -eq (Get-FileHash -LiteralPath (Join-Path $second $file.Name)).Hash) "$($file.Name) is reproducible"
@@ -115,6 +124,14 @@ Test-Exception { & $builder -Version '1.2.4' -RepositoryPath $source -OutputPath
Test-Exception { & $builder -Version '01.2.3' -RepositoryPath $source } 'ValidatePattern|validation|pattern'
Test-Exception { & $builder -Version '1.2.3' -RepositoryPath $source -OutputPath $first } 'already exists'
Test-Exception { & $builder -Version '1.2.3' -RepositoryPath $source -OutputPath (Join-Path $fixtureRoot 'invalid-url') -ChecklistUrl 'http://example.com/123' } 'GitHub issue URL'
$nativeChangelog = (Get-Content -LiteralPath (Join-Path $source 'CHANGELOG.md') -Raw).Replace('## [1.2.3](https://example.com/current)', '## 1.2.3')
Set-Content -LiteralPath (Join-Path $source 'CHANGELOG.md') -Value $nativeChangelog
git -C $source add -- CHANGELOG.md
git -C $source -c user.name=Fixture -c user.email=fixture@example.invalid -c commit.gpgsign=false commit --quiet -m 'test: native release heading fixture'
$nativeOutput = Join-Path $fixtureRoot 'native-headings'
& $builder -Version '1.2.3' -RepositoryPath $source -OutputPath $nativeOutput
$nativeNotes = Get-Content -LiteralPath (Join-Path $nativeOutput "$prefix.notes.md") -Raw
Test-Condition ($nativeNotes -match 'Native category notes' -and $nativeNotes -match 'Current breaking details' -and $nativeNotes -notmatch 'Older notes|Future notes') 'native unlinked version heading retains only its release categories'
Set-Content -LiteralPath (Join-Path $source 'CHANGELOG.md') -Value '# Changelog without release notes'
git -C $source add -- CHANGELOG.md
git -C $source -c user.name=Fixture -c user.email=fixture@example.invalid -c commit.gpgsign=false commit --quiet -m 'test: missing release notes fixture'
+155
View File
@@ -0,0 +1,155 @@
#Requires -Version 7.4
# Copyright (c) 2026 James Maes
# SPDX-License-Identifier: MIT
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
BeforeAll `
{
$lines = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../.github/workflows/release-velopack.yml')
$start = [Array]::IndexOf($lines, " @'")
$end = $start + 1
while($end -lt $lines.Count -and -not $lines[$end].StartsWith(" '@ | Set-Content")) { $end++ }
if($start -lt 0 -or $end -ge $lines.Count) { throw 'Cannot locate the actual Velopack signing wrapper.' }
$script:Wrapper = Join-Path $TestDrive 'sign-one.ps1'
[IO.File]::WriteAllLines($script:Wrapper, @($lines[($start + 1)..($end - 1)] | ForEach-Object { $_.Substring(10) }))
$dependencyStart = [Array]::IndexOf($lines, ' - name: Prepare pinned Artifact Signing CLI dependency')
if($dependencyStart -lt 0) { throw 'Cannot locate the actual signing dependency bootstrap.' }
$dependencyStart += 4
$dependencyEnd = $dependencyStart
while($dependencyEnd -lt $lines.Count -and $lines[$dependencyEnd].StartsWith(' ')) { $dependencyEnd++ }
$script:Bootstrap = Join-Path $TestDrive 'sign-dependency.ps1'
# GitHub's default pwsh shell prepends this statement to every inline run body.
[IO.File]::WriteAllLines($script:Bootstrap, @('$ErrorActionPreference = "stop"') + @($lines[$dependencyStart..($dependencyEnd - 1)] | ForEach-Object { $_.Substring(10) }))
$script:OldTool = $env:K22_VPK_SIGN_TOOL
$env:K22_VPK_SIGN_TOOL = 'Invoke-WrapperTool'
<#
.SYNOPSIS
Simulates signing and verification without using credentials.
#>
function Invoke-WrapperTool
{
[CmdletBinding()]
param([Parameter(ValueFromRemainingArguments)] [string[]] $ToolArguments)
$ToolCalls.Add($ToolArguments)
$global:LASTEXITCODE = if($ToolArguments[0] -eq 'sign') { $SignExit } else { $VerifyExit }
}
}
Describe 'The actual Artifact Signing dependency bootstrap' `
{
BeforeEach `
{
$script:InstallRoot = Join-Path $TestDrive ([guid]::NewGuid().ToString('N'))
$script:PackageDirectory = Join-Path $script:InstallRoot 'downloads'
[void][IO.Directory]::CreateDirectory($script:PackageDirectory)
$script:Destination = Join-Path $script:InstallRoot 'ArtifactSigning/sign/sign.0.9.1-beta.26227.3'
Mock Invoke-WebRequest `
{
$fixture = Join-Path (Split-Path -Parent $OutFile) 'fixture'
[void][IO.Directory]::CreateDirectory((Join-Path $fixture 'tools/net8.0/any'))
[IO.File]::WriteAllText((Join-Path $fixture 'tools/net8.0/any/sign.dll'), 'fixture')
[IO.Compression.ZipFile]::CreateFromDirectory($fixture, $OutFile)
}
Mock Get-FileHash { [pscustomobject]@{ Hash = '34FD0D4AEABDBC363A48883881865B4DD65E6C11BA916028082FA23F3E1B1BA1' } }
Mock dotnet { $global:LASTEXITCODE = 0 }
}
It 'stages the verified tool where the official action finds it' `
{
& $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory
Get-Content -LiteralPath (Join-Path $script:Destination 'tools/net8.0/any/sign.dll') | Should -Be 'fixture'
Should -Invoke dotnet -Exactly 1
}
It 'rejects a changed download before verification or extraction' `
{
Mock Get-FileHash { [pscustomobject]@{ Hash = 'bad' } }
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*checksum mismatch*'
Test-Path -LiteralPath $script:Destination | Should -BeFalse
Should -Invoke dotnet -Exactly 0
}
It 'rejects an invalid NuGet signature before extraction' `
{
Mock dotnet { $global:LASTEXITCODE = 1 }
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*signature verification failed*'
Test-Path -LiteralPath $script:Destination | Should -BeFalse
}
It 'preserves a preexisting dependency directory' `
{
[void][IO.Directory]::CreateDirectory($script:Destination)
$marker = Join-Path $script:Destination 'marker.txt'
[IO.File]::WriteAllText($marker, 'preserve')
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*already exists*'
Get-Content -LiteralPath $marker | Should -Be 'preserve'
Should -Invoke Invoke-WebRequest -Exactly 0
}
It 'rejects a package without the expected executable' `
{
Mock Invoke-WebRequest `
{
$emptyFixture = Join-Path (Split-Path -Parent $OutFile) 'empty'
[void][IO.Directory]::CreateDirectory($emptyFixture)
[IO.Compression.ZipFile]::CreateFromDirectory($emptyFixture, $OutFile)
}
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*missing its executable*'
}
}
AfterAll `
{
$env:K22_VPK_SIGN_TOOL = $script:OldTool
}
Describe 'The actual Velopack signing wrapper' `
{
BeforeEach `
{
$script:ToolCalls = [Collections.Generic.List[object]]::new()
$script:SignExit = 0
$script:VerifyExit = 0
Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = 'NotSigned' } }
}
It 'preserves a valid existing signature without signing again' `
{
Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = 'Valid' } }
& $script:Wrapper -File 'a file.dll'
$script:ToolCalls.Count | Should -Be 0
}
It 'refuses an invalid existing signature' `
{
Mock Get-AuthenticodeSignature { [pscustomobject]@{ Status = 'HashMismatch' } }
{ & $script:Wrapper -File 'a file.dll' } | Should -Throw '*invalid signature*'
$script:ToolCalls.Count | Should -Be 0
}
It 'stops on signing failure before verification' `
{
$script:SignExit = 1
{ & $script:Wrapper -File 'a file.dll' } | Should -Throw '*Artifact Signing failed*'
$script:ToolCalls.Count | Should -Be 1
}
It 'stops when signature verification fails' `
{
$script:VerifyExit = 1
{ & $script:Wrapper -File 'a file.dll' } | Should -Throw '*Signature verification failed*'
$script:ToolCalls.Count | Should -Be 2
}
It 'uses SHA256, RFC3161 timestamping and all-signature trust verification' `
{
& $script:Wrapper -File 'a file.dll'
$script:ToolCalls.Count | Should -Be 2
($script:ToolCalls[0] -join ' ') | Should -BeLike 'sign /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256* a file.dll'
($script:ToolCalls[1] -join ' ') | Should -Be 'verify /pa /all /v a file.dll'
}
}
+361
View File
@@ -0,0 +1,361 @@
#Requires -Version 7.4
# Copyright (c) 2026 James Maes (KofTwentyTwo)
# SPDX-License-Identifier: MIT
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
BeforeDiscovery `
{
Import-Module (Join-Path $PSScriptRoot 'WindowsSigning.psm1') -Force -Global
}
Describe 'Owned binary catalog' `
{
BeforeEach `
{
$script:Root = Join-Path $TestDrive ([guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $script:Root | Out-Null
$script:Catalog = Join-Path $script:Root 'catalog.txt'
$script:Manifest = Join-Path $script:Root 'manifest.json'
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $script:Root 'Owned.dll')
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $script:Root 'Microsoft.dll')
}
It 'includes only explicit owned names and inventories other binaries' `
{
New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest -PreserveOtherBinaries
@(Get-Content -LiteralPath $script:Catalog) | Should -HaveCount 1
Get-Content -LiteralPath $script:Catalog | Should -Be 'Owned.dll'
$entries = @(Get-Content -LiteralPath $script:Manifest -Raw | ConvertFrom-Json)
$entries | Should -HaveCount 2
@($entries | Where-Object owned).relativePath | Should -Be 'Owned.dll'
@($entries | Where-Object { -not $_.owned }).relativePath | Should -Be 'Microsoft.dll'
}
It 'rejects a missing required binary rather than silently skipping signing' `
{
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Missing.exe') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } |
Should -Throw '*No binary matches*'
}
It 'rejects parent traversal' `
{
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('../Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } |
Should -Throw '*relative paths*'
}
It 'supports WhatIf without writing signing inputs' `
{
New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest -WhatIf
Test-Path -LiteralPath $script:Catalog | Should -BeFalse
Test-Path -LiteralPath $script:Manifest | Should -BeFalse
}
It 'rejects malformed owned PE input before writing a catalog' `
{
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Owned.dll'), [byte[]]::new(128))
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Throw '*Invalid published PE binary: Owned.dll*'
Test-Path -LiteralPath $script:Catalog | Should -BeFalse
}
It 'rejects an already malformed preserved dependency' `
{
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Microsoft.dll'), [byte[]]::new(128))
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest -PreserveOtherBinaries } | Should -Throw '*Invalid published PE binary: Microsoft.dll*'
Test-Path -LiteralPath $script:Manifest | Should -BeFalse
}
It 'does not treat package-format signing targets as PE images' `
{
Set-Content -LiteralPath (Join-Path $script:Root 'Setup.msi') -Value 'package fixture; signature validation is a later gate'
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Setup.msi') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Not -Throw
Get-Content -LiteralPath $script:Catalog | Should -Be 'Setup.msi'
}
It 'ignores excluded binaries when dependency preservation is disabled' `
{
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Microsoft.dll'), [byte[]]::new(128))
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Not -Throw
@(Get-Content -LiteralPath $script:Manifest -Raw | ConvertFrom-Json) | Should -HaveCount 1
}
}
Describe 'Signature and distributed-byte gates' `
{
InModuleScope WindowsSigning `
{
BeforeAll `
{
<#
.SYNOPSIS
Supplies deterministic SignTool results for rejection-path tests.
#>
function Invoke-ProbeSignTool
{
$global:LASTEXITCODE = $script:ProbeExitCode
return $script:ProbeOutput
}
}
BeforeEach `
{
$script:ProbeExitCode = 0
$script:ProbeOutput = 'Hash of file (sha256): 0123456789'
$script:ProbeSignature = [pscustomobject]@{
Status = 'Valid'
SignerCertificate = [pscustomobject]@{ Subject = 'CN=Verified Publisher'; SubjectName = [pscustomobject]@{ Name = 'CN=Verified Publisher' } }
TimeStamperCertificate = [pscustomobject]@{ Subject = 'CN=Timestamp Authority' }
}
Mock Get-AuthenticodeSignature { return $script:ProbeSignature }
}
It 'accepts a trusted timestamped SHA256 signature for the verified subject' `
{
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
}
It 'rejects a native verification failure' `
{
$script:ProbeExitCode = 1
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*verification failed*'
}
It 'rejects a different publisher' `
{
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Other Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Unexpected publisher*'
}
It 'rejects a missing timestamp' `
{
$script:ProbeSignature.TimeStamperCertificate = $null
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*timestamped*'
}
It 'rejects an untrusted certificate' `
{
$script:ProbeSignature.Status = 'NotTrusted'
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*timestamped*'
}
It 'rejects a non-SHA256 digest' `
{
$script:ProbeOutput = 'Hash of file (sha1): 0123456789'
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*SHA256*'
}
It 'rejects a SHA1 signature appended alongside a SHA256 signature' `
{
$script:ProbeOutput = "Hash of file (sha256): abc`nHash of file (sha1): def"
{ Assert-WindowsSignature -Files @('Owned.dll') -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*SHA256*'
}
It 'rejects modified third-party binaries' `
{
$root = Join-Path $TestDrive 'preserve'
New-Item -ItemType Directory -Path $root | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Microsoft.dll')
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
Set-Content -LiteralPath (Join-Path $root 'Microsoft.dll') -Value 'modified'
{ Assert-WindowsSigningManifest -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Third-party binary was modified*'
}
It 'refuses to catalog another publisher signature for replacement' `
{
$root = Join-Path $TestDrive 'other-publisher'
New-Item -ItemType Directory -Path $root | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
$script:ProbeSignature.SignerCertificate.SubjectName.Name = 'CN=Other Publisher'
{ New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath (Join-Path $root 'manifest.json') -ExpectedSubject 'CN=Verified Publisher' } | Should -Throw '*another publisher*'
Test-Path -LiteralPath (Join-Path $root 'catalog.txt') | Should -BeFalse
}
It 'rejects changed owned bytes in the final package' `
{
$root = Join-Path $TestDrive 'changed'
$assets = Join-Path $root 'assets'
$packed = Join-Path $root 'packed'
New-Item -ItemType Directory -Path $root, $assets, $packed | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
Set-Content -LiteralPath (Join-Path $packed 'Owned.dll') -Value 'modified after signing'
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned.1.0.0.nupkg'))
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Distributed binary changed*'
}
It 'rejects an archive missing its required owned binary' `
{
$root = Join-Path $TestDrive 'missing'
$assets = Join-Path $root 'assets'
$packed = Join-Path $root 'packed'
New-Item -ItemType Directory -Path $root, $assets, $packed | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
Set-Content -LiteralPath (Join-Path $packed 'README.txt') -Value 'no binary'
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned.1.0.0.nupkg'))
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Owned binary missing*'
}
It 'rejects a missing dependency from <Archive>' -TestCases @(
@{ Archive = 'Owned-Portable.zip'; Head = 'app' },
@{ Archive = 'Owned-1.0.0-full.nupkg'; Head = 'app' },
@{ Archive = 'Owned-cli-win-x64.zip'; Head = 'cli' }
) `
{
param($Archive, $Head)
$root = Join-Path $TestDrive ('missing-dependency-' + [guid]::NewGuid().ToString('N'))
$publish = Join-Path $root 'publish'
$assets = Join-Path $root 'assets'
$packed = Join-Path $root 'packed'
New-Item -ItemType Directory -Path "$publish/$Head", $assets, $packed | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/$Head/Owned.dll"
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/$Head/Microsoft.dll"
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $publish -Patterns @("$Head/Owned.dll") -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
Copy-Item -LiteralPath "$publish/$Head/Owned.dll" -Destination $packed
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets $Archive))
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Third-party binary missing*'
}
It 'allows only the default excluded foreign runtime helpers to be omitted' `
{
$root = Join-Path $TestDrive 'excluded-runtime'
$publish = Join-Path $root 'publish'
$assets = Join-Path $root 'assets'
$packed = Join-Path $root 'packed'
New-Item -ItemType Directory -Path "$publish/app", $assets, $packed | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Microsoft.dll"
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/createdump.exe"
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.vshost.exe"
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
Copy-Item -LiteralPath "$publish/app/Owned.dll", "$publish/app/Microsoft.dll" -Destination $packed
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned-Portable.zip'))
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
}
It 'rejects changed dependency bytes in the final archive' `
{
$root = Join-Path $TestDrive 'changed-dependency'
$publish = Join-Path $root 'publish'
$assets = Join-Path $root 'assets'
$packed = Join-Path $root 'packed'
New-Item -ItemType Directory -Path "$publish/app", $assets, $packed | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Microsoft.dll"
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
Copy-Item -LiteralPath "$publish/app/Owned.dll" -Destination $packed
Set-Content -LiteralPath (Join-Path $packed 'Microsoft.dll') -Value 'changed'
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned-Portable.zip'))
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Throw '*Distributed binary changed*'
}
It 'accepts the exact signed bytes shipped in a package' `
{
$root = Join-Path $TestDrive 'valid'
$assets = Join-Path $root 'assets'
New-Item -ItemType Directory -Path $root, $assets | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
$packed = Join-Path $root 'packed'
New-Item -ItemType Directory -Path $packed | Out-Null
Copy-Item -LiteralPath (Join-Path $root 'Owned.dll') -Destination $packed
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets 'Owned.1.0.0.nupkg'))
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
}
It 'verifies separate NuGet packages whose IDs share the core package prefix' `
{
$root = Join-Path $TestDrive 'prefix'
$assets = Join-Path $root 'assets'
New-Item -ItemType Directory -Path $root, $assets | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.Adapter.dll')
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll', 'Owned.Adapter.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
foreach($name in @('Owned', 'Owned.Adapter'))
{
$packed = Join-Path $root $name
New-Item -ItemType Directory -Path $packed | Out-Null
Copy-Item -LiteralPath (Join-Path $root "$name.dll") -Destination $packed
[IO.Compression.ZipFile]::CreateFromDirectory($packed, (Join-Path $assets "$name.1.0.0.nupkg"))
}
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
}
It 'preserves same-name third-party satellite binaries in different culture directories' `
{
$root = Join-Path $TestDrive 'cultures'
$publish = Join-Path $root 'publish'
$assets = Join-Path $root 'assets'
New-Item -ItemType Directory -Path $assets, "$publish/app/en", "$publish/app/fr" | Out-Null
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/en/Library.resources.dll"
[IO.File]::AppendAllText("$publish/app/en/Library.resources.dll", 'English')
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/fr/Library.resources.dll"
[IO.File]::AppendAllText("$publish/app/fr/Library.resources.dll", 'French')
$manifest = Join-Path $root 'manifest.json'
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
[IO.Compression.ZipFile]::CreateFromDirectory("$publish/app", (Join-Path $assets 'Owned-Portable.zip'))
{ Assert-WindowsDistribution -Directory $assets -ManifestPath $manifest -ExpectedSubject 'CN=Verified Publisher' -SignTool Invoke-ProbeSignTool } | Should -Not -Throw
}
}
}
Describe 'Velopack installer payload' `
{
BeforeEach `
{
$root = Join-Path $TestDrive ([guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $root | Out-Null
$script:Installer = Join-Path $root 'Probe-Setup.exe'
$script:FullPackage = Join-Path $root 'Probe-full.nupkg'
$payload = [Text.Encoding]::UTF8.GetBytes('exact full package payload')
[IO.File]::WriteAllBytes($script:FullPackage, $payload)
Copy-Item -LiteralPath (Get-WindowsSignTool) -Destination $script:Installer
$stream = [IO.File]::Open($script:Installer, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite)
$reader = [Reflection.PortableExecutable.PEReader]::new($stream, [Reflection.PortableExecutable.PEStreamOptions]::LeaveOpen)
try
{
$headerPosition = $reader.PEHeaders.SectionHeaders[0].PointerToRawData
$packageOffset = $stream.Length
$stream.Position = $headerPosition
$stream.Write([BitConverter]::GetBytes($packageOffset))
$stream.Write([BitConverter]::GetBytes([long] $payload.Length))
$stream.Write([Convert]::FromHexString('94F0B17B6893E02937EB34EF53AAE7D42B54F5707EF5D6F57854983E5E94ED7D'))
$stream.Position = $packageOffset
$stream.Write($payload)
}
finally
{
$reader.Dispose()
$stream.Dispose()
}
}
It 'accepts the exact embedded full-package bytes' `
{
{ Assert-VelopackInstallerPayload -Installer $script:Installer -FullPackage $script:FullPackage } | Should -Not -Throw
}
It 'rejects a different embedded payload of the same length' `
{
$bytes = [IO.File]::ReadAllBytes($script:FullPackage)
$bytes[0] = 0
[IO.File]::WriteAllBytes($script:FullPackage, $bytes)
{ Assert-VelopackInstallerPayload -Installer $script:Installer -FullPackage $script:FullPackage } | Should -Throw '*different full package*'
}
It 'rejects a different full-package length' `
{
[IO.File]::WriteAllText($script:FullPackage, 'short')
{ Assert-VelopackInstallerPayload -Installer $script:Installer -FullPackage $script:FullPackage } | Should -Throw '*length differs*'
}
}
+337
View File
@@ -0,0 +1,337 @@
#Requires -Version 7.4
# Copyright (c) 2026 James Maes (KofTwentyTwo)
# SPDX-License-Identifier: MIT
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
<#
.SYNOPSIS
Resolves the newest installed x64 Windows SDK SignTool.
#>
function Get-WindowsSignTool
{
[CmdletBinding()]
param()
$sdk = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits/10/bin'
$tools = @(Get-ChildItem -Path "$sdk/*/x64/signtool.exe" -File -ErrorAction SilentlyContinue |
Sort-Object { [version] $_.Directory.Parent.Name } -Descending)
if($tools.Count -eq 0) { throw 'Install the Windows SDK with x64 SignTool.' }
return $tools[0].FullName
}
<#
.SYNOPSIS
Builds an explicit signing catalog and records other binaries for preservation checks.
#>
function New-WindowsSigningCatalog
{
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)] [string] $Root,
[Parameter(Mandatory)] [string[]] $Patterns,
[Parameter(Mandatory)] [string] $CatalogPath,
[Parameter(Mandatory)] [string] $ManifestPath,
[string] $ExpectedSubject = '',
[switch] $PreserveOtherBinaries
)
$rootPath = (Resolve-Path -LiteralPath $Root).Path
$binaries = @(Get-ChildItem -LiteralPath $rootPath -File -Recurse |
Where-Object Extension -In '.exe', '.dll', '.msi', '.msix')
$owned = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
foreach($pattern in $Patterns)
{
$pattern = $pattern.Trim().Replace('\', '/')
if([string]::IsNullOrWhiteSpace($pattern) -or $pattern.Split('/') -contains '..' -or [IO.Path]::IsPathRooted($pattern))
{
throw "Signing patterns must be nonempty relative paths or file names: '$pattern'."
}
$catalogMatches = @($binaries | Where-Object {
$candidate = if($pattern.Contains('/')) { [IO.Path]::GetRelativePath($rootPath, $_.FullName).Replace('\', '/') } else { $_.Name }
$candidate -like $pattern
})
if($catalogMatches.Count -eq 0) { throw "No binary matches required signing pattern '$pattern'." }
foreach($file in $catalogMatches)
{
if($ExpectedSubject)
{
$expected = [Security.Cryptography.X509Certificates.X500DistinguishedName]::new($ExpectedSubject).Name
$existing = Get-AuthenticodeSignature -LiteralPath $file.FullName
if($null -ne $existing.SignerCertificate -and $existing.SignerCertificate.SubjectName.Name -cne $expected)
{
throw "Refusing to replace another publisher's signature on '$($file.FullName)'. Keep it outside the owned catalog."
}
if($existing.Status -notin 'Valid', 'NotSigned') { throw "Invalid existing signature on '$($file.FullName)'." }
}
$null = $owned.Add($file.FullName)
}
}
foreach($file in $binaries | Where-Object { ($PreserveOtherBinaries -or $owned.Contains($_.FullName)) -and $_.Extension -in '.exe', '.dll' })
{
$stream = [IO.File]::OpenRead($file.FullName)
try
{
$reader = [Reflection.PortableExecutable.PEReader]::new($stream)
try
{
if($null -eq $reader.PEHeaders.PEHeader) { throw 'Missing executable header.' }
}
finally { $reader.Dispose() }
}
catch { throw "Invalid published PE binary: $($file.Name). $($_.Exception.Message)" }
finally { $stream.Dispose() }
}
$entries = @($binaries | Where-Object { $PreserveOtherBinaries -or $owned.Contains($_.FullName) } | ForEach-Object {
[ordered]@{
path = $_.FullName
relativePath = [IO.Path]::GetRelativePath($rootPath, $_.FullName).Replace('\', '/')
owned = $owned.Contains($_.FullName)
sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash
}
})
$catalogDirectory = [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($CatalogPath))
if(-not $PSCmdlet.ShouldProcess("$CatalogPath; $ManifestPath", 'Write signing catalog and preservation manifest')) { return }
New-Item -ItemType Directory -Force -Path $catalogDirectory | Out-Null
@($owned | Sort-Object | ForEach-Object { [IO.Path]::GetRelativePath($catalogDirectory, $_) }) |
Set-Content -LiteralPath $CatalogPath -Encoding utf8NoBOM
ConvertTo-Json -InputObject $entries -Depth 5 | Set-Content -LiteralPath $ManifestPath -Encoding utf8NoBOM
}
<#
.SYNOPSIS
Requires all Authenticode signatures to validate, with the configured publisher and a timestamp.
#>
function Assert-WindowsSignature
{
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string[]] $Files,
[Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ExpectedSubject,
[string] $SignTool = (Get-WindowsSignTool)
)
if($Files.Count -eq 0) { throw 'No shipped binaries were supplied for verification.' }
$expected = [Security.Cryptography.X509Certificates.X500DistinguishedName]::new($ExpectedSubject).Name
foreach($file in $Files)
{
$output = (& $SignTool verify /pa /all /v $file 2>&1) -join "`n"
Write-Output $output
if($LASTEXITCODE -ne 0) { throw "SignTool verification failed for '$file'." }
$signature = Get-AuthenticodeSignature -LiteralPath $file
if($signature.Status -ne 'Valid' -or $null -eq $signature.SignerCertificate -or $null -eq $signature.TimeStamperCertificate)
{
throw "Missing valid timestamped Authenticode signature on '$file'."
}
if($signature.SignerCertificate.SubjectName.Name -cne $expected)
{
throw "Unexpected publisher on '$file': $($signature.SignerCertificate.Subject)."
}
$digests = @([regex]::Matches($output, '(?i)Hash of file \(([^)]+)\)') | ForEach-Object { $_.Groups[1].Value })
if($digests.Count -eq 0 -or @($digests | Where-Object { $_ -ne 'sha256' }).Count -gt 0)
{
throw "SignTool did not report a SHA256 file digest for '$file'. Use the English Windows SDK verification output."
}
}
}
<#
.SYNOPSIS
Verifies signed catalog entries and checks that other binary bytes were preserved.
#>
function Assert-WindowsSigningManifest
{
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $ManifestPath,
[Parameter(Mandatory)] [string] $ExpectedSubject,
[string] $SignTool = (Get-WindowsSignTool)
)
$entries = @(Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json)
$owned = @($entries | Where-Object owned | ForEach-Object path)
Assert-WindowsSignature -Files $owned -ExpectedSubject $ExpectedSubject -SignTool $SignTool
foreach($entry in $entries | Where-Object { -not $_.owned })
{
if((Get-FileHash -LiteralPath $entry.path -Algorithm SHA256).Hash -ne $entry.sha256)
{
throw "Third-party binary was modified: $($entry.relativePath)."
}
}
}
<#
.SYNOPSIS
Checks signed binaries inside the final ZIP/NuGet assets and their bytes against the signed build.
#>
function Assert-WindowsDistribution
{
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $Directory,
[Parameter(Mandatory)] [string] $ManifestPath,
[Parameter(Mandatory)] [string] $ExpectedSubject,
[string] $SignTool = (Get-WindowsSignTool)
)
$entries = @(Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json)
$owned = @($entries | Where-Object owned)
if($owned.Count -eq 0) { throw 'The signing manifest has no owned binaries.' }
$assets = @(Get-ChildItem -LiteralPath $Directory -File)
$archives = @($assets | Where-Object { $_.Extension -in '.zip', '.nupkg' -and $_.Name -notlike '*-delta.nupkg' })
if($archives.Count -eq 0) { throw 'No final ZIP or full NuGet package was produced.' }
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
foreach($archive in $archives)
{
$scratch = Join-Path ([IO.Path]::GetTempPath()) ('k22-signing-' + [guid]::NewGuid().ToString('N'))
try
{
[IO.Compression.ZipFile]::ExtractToDirectory($archive.FullName, $scratch)
$files = @(Get-ChildItem -LiteralPath $scratch -File -Recurse)
$archiveSeen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
foreach($entry in $entries)
{
$name = [IO.Path]::GetFileName($entry.path)
$packageRelative = if($entry.relativePath.StartsWith('app/') -or $entry.relativePath.StartsWith('cli/')) { $entry.relativePath.Substring(4) } else { $name }
$copies = @($files | Where-Object {
$relative = [IO.Path]::GetRelativePath($scratch, $_.FullName).Replace('\', '/')
$_.Name -eq $name -and ($relative -eq $packageRelative -or $relative.EndsWith('/' + $packageRelative, [StringComparison]::OrdinalIgnoreCase))
})
foreach($copy in $copies)
{
$hash = (Get-FileHash -LiteralPath $copy.FullName -Algorithm SHA256).Hash
$expectedHash = if($entry.owned) { (Get-FileHash -LiteralPath $entry.path -Algorithm SHA256).Hash } else { $entry.sha256 }
if($hash -ne $expectedHash)
{
# The portable archive also carries a generated launcher at its root.
if($entry.owned -and $copy.Extension -eq '.exe' -and $copy.DirectoryName -eq $scratch -and $archive.Name -like '*-Portable.zip')
{
Assert-WindowsSignature -Files @($copy.FullName) -ExpectedSubject $ExpectedSubject -SignTool $SignTool
continue
}
throw "Distributed binary changed in $($archive.Name): $name."
}
if($entry.owned)
{
Assert-WindowsSignature -Files @($copy.FullName) -ExpectedSubject $ExpectedSubject -SignTool $SignTool
$null = $seen.Add($entry.path)
}
$null = $archiveSeen.Add($entry.path)
}
}
$required = @($entries | Where-Object {
if($archive.Name -like '*-cli-*.zip') { return $_.relativePath.StartsWith('cli/') }
if($archive.Name -like '*-Portable.zip' -or $archive.Name -like '*-full.nupkg')
{
# Match Velopack's default exclusions for runtime helpers.
if(-not $_.owned -and $_.relativePath -match '(?:^|/)createdump[^/]*|\.vshost\.') { return $false }
return $_.relativePath.StartsWith('app/')
}
$packageId = [regex]::Escape([IO.Path]::GetFileNameWithoutExtension($_.path))
return $_.owned -and $archive.Name -match "^$packageId\.[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?\.nupkg$"
})
foreach($entry in $required)
{
if(-not $archiveSeen.Contains($entry.path))
{
$kind = if($entry.owned) { 'Owned' } else { 'Third-party' }
throw "$kind binary missing from $($archive.Name): $($entry.relativePath)."
}
}
# Velopack creates these branded helpers before its signing hook.
$updates = @($files | Where-Object { $_.Name -in 'Update.exe', 'Squirrel.exe' -or $_.Name -like '*_ExecutionStub.exe' } | ForEach-Object FullName)
if($updates.Count -gt 0) { Assert-WindowsSignature -Files $updates -ExpectedSubject $ExpectedSubject -SignTool $SignTool }
}
finally
{
$temporaryRoot = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath([IO.Path]::GetTempPath()))
if([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($scratch)) -ne $temporaryRoot -or [IO.Path]::GetFileName($scratch) -notlike 'k22-signing-*')
{
throw 'Refusing to remove a directory outside the signing scratch location.'
}
if(Test-Path -LiteralPath $scratch) { Remove-Item -LiteralPath $scratch -Recurse -Force }
}
}
foreach($entry in $owned)
{
if(-not $seen.Contains($entry.path)) { throw "Owned binary is missing from distributed archives: $($entry.relativePath)." }
}
$installers = @($assets | Where-Object Extension -In '.exe', '.msi', '.msix' | ForEach-Object FullName)
if($installers.Count -gt 0) { Assert-WindowsSignature -Files $installers -ExpectedSubject $ExpectedSubject -SignTool $SignTool }
foreach($setup in $assets | Where-Object Name -Like '*-Setup.exe')
{
$fullPackages = @($assets | Where-Object Name -Like '*-full.nupkg')
if($fullPackages.Count -ne 1) { throw 'Require one current full package to verify the installer payload.' }
Assert-VelopackInstallerPayload -Installer $setup.FullName -FullPackage $fullPackages[0].FullName
}
}
<#
.SYNOPSIS
Requires the signed Velopack installer to embed the exact verified full update package.
#>
function Assert-VelopackInstallerPayload
{
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] $Installer,
[Parameter(Mandatory)] [string] $FullPackage
)
# Velopack 1.2.161 SetupBundle: two little-endian Int64s followed by this marker.
$marker = '94F0B17B6893E02937EB34EF53AAE7D42B54F5707EF5D6F57854983E5E94ED7D'
$stream = [IO.File]::OpenRead($Installer)
$reader = [Reflection.PortableExecutable.PEReader]::new($stream, [Reflection.PortableExecutable.PEStreamOptions]::LeaveOpen)
$hasher = [Security.Cryptography.IncrementalHash]::CreateHash([Security.Cryptography.HashAlgorithmName]::SHA256)
try
{
$offset = 0L
$length = 0L
$headersFound = 0
foreach($section in $reader.PEHeaders.SectionHeaders)
{
[byte[]] $bytes = $reader.GetSectionData($section.VirtualAddress).GetContent()
$position = [Convert]::ToHexString($bytes).IndexOf($marker, [StringComparison]::Ordinal)
if($position -lt 32 -or $position % 2 -ne 0) { continue }
$headersFound++
$offset = [BitConverter]::ToInt64($bytes, $position / 2 - 16)
$length = [BitConverter]::ToInt64($bytes, $position / 2 - 8)
}
if($headersFound -ne 1 -or $offset -le 0 -or $length -le 0 -or $offset -gt $stream.Length -or $length -gt $stream.Length - $offset)
{
throw 'The Velopack installer bundle header is missing, ambiguous or outside the file.'
}
if($length -ne (Get-Item -LiteralPath $FullPackage).Length) { throw 'Installer payload length differs from the full package.' }
$stream.Position = $offset
$buffer = [byte[]]::new(1048576)
$remaining = $length
while($remaining -gt 0)
{
$count = $stream.Read($buffer, 0, [int] [math]::Min($buffer.Length, $remaining))
if($count -eq 0) { throw 'Unexpected end of the installer payload.' }
$hasher.AppendData($buffer, 0, $count)
$remaining -= $count
}
$hash = [Convert]::ToHexString($hasher.GetHashAndReset())
if($hash -ne (Get-FileHash -LiteralPath $FullPackage -Algorithm SHA256).Hash) { throw 'Installer embeds a different full package.' }
}
finally
{
$hasher.Dispose()
$reader.Dispose()
$stream.Dispose()
}
}
Export-ModuleMember -Function Get-WindowsSignTool, New-WindowsSigningCatalog, Assert-WindowsSignature, Assert-WindowsSigningManifest, Assert-WindowsDistribution, Assert-VelopackInstallerPayload
+1 -1
View File
@@ -1 +1 @@
0.1.0
1.2.1