diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 77e6268..42e25b7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,32 +28,11 @@ updates: # dependabot-automerge.yml workflow for low-touch security # maintenance. - package-ecosystem: "cargo" - directory: "/" + directory: "/rust/mustfile-orchestrator" schedule: interval: "weekly" open-pull-requests-limit: 0 - # Elixir/Mix - - package-ecosystem: "mix" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 3 - - # Node.js/npm - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 3 - - # Python/pip - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 3 - # NOTE: Nix is banned estate-wide — Guix is the sole package manager. # Do not add a `nix` ecosystem here. Guix has no Dependabot ecosystem; # pin Guix channels/manifests via build/ tooling instead. diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index c7479ee..35c9d3d 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -6,8 +6,8 @@ workflows: '.github/workflows/boj-build.yml': - 'actions/checkout@v7.0.1' '.github/workflows/codeql.yml': - - 'actions/checkout@v7.0.1' - - 'github/codeql-action@v4.37.9' + - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' + - 'github/codeql-action@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2' '.github/workflows/container-build.yml': - 'actions/checkout@v7.0.1' '.github/workflows/dependabot-automerge.yml': @@ -19,12 +19,14 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/estate-rules.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/governance.yml': [] + '.github/workflows/governance.yml': + - 'hyperpolymath/standards@11ba299dca221b6d4e970eeb58dd17ccbb825a9a' '.github/workflows/guix-nix-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/guix-policy.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/hypatia-scan.yml': [] + '.github/workflows/hypatia-scan.yml': + - 'hyperpolymath/standards@11ba299dca221b6d4e970eeb58dd17ccbb825a9a' '.github/workflows/instant-sync.yml': - 'peter-evans/repository-dispatch@v4.0.1' '.github/workflows/jekyll-gh-pages.yml': @@ -35,10 +37,11 @@ workflows: - 'actions/upload-pages-artifact@v5.0.0' '.github/workflows/jekyll.yml': - 'actions/checkout@v7.0.1' - - 'ruby/setup-ruby@v1.321.0' + - 'ruby/setup-ruby@v1.326.0' '.github/workflows/label-triage.yml': [] '.github/workflows/labels.yml': [] - '.github/workflows/mirror.yml': [] + '.github/workflows/mirror.yml': + - 'hyperpolymath/standards@11ba299dca221b6d4e970eeb58dd17ccbb825a9a' '.github/workflows/npm-bun-blocker.yml': - 'actions/checkout@v7.0.1' '.github/workflows/openssf-compliance.yml': @@ -49,7 +52,7 @@ workflows: - 'actions/configure-pages@v6.0.0' - 'actions/deploy-pages@v5.0.1' - 'actions/upload-pages-artifact@v5.0.0' - - 'haskell-actions/setup@v2.12.0' + - 'haskell-actions/setup@v2.12.1' '.github/workflows/quality.yml': - 'actions/checkout@v7.0.1' - 'editorconfig-checker/action-editorconfig-checker@v3.0.0' @@ -62,17 +65,15 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/runtime-policy.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/scorecard-enforcer.yml': - - 'actions/checkout@v7.0.1' - - 'github/codeql-action@main' - - 'ossf/scorecard-action@v2.4.4' - '.github/workflows/scorecard.yml': [] - '.github/workflows/secret-scanner.yml': [] + '.github/workflows/scorecard.yml': + - 'hyperpolymath/standards@11ba299dca221b6d4e970eeb58dd17ccbb825a9a' + '.github/workflows/secret-scanner.yml': + - 'hyperpolymath/standards@11ba299dca221b6d4e970eeb58dd17ccbb825a9a' '.github/workflows/security-policy.yml': - 'actions/checkout@v7.0.1' '.github/workflows/sonarqube.yml': - 'actions/checkout@v7.0.1' - - 'sonarsource/sonarqube-scan-action@v8.2.1' + - 'sonarsource/sonarqube-scan-action@v8.2.2' '.github/workflows/static-analysis-gate.yml': - 'actions/checkout@v7.0.1' - 'actions/download-artifact@v8.0.1' @@ -84,6 +85,7 @@ workflows: - 'actions/checkout@v7.0.1' '.github/workflows/workflow-linter.yml': - 'actions/checkout@v7.0.1' + - 'oven-sh/setup-bun@v2.2.0' dependencies: 'actions/attest-build-provenance@v4.2.2': ref: 'v4.2.2' @@ -102,6 +104,11 @@ dependencies: commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9' owner_id: 44036562 repo_id: 215566462 + 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1': + ref: '3d3c42e5aac5ba805825da76410c181273ba90b1' + commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' + owner_id: 44036562 + repo_id: 197814629 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1' @@ -159,19 +166,14 @@ dependencies: commit: 'sha1-54075bcc5e249e4758d363f27d099f55d843f124' owner_id: 47606891 repo_id: 331103973 - 'github/codeql-action@main': - ref: 'main' - commit: 'sha1-662472033e021d55d94146f66f6058822b0b39fd' - owner_id: 9919 - repo_id: 259445878 - 'github/codeql-action@v4.37.9': - ref: 'v4.37.9' - commit: 'sha1-cdf488f595d80d6e07e03d4674febd5ab45fa938' + 'github/codeql-action@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2': + ref: 'v4.38.2' + commit: 'sha1-2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2' owner_id: 9919 repo_id: 259445878 - 'haskell-actions/setup@v2.12.0': - ref: 'v2.12.0' - commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d' + 'haskell-actions/setup@v2.12.1': + ref: 'v2.12.1' + commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d' owner_id: 75048950 repo_id: 623796603 'hyperpolymath/k9-ecosystem@main': @@ -179,19 +181,24 @@ dependencies: commit: 'sha1-89f3c2702f4f650a92aa7411502f38da06abd562' owner_id: 6759885 repo_id: 1275650185 - 'ossf/scorecard-action@v2.4.4': - ref: 'v2.4.4' - commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' - owner_id: 67707773 - repo_id: 421101922 + 'hyperpolymath/standards@11ba299dca221b6d4e970eeb58dd17ccbb825a9a': + ref: '11ba299dca221b6d4e970eeb58dd17ccbb825a9a' + commit: 'sha1-11ba299dca221b6d4e970eeb58dd17ccbb825a9a' + owner_id: 6759885 + repo_id: 1116521501 + 'oven-sh/setup-bun@v2.2.0': + ref: 'v2.2.0' + commit: 'sha1-0c5077e51419868618aeaa5fe8019c62421857d6' + owner_id: 108928776 + repo_id: 512644635 'peter-evans/repository-dispatch@v4.0.1': ref: 'v4.0.1' commit: 'sha1-28959ce8df70de7be546dd1250a005dd32156697' owner_id: 18365890 repo_id: 220359305 - 'ruby/setup-ruby@v1.321.0': - ref: 'v1.321.0' - commit: 'sha1-95ef2b042f9d7a56d8268cba8559e2842e2ad01b' + 'ruby/setup-ruby@v1.326.0': + ref: 'v1.326.0' + commit: 'sha1-762794c140bbeda0f1224786aa33b4b46783a6c1' owner_id: 210414 repo_id: 231208785 'softprops/action-gh-release@v3.0.3': @@ -199,8 +206,8 @@ dependencies: commit: 'sha1-efb35369e0ad2afab669f228072c1b0d510eae64' owner_id: 2242 repo_id: 204253808 - 'sonarsource/sonarqube-scan-action@v8.2.1': - ref: 'v8.2.1' - commit: 'sha1-22918119ff8e1ca75a623e15c8296b6ea4fbe28f' + 'sonarsource/sonarqube-scan-action@v8.2.2': + ref: 'v8.2.2' + commit: 'sha1-ba9859eae8dd6bd29e412f25ddbbef3d032000f4' owner_id: 545988 repo_id: 366408409 diff --git a/.github/workflows/scorecard-enforcer.yml b/.github/workflows/scorecard-enforcer.yml deleted file mode 100644 index 02c5c5b..0000000 --- a/.github/workflows/scorecard-enforcer.yml +++ /dev/null @@ -1,71 +0,0 @@ -# This workflow is managed by gh actions-lock. -# SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. -# Prevention workflow - runs OpenSSF Scorecard and fails on low scores -name: OpenSSF Scorecard Enforcer -on: - push: - branches: [main] - schedule: - - cron: '0 6 * * 1' # Weekly on Monday - workflow_dispatch: -jobs: - scorecard: - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - id-token: write # For OIDC - steps: - - uses: actions/checkout@v7.0.1 - with: - persist-credentials: false - - name: Run Scorecard - uses: ossf/scorecard-action@v2.4.4 - with: - results_file: results.sarif - results_format: sarif - publish_results: true - - name: Upload SARIF - uses: github/codeql-action/upload-sarif@main - with: - sarif_file: results.sarif - - name: Check minimum score - run: | - # Parse score from results - SCORE=$(jq -r '.runs[0].tool.driver.properties.score // 0' results.sarif 2>/dev/null || echo "0") - - echo "OpenSSF Scorecard Score: $SCORE" - - # Minimum acceptable score (0-10 scale) - MIN_SCORE=5 - - if [ "$(echo "$SCORE < $MIN_SCORE" | bc -l)" = "1" ]; then - echo "::error::Scorecard score $SCORE is below minimum $MIN_SCORE" - exit 1 - fi - timeout-minutes: 15 - # Check specific high-priority items - check-critical: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v7.0.1 - - name: Check SECURITY.md exists - run: | - if [ ! -f "SECURITY.md" ]; then - echo "::error::SECURITY.md is required" - exit 1 - fi - - name: Check for pinned dependencies - run: | - # Check workflows for unpinned actions - unpinned=$(grep -r "uses:.*@v[0-9]" .github/workflows/*.yml 2>/dev/null | grep -v "#" | head -5 || true) - if [ -n "$unpinned" ]; then - echo "::warning::Found unpinned actions:" - echo "$unpinned" - fi - timeout-minutes: 15 -permissions: - contents: read diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 92c87a8..c892124 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -19,6 +19,7 @@ jobs: # without security-events: write here the scorecard SARIF upload fails with # startup_failure (hypatia WF018). id-token: write enables OIDC publish. permissions: + actions: read contents: read security-events: write id-token: write diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index 98c9abc..86bed3c 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -1,6 +1,5 @@ # This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. # workflow-linter.yml - Validates GitHub workflows against RSR security standards # This workflow can be copied to other repos for consistent enforcement name: Workflow Security Linter @@ -22,7 +21,7 @@ permissions: jobs: lint-workflows: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read @@ -37,13 +36,15 @@ jobs: failed=0 for file in .github/workflows/*.yml .github/workflows/*.yaml; do [ -f "$file" ] || continue - if ! head -1 "$file" | grep -q "^# SPDX-License-Identifier:"; then + # actions-lock may prepend its own comment. Require the licence + # in the leading comment block, before the workflow body. + if ! awk '/^# SPDX-License-Identifier:/ { found=1 } /^[^#[:space:]]/ { exit } END { exit !found }' "$file"; then echo "ERROR: $file missing SPDX header" failed=1 fi done if [ $failed -eq 1 ]; then - echo "Add '# SPDX-License-Identifier: MPL-2.0' as first line" + echo "Add '# SPDX-License-Identifier: MPL-2.0' to the leading comment block" exit 1 fi echo "All workflows have SPDX headers" @@ -65,18 +66,34 @@ jobs: fi echo "All workflows have permissions declared" + # Bun is the estate's first-choice runtime (LANGUAGE-POLICY.adoc §1: + # Bun > Deno > pnpm > npm) and executes .ts directly (§1.2). Tag form + # matches every other ref in this repo and is resolved by actions.lock. + - name: Set up Bun + uses: oven-sh/setup-bun@v2.2.0 + with: + bun-version: latest + - name: Check SHA-Pinned Actions run: | echo "=== Checking Action Pinning ===" # Find any uses: lines that don't have @SHA format # Pattern: uses: owner/repo@<40-char-hex> - unpinned=$(grep -rnE "^[[:space:]]+uses:" .github/workflows/ | \ - grep -v "@[a-f0-9]\{40\}" | \ - grep -v "uses: \./\|uses: docker://\|uses: actions/github-script" || true) + # Delegated to scripts/check-action-pinning.js. The rule it enforces is + # unchanged in spirit — every action ref must resolve to an immutable + # commit — but "pinned" now includes refs the workflow lockfile + # resolves, which is how this repo pins them. Kept as a script rather + # than inline because the inline form needs a heredoc inside a YAML + # block scalar, and that is a well-known way to ship a gate that + # silently does nothing. + if ! bun scripts/check-action-pinning.js; then + unpinned="see above" + else + unpinned="" + fi if [ -n "$unpinned" ]; then echo "ERROR: Found unpinned actions:" - echo "$unpinned" echo "" echo "Replace version tags with SHA pins, e.g.:" echo " uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6.0.1" @@ -116,7 +133,6 @@ jobs: has_rb=$(find . -name "*.rb" -path "*/lib/*" -o -path "*/app/*" 2>/dev/null | head -1) echo "Detected languages:" - [ -n "$has_js" ] && echo " - javascript-typescript" [ -n "$has_py" ] && echo " - python" [ -n "$has_go" ] && echo " - go" [ -n "$has_rs" ] && echo " - rust (note: CodeQL rust is limited)" diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 4cee253..0000000 --- a/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1776169885, - "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index bcbaa23..0000000 --- a/flake.nix +++ /dev/null @@ -1,76 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -# -# RSR-template-repo - Nix Flake (fallback for Guix) -# Primary: guix.scm | Fallback: flake.nix -# -# Usage: -# nix develop # Enter dev shell -# nix build # Build package -# nix flake check # Validate flake -{ - description = "RSR Template Repository - Canonical template for RSR projects"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in - { - packages.default = pkgs.stdenv.mkDerivation { - pname = "rsr-template-repo"; - version = "0.1.0"; - - src = ./.; - - meta = with pkgs.lib; { - description = "Canonical template for RSR (Rhodium Standard Repository) projects"; - homepage = "https://github.com/hyperpolymath/RSR-template-repo"; - license = licenses.agpl3Plus; - maintainers = [ ]; - platforms = platforms.all; - }; - }; - - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - # Core tools - just - git - - # Guile/Scheme (for SCM files) - guile - - # Documentation - asciidoctor - - # Security tools - gitleaks - trivy - - # Container tools - nerdctl - ]; - - shellHook = '' - echo "RSR Template Repository - Development Shell (Nix)" - echo "Primary package manager: Guix (guix.scm)" - echo "Fallback: Nix (flake.nix)" - echo "" - echo "Available commands:" - echo " just - Show all recipes" - echo " just validate - Run RSR validation" - echo " just info - Show project info" - ''; - }; - - # Expose checks - checks.default = self.packages.${system}.default; - } - ); -} diff --git a/scripts/check-action-pinning.js b/scripts/check-action-pinning.js new file mode 100644 index 0000000..80533f6 --- /dev/null +++ b/scripts/check-action-pinning.js @@ -0,0 +1,105 @@ +#!/usr/bin/env bun +// SPDX-License-Identifier: MPL-2.0 +// +// Assert every workflow action ref is pinned — inline SHA, or via actions.lock. +// +// What counts as "pinned" changed when GitHub introduced workflow lockfiles. A +// ref is pinned if EITHER it is an inline 40-hex SHA, OR +// .github/workflows/actions.lock resolves it to one. The previous inline-only +// rule rejected all 40 of this repo's own refs, so `Workflow Security Linter` +// was red on main permanently — and every repo minted from this template +// inherited both the tag-form workflows and the gate that rejects them. +// +// Runtime: Bun — the estate's first-choice runtime per LANGUAGE-POLICY.adoc §1 +// (Bun > Deno > pnpm > npm). Plain JavaScript, not TypeScript: TypeScript is +// banned estate-wide (owner ruling; CLAUDE.md banned-languages table). Note +// LANGUAGE-POLICY.adoc §1.2 currently claims TS is "permitted under Bun" — that +// line is wrong and should be corrected; the ban stands. +// +// Python was the first draft of this script and is banned with no exceptions. +// +// Three details are load-bearing, each of them a defect found by testing: +// +// `-?` in the pattern. `- uses:` is the list-item form and by far the most +// common; a `\s+uses:` pattern silently misses every one of them. Estate +// memory records this exact failure — "green linter != full SHA-pinning". A +// gate that cannot see the common case reads as coverage and enforces nothing. +// +// Case-insensitive comparison. Workflows write `SonarSource/...`; the lockfile +// records `sonarsource/...`. Action refs are case-insensitive in practice, so +// a case-sensitive match reports a false positive on a correctly-pinned action. +// +// Only *.yml / *.yaml, depth 1. A bare recursive grep also reads actions.lock +// (whose `uses:` keys are lockfile entries, not refs) and *.yml.template +// (whose tag ref is deliberate and resolved at mint time). Both were reported +// as unpinned, which made the gate unsatisfiable the moment a lockfile existed. +// +// Exit 0 if every ref is pinned, 1 otherwise, listing file:line: ref. + +import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; +import { join } from "node:path"; + +const WF = ".github/workflows"; +const LOCK = join(WF, "actions.lock"); +// `-?` matches the list-item form; see the header. +const USES = /^\s*-?\s*uses:\s*([^\s#]+)/; +const SHA = /@[a-f0-9]{40}$/; +const EXEMPT_PREFIX = ["./", "$", "docker://"]; + +function workflowFiles() { + if (!existsSync(WF) || !statSync(WF).isDirectory()) return []; + return readdirSync(WF) + .filter((f) => f.endsWith(".yml") || f.endsWith(".yaml")) + .sort() + .map((f) => join(WF, f)); +} + +function main() { + if (!existsSync(WF)) { + console.log("no .github/workflows — nothing to check"); + return 0; + } + + let known = new Set(); + if (existsSync(LOCK)) { + const lock = readFileSync(LOCK, "utf8"); + known = new Set( + [...lock.matchAll(/'([^']+@[^']+)'/g)].map((m) => m[1].toLowerCase()), + ); + console.log(`lockfile present: ${known.size} ref(s) resolvable through it`); + } else { + console.log("no lockfile — every ref must be an inline 40-character SHA"); + } + + const bad = []; + for (const wf of workflowFiles()) { + const lines = readFileSync(wf, "utf8").split("\n"); + lines.forEach((line, i) => { + const m = USES.exec(line); + if (!m) return; + const ref = m[1]; + if (EXEMPT_PREFIX.some((p) => ref.startsWith(p))) return; + if (ref.includes("actions/github-script")) return; + if (SHA.test(ref)) return; + const at = ref.lastIndexOf("@"); + const name = at === -1 ? ref : ref.slice(0, at); + const tag = at === -1 ? "" : ref.slice(at + 1); + // subpath actions key by repo root: github/codeql-action/init -> github/codeql-action + const root = name.split("/").slice(0, 2).join("/"); + if (known.has(ref.toLowerCase()) || known.has(`${root}@${tag}`.toLowerCase())) return; + bad.push(`${wf}:${i + 1}: ${ref}`); + }); + } + + if (bad.length) { + console.log("\nERROR: these action refs are neither SHA-pinned nor covered by the lockfile:"); + for (const b of bad) console.log(` ${b}`); + console.log("\nEither pin to a full commit SHA, or run `gh actions-lock` so the"); + console.log("lockfile resolves the ref."); + return 1; + } + console.log("all action refs are pinned"); + return 0; +} + +process.exit(main());