diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b133f07..7f29fc2 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 ca79ca5..15e6d5d 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 3ed02fe..bd1b16a 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 913bc03..078b1ad 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 dc8b39b..eab0c25 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 99d80c3..910fd2f 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 0000000..8b394c6 --- /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 0000000..b14a451 --- /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 0000000..5e10390 --- /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 0000000..0d270cd --- /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 3ab1c30..ded780e 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 fdfb498..0af7dd1 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 7908da1..15dfc12 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');