From 9202c8652bbadb0a43f92872defc40850782b697 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 23 Apr 2026 18:55:50 +0200 Subject: [PATCH] Harden Scorecard signals around release signing and PR checks Add pull_request coverage for CI and CodeQL, exact dependency pins, npm Dependabot coverage, and signed GitHub release assets via Sigstore bundles. Also fix the security advisory link and widen CODEOWNERS so stricter review settings have matching repo metadata. Constraint: Signed-release credit only improves on future or re-run releases because existing tags have no uploaded assets Rejected: Require two reviewers immediately | likely blocks this mostly single-maintainer repo without proving sustainable reviewer capacity Confidence: medium Scope-risk: moderate Directive: If branch protection is tightened after merge, keep required check names aligned with workflow job names before renaming them Tested: node --test test/metadata.test.js Tested: timeout 180 npm test Tested: openspec validate agent-codex-harden-scorecard-best-practices-2026-04-23-18-42 --type change --strict Tested: openspec validate --specs Not-tested: Live GitHub Actions release run producing .sigstore.json assets Not-tested: Live Scorecard rerun after the next signed release and branch-protection update --- .github/CODEOWNERS | 3 +- .github/dependabot.yml | 5 ++ .github/workflows/ci.yml | 3 ++ .github/workflows/codeql.yml | 3 ++ .github/workflows/release.yml | 37 +++++++++++++- SECURITY.md | 2 +- .../.openspec.yaml | 2 + .../proposal.md | 19 +++++++ .../harden-scorecard-best-practices/spec.md | 45 +++++++++++++++++ .../tasks.md | 34 +++++++++++++ package-lock.json | 6 +-- package.json | 6 +-- test/metadata.test.js | 50 +++++++++++++++++++ 13 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/.openspec.yaml create mode 100644 openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/proposal.md create mode 100644 openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/specs/harden-scorecard-best-practices/spec.md create mode 100644 openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/tasks.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b133f07f..7f29fc21 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,2 @@ -* @recodeecom +# Default code owners for repository-wide reviews. +* @recodeecom @NagyVikt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ca79ca5b..15e6d5d9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,10 @@ version: 2 updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + versioning-strategy: lockfile-only - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ed02fe2..bd1b16ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 913bc039..078b1adc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main schedule: - cron: '35 3 * * 1' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc8b39b0..eab0c251 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release to npm (provenance) +name: Release to npm (provenance + signed assets) on: workflow_dispatch: @@ -14,6 +14,9 @@ jobs: if: github.repository == 'recodeee/gitguardex' runs-on: ubuntu-latest environment: npm + permissions: + contents: write + id-token: write steps: - name: Checkout @@ -28,6 +31,9 @@ jobs: registry-url: https://registry.npmjs.org cache: npm + - name: Install Cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + - name: Install run: npm ci --ignore-scripts @@ -55,6 +61,14 @@ jobs: echo "already_published=false" >> "$GITHUB_OUTPUT" fi + - name: Pack release tarball + id: pack + run: | + mkdir -p dist + package_file="$(npm pack --pack-destination dist | tail -n 1)" + echo "package_file=${package_file}" >> "$GITHUB_OUTPUT" + echo "package_path=dist/${package_file}" >> "$GITHUB_OUTPUT" + - name: Publish with provenance if: ${{ steps.registry.outputs.already_published != 'true' }} run: npm publish --provenance --access public @@ -65,3 +79,24 @@ jobs: PACKAGE_NAME: ${{ steps.pkg.outputs.name }} PACKAGE_VERSION: ${{ steps.pkg.outputs.version }} run: echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already on npm; skipping publish." + + - name: Generate release checksum + run: | + sha256sum "${{ steps.pack.outputs.package_path }}" > "${{ steps.pack.outputs.package_path }}.sha256" + + - name: Sign release tarball with Sigstore + run: | + cosign sign-blob "${{ steps.pack.outputs.package_path }}" \ + --bundle "${{ steps.pack.outputs.package_path }}.sigstore.json" \ + --yes + + - name: Upload signed release assets + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name || format('v{0}', steps.pkg.outputs.version) }} + run: | + gh release upload "$RELEASE_TAG" \ + "${{ steps.pack.outputs.package_path }}" \ + "${{ steps.pack.outputs.package_path }}.sha256" \ + "${{ steps.pack.outputs.package_path }}.sigstore.json" \ + --clobber diff --git a/SECURITY.md b/SECURITY.md index 99d80c3a..910fd2f8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,7 @@ Only the latest published GitGuardex CLI build is supported for security fixes. Please report security issues privately by opening a GitHub security advisory: -- https://github.com/recodeecom/multiagent-safety/security/advisories/new +- https://github.com/recodeee/gitguardex/security/advisories/new If advisories are unavailable, open a private report via GitHub issue contact details and avoid posting exploit details publicly. diff --git a/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/.openspec.yaml b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/.openspec.yaml new file mode 100644 index 00000000..8b394c66 --- /dev/null +++ b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-23 diff --git a/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/proposal.md b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/proposal.md new file mode 100644 index 00000000..b14a4516 --- /dev/null +++ b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/proposal.md @@ -0,0 +1,19 @@ +## Why + +- GitGuardex already ships several Scorecard-friendly guardrails, but key supply-chain signals still lag behind the repository's actual intent. +- The release workflow publishes to npm with provenance but leaves GitHub releases without signed assets, so Scorecard cannot award signed-release credit. +- CI and CodeQL do not run on pull requests to `main`, which weakens branch-protection hardening because there are no stable pre-merge checks to require. +- Package metadata and security/reporting metadata still contain avoidable drift such as range-based dependency specifiers and a repo link that points elsewhere. + +## What Changes + +- Run CI and CodeQL for pull requests targeting `main` so required status checks have real pre-merge coverage. +- Extend the release workflow to build the npm tarball, checksum it, sign it with a Sigstore bundle, and upload those artifacts to the matching GitHub release. +- Expand Dependabot to cover npm packages and pin package dependency specifiers exactly in both `package.json` and `package-lock.json`. +- Correct security-reporting and code-owner metadata needed for stricter GitHub review settings. + +## Impact + +- Affects GitHub Actions workflows, dependency metadata, and security/review docs only; runtime CLI behavior stays unchanged. +- Signed-release score gains apply to future or re-run releases; historical releases without assets remain unchanged until republished or manually backfilled. +- Branch-protection, maintained, contributors, and code-review scores still depend partly on live GitHub settings, repo age, and human review history outside this diff. diff --git a/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/specs/harden-scorecard-best-practices/spec.md b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/specs/harden-scorecard-best-practices/spec.md new file mode 100644 index 00000000..5e103908 --- /dev/null +++ b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/specs/harden-scorecard-best-practices/spec.md @@ -0,0 +1,45 @@ +## ADDED Requirements + +### Requirement: Pull request checks for protected main +The repository SHALL run CI and CodeQL workflows on pull requests targeting `main` so branch-protection rules can require real pre-merge status checks. + +#### Scenario: CI runs on pull requests +- **WHEN** a pull request targets `main` +- **THEN** `.github/workflows/ci.yml` triggers for that pull request +- **AND** the workflow remains enabled for direct pushes to `main`. + +#### Scenario: CodeQL runs on pull requests +- **WHEN** a pull request targets `main` +- **THEN** `.github/workflows/codeql.yml` triggers for that pull request +- **AND** the scheduled scan remains enabled. + +### Requirement: Signed GitHub release assets +The release workflow SHALL publish signed GitHub release assets for the package tarball in addition to npm provenance. + +#### Scenario: Release uploads signed artifacts +- **WHEN** `.github/workflows/release.yml` runs for a published release +- **THEN** it builds the npm tarball, generates a SHA256 checksum, creates a Sigstore bundle for the tarball, and uploads those files to the matching GitHub release +- **AND** the workflow continues to publish to npm with provenance when the version is not already published. + +### Requirement: Pinned dependency and update metadata +The repository SHALL keep supply-chain metadata aligned with stricter Scorecard expectations. + +#### Scenario: Package specs stay exact +- **WHEN** runtime or dev dependencies are declared in `package.json` +- **THEN** their versions are pinned exactly +- **AND** `package-lock.json` reflects those exact specifiers. + +#### Scenario: Automated update coverage includes npm +- **WHEN** Dependabot configuration is evaluated +- **THEN** it schedules updates for both npm dependencies and GitHub Actions. + +### Requirement: Security and ownership metadata points at this repository +Repository security and ownership metadata SHALL reference the live GitGuardex repository surfaces. + +#### Scenario: Security reporting points at this repo +- **WHEN** maintainers or users read `SECURITY.md` +- **THEN** the private advisory link targets `recodeee/gitguardex`. + +#### Scenario: Code owners cover default review paths +- **WHEN** repository-wide ownership is evaluated +- **THEN** `.github/CODEOWNERS` defines default owners for all files. diff --git a/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/tasks.md b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/tasks.md new file mode 100644 index 00000000..0d270cdf --- /dev/null +++ b/openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/tasks.md @@ -0,0 +1,34 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## Handoff + +- Handoff: change=`agent-codex-harden-scorecard-best-practices-2026-04-23-18-42`; branch=`agent/codex/harden-scorecard-best-practices-2026-04-23-18-42`; scope=`Scorecard-facing workflows, dependency metadata, and security/review docs`; action=`finish repo-side hardening, then apply and verify reachable GitHub settings`. +- Copy prompt: Continue `agent-codex-harden-scorecard-best-practices-2026-04-23-18-42` on branch `agent/codex/harden-scorecard-best-practices-2026-04-23-18-42`. Work inside the existing sandbox, review `openspec/changes/agent-codex-harden-scorecard-best-practices-2026-04-23-18-42/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/harden-scorecard-best-practices-2026-04-23-18-42 --base main --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-harden-scorecard-best-practices-2026-04-23-18-42`. +- [x] 1.2 Define normative requirements in `specs/harden-scorecard-best-practices/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. +- [x] 3.2 Run `openspec validate agent-codex-harden-scorecard-best-practices-2026-04-23-18-42 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/harden-scorecard-best-practices-2026-04-23-18-42 --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch). diff --git a/package-lock.json b/package-lock.json index 3ab1c303..ded780eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "7.0.26", "license": "MIT", "dependencies": { - "jsonc-parser": "^3.3.1", - "semver": "^7.7.4" + "jsonc-parser": "3.3.1", + "semver": "7.7.4" }, "bin": { "gitguardex": "bin/multiagent-safety.js", @@ -19,7 +19,7 @@ "multiagent-safety": "bin/multiagent-safety.js" }, "devDependencies": { - "fast-check": "^3.23.2" + "fast-check": "3.23.2" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index fdfb4987..0af7dd12 100644 --- a/package.json +++ b/package.json @@ -68,10 +68,10 @@ "access": "public" }, "devDependencies": { - "fast-check": "^3.23.2" + "fast-check": "3.23.2" }, "dependencies": { - "jsonc-parser": "^3.3.1", - "semver": "^7.7.4" + "jsonc-parser": "3.3.1", + "semver": "7.7.4" } } diff --git a/test/metadata.test.js b/test/metadata.test.js index 7908da1f..15dfc123 100644 --- a/test/metadata.test.js +++ b/test/metadata.test.js @@ -32,6 +32,7 @@ test('release workflow publishes with provenance in CI', () => { const workflow = fs.readFileSync(workflowPath, 'utf8'); assert.match(workflow, /name:\s+Checkout\s+uses:\s+actions\/checkout@[0-9a-f]{40}[^\n]*\n\s+with:\s*\n\s+fetch-depth:\s+0/s); assert.match(workflow, /npm publish --provenance --access public/); + assert.match(workflow, /name:\s+Install Cosign\s+uses:\s+sigstore\/cosign-installer@[0-9a-f]{40}[^\n]*# v4\.1\.1/s); }); test('release workflow skips publish when the current version is already on npm', () => { @@ -45,6 +46,19 @@ test('release workflow skips publish when the current version is already on npm' assert.match(workflow, /skipping publish\./); }); +test('release workflow uploads signed GitHub release assets for the package tarball', () => { + const workflowPath = path.join(repoRoot, '.github', 'workflows', 'release.yml'); + const workflow = fs.readFileSync(workflowPath, 'utf8'); + assert.match(workflow, /name:\s+Pack release tarball/); + assert.match(workflow, /npm pack --pack-destination dist/); + assert.match(workflow, /sha256sum "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}" > "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}\.sha256"/); + assert.match(workflow, /cosign sign-blob "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}"/); + assert.match(workflow, /--bundle "\$\{\{\s*steps\.pack\.outputs\.package_path\s*\}\}\.sigstore\.json"/); + assert.match(workflow, /name:\s+Upload signed release assets/); + assert.match(workflow, /GH_TOKEN:\s+\$\{\{\s*github\.token\s*\}\}/); + assert.match(workflow, /gh release upload "\$RELEASE_TAG"/); +}); + test('release workflow only publishes from published releases or manual dispatch', () => { const workflowPath = path.join(repoRoot, '.github', 'workflows', 'release.yml'); const workflow = fs.readFileSync(workflowPath, 'utf8'); @@ -129,6 +143,13 @@ test('security workflows are present and use pinned GitHub Actions SHAs', () => } }); +test('CI and CodeQL workflows run on pull requests targeting main', () => { + const ciWorkflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'ci.yml'), 'utf8'); + const codeqlWorkflow = fs.readFileSync(path.join(repoRoot, '.github', 'workflows', 'codeql.yml'), 'utf8'); + assert.match(ciWorkflow, /pull_request:\s*\n\s*branches:\s*\n\s*-\s*main/s); + assert.match(codeqlWorkflow, /pull_request:\s*\n\s*branches:\s*\n\s*-\s*main/s); +}); + test('code review workflow does not gate startup on secrets context', () => { const workflowPath = path.join(repoRoot, '.github', 'workflows', 'cr.yml'); const workflow = fs.readFileSync(workflowPath, 'utf8'); @@ -137,6 +158,35 @@ test('code review workflow does not gate startup on secrets context', () => { assert.match(workflow, /if:\s+\$\{\{\s*env\.OPENAI_API_KEY != ''\s*\}\}/); }); +test('security metadata points at the live repo and dependabot covers npm plus actions', () => { + const securityPolicy = fs.readFileSync(path.join(repoRoot, 'SECURITY.md'), 'utf8'); + const dependabot = fs.readFileSync(path.join(repoRoot, '.github', 'dependabot.yml'), 'utf8'); + const codeowners = fs.readFileSync(path.join(repoRoot, '.github', 'CODEOWNERS'), 'utf8'); + + assert.match(securityPolicy, /https:\/\/github\.com\/recodeee\/gitguardex\/security\/advisories\/new/); + assert.match(dependabot, /package-ecosystem:\s+npm/); + assert.match(dependabot, /package-ecosystem:\s+github-actions/); + assert.match(dependabot, /versioning-strategy:\s+lockfile-only/); + assert.match(codeowners, /^# Default code owners/m); + assert.match(codeowners, /^\*\s+@recodeecom\s+@NagyVikt$/m); +}); + +test('package manifest pins runtime and dev dependency versions exactly', () => { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const lockfile = fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8'); + + for (const sectionName of ['dependencies', 'devDependencies']) { + const section = pkg[sectionName] || {}; + for (const [name, version] of Object.entries(section)) { + assert.doesNotMatch(version, /^[~^]/, `${sectionName}.${name} must stay pinned exactly`); + } + } + + assert.match(lockfile, /"jsonc-parser": "3\.3\.1"/); + assert.match(lockfile, /"semver": "7\.7\.4"/); + assert.match(lockfile, /"fast-check": "3\.23\.2"/); +}); + test('frontend mirror workflow skips cleanly when the mirror PAT is missing', () => { const workflowPath = path.join(repoRoot, '.github', 'workflows', 'sync-frontend-mirror.yml'); const workflow = fs.readFileSync(workflowPath, 'utf8');