From 0101d2fd884ff011d57bae6d523821d70aed06a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 16:03:28 +0900 Subject: [PATCH 1/3] ci(security): deduplicate central PR scans Keep Bandit, CodeQL, Scorecard, and Trivy as bounded trusted-branch backstops while central required workflows own their PR evidence. Preserve repository-native dependency and secret checks where central coverage is incomplete. Signed-off-by: Seongho Bae Commit-Message-Assisted-by: Codex (OpenAI) --- .github/workflows/bandit.yml | 12 ++++--- .github/workflows/codeql.yml | 4 +++ .github/workflows/ossf-scorecard.yml | 4 +++ .github/workflows/secret-scan-gate.yml | 6 ++++ .github/workflows/security-audit.yml | 6 ++++ .github/workflows/trivy.yml | 4 +++ CHANGELOG.md | 3 +- docs/architecture/overview.md | 2 +- docs/repository/bootstrap-plan.md | 5 ++- docs/security/code-security.md | 19 +++++++++--- docs/security/github-required-checks.md | 31 ++++++++++++++++--- .../github-bootstrap-execution-policy.md | 4 +-- scripts/checks/verify_supply_chain.py | 18 +++++++++-- .../tests/test_supply_chain_policy.py | 28 ++++++++++++++++- 14 files changed, 125 insertions(+), 21 deletions(-) diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index 6db7276da..7aaf62f54 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -5,10 +5,14 @@ on: branches: - develop - main - pull_request: - branches: - - develop - - main + workflow_dispatch: + +# Pull-request SAST is owned by the organization-required SAST Semgrep +# workflow. Keep Bandit as a trusted-branch/manual backstop without duplicating +# PR checkout, dependency installation, and Python scanning. +concurrency: + group: bandit-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 27c5b540f..b8a2aa4ba 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,6 +7,10 @@ on: - main workflow_dispatch: +concurrency: + group: codeql-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + permissions: actions: read contents: read diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 2a4b6eaa9..162ddc0a2 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -9,6 +9,10 @@ on: - develop - main +concurrency: + group: ossf-scorecard-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + permissions: read-all jobs: diff --git a/.github/workflows/secret-scan-gate.yml b/.github/workflows/secret-scan-gate.yml index 88f72b419..3ff6873e9 100644 --- a/.github/workflows/secret-scan-gate.yml +++ b/.github/workflows/secret-scan-gate.yml @@ -10,6 +10,12 @@ on: - develop - main +concurrency: + group: >- + secret-scan-gate-${{ github.repository }}-${{ + github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index f6737f1f6..da93100b0 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -10,6 +10,12 @@ on: - develop - main +concurrency: + group: >- + security-audit-${{ github.repository }}-${{ + github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index d79ec32e1..dee8e9b20 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -6,6 +6,10 @@ on: - develop - main +concurrency: + group: trivy-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6f7e784..006049730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Changed +- Moved pull-request Bandit execution to the organization-required SAST owner while retaining Bandit, CodeQL, Scorecard, and Trivy as bounded trusted-branch backstops; repository-native dependency audits and the supplemental secret gate remain PR checks because central workflows do not fully replace them. - Pinned npm `10.9.9` as the approved lockfile generator, activated it through Node-bundled Corepack before dependency consumption, and fail closed unless its bundled `tar` is at least `7.5.19`; primary CI still consumes the committed lock only through frozen `npm ci` validation, rejects mutable npm resolution in the lock gate, requires integrity evidence for public-registry lock entries, and preserves generator-sensitive root `@esbuild/*` peer metadata. ### Fixed @@ -74,4 +75,4 @@ - `ChordsFeature` (코드 분석) 화면에서 각 파트(Role)의 `transpositionPlan`(이조/조옮김 계획)을 표시하는 기능을 추가했습니다. - `RangesFeature` (음역대 분석) 화면에서 겹침 경고(Overlap warning) 외에 해당 파트의 채보(Transcription) 가능 노드 수를 요약하여 보여주는 기능을 추가했습니다. -- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`). \ No newline at end of file +- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`). diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 3cf5261b9..303dd3570 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -41,6 +41,6 @@ GitHub is the source of truth for repository governance, PR review, CI/CD, Code ## CI/CD and release flow -- PRs into `develop` and `main` run CI, dependency review, security audit, secret-scan gate, SBOM generation, and CodeQL +- PRs into `develop` and `main` run repository CI, security audit, secret-scan and SBOM checks alongside organization-required OSV, dependency-review, Trivy, CodeQL/code-quality, Semgrep SAST, Strix, and Noema evidence - release flows publish desktop artifacts plus SBOM evidence to GitHub Releases through a tag-driven draft-before-publish path - branch protection connects stable required checks after bootstrap workflows exist diff --git a/docs/repository/bootstrap-plan.md b/docs/repository/bootstrap-plan.md index b16f458a1..72ee651f5 100644 --- a/docs/repository/bootstrap-plan.md +++ b/docs/repository/bootstrap-plan.md @@ -32,11 +32,14 @@ After workflows exist, require these stable checks on `main` and `develop`: - `ci / build-and-test` - `dependency-review` - `security-audit` -- `CodeQL` - `sbom` - `release-preflight` - `gate / build / windows` - `gate / build / macos` +- `trivy-fs` +- `Analyze (javascript-typescript)` +- `Analyze (python)` +- organization-required Security Scan, CodeQL/code-quality, SAST Semgrep, Strix, Noema, OpenCode, scheduler, and empty-PR workflows ## Initial README exception diff --git a/docs/security/code-security.md b/docs/security/code-security.md index f9163b9c4..93fd24096 100644 --- a/docs/security/code-security.md +++ b/docs/security/code-security.md @@ -6,12 +6,21 @@ BandScope treats GitHub Code Security as part of bootstrap governance. ## Required controls -- CodeQL or equivalent code scanning workflow -- Trivy filesystem vulnerability scan -- dependency review on pull requests -- security audit workflow for npm, Python, and Rust dependencies in scope +- organization-required CodeQL/code-quality evidence and multi-language SAST on pull requests +- organization-required Trivy filesystem and OSV vulnerability scans +- organization-required dependency review on pull requests +- repository security audit workflow for npm, Python, and Rust dependencies in scope - Dependabot alerts and security updates -- secret scanning in GitHub plus a supplemental secret-scan gate workflow +- secret scanning in GitHub plus the repository supplemental secret-scan gate workflow + +The central Security Scan owns PR OSV, dependency-review, Trivy, and soft +Scorecard evidence. Central Gitleaks currently runs only for +`ContextualWisdomLab/.github`, so BandScope keeps `secret-scan-gate` on pull +requests. The repository `security-audit` also remains a pull-request check: its +native npm, pip, and Cargo audits and documented exception handling are not +fully replaced by the central scanners. Bandit, CodeQL, Scorecard, and Trivy +remain push/schedule/manual backstops only; central SAST and CodeQL evidence own +their pull-request paths. ## Enforcement diff --git a/docs/security/github-required-checks.md b/docs/security/github-required-checks.md index eb62fdae3..1dbf554d9 100644 --- a/docs/security/github-required-checks.md +++ b/docs/security/github-required-checks.md @@ -9,12 +9,19 @@ These are the merge-gate status checks that should be required on protected bran - `ci / build-and-test` - `dependency-review` - `security-audit` -- `CodeQL` -- `trivy-fs-scan` - `sbom` - `release-preflight` - `gate / build / windows` - `gate / build / macos` +- `trivy-fs` +- `coverage-evidence` +- `opencode-review` +- `strix` +- `scan-pr-queue` +- `osv-scan` +- `scorecard` +- `Analyze (javascript-typescript)` +- `Analyze (python)` `gate / build / windows` must cover both Windows `amd64` and Windows `arm64`. `gate / build / macos` must cover both macOS Intel (`amd64`) and macOS `arm64`. @@ -24,12 +31,28 @@ These are the merge-gate status checks that should be required on protected bran - `ci / build-and-test` - `dependency-review` - `security-audit` -- `CodeQL` -- `trivy-fs-scan` - `sbom` - `release-preflight` - `gate / build / windows` - `gate / build / macos` +- `trivy-fs` +- `Analyze (javascript-typescript)` +- `Analyze (python)` + +The organization required-workflow rule is the authoritative PR owner for +`osv-scan`, `dependency-review`, `trivy-fs`, Scorecard visibility, Semgrep SAST, +Strix, and Noema. Repository-local Bandit, CodeQL, Scorecard, and Trivy run only +after trusted-branch pushes, on schedules where applicable, or manually. The +repository `security-audit` and `secret-scan-gate` remain PR-scoped because +their coverage has not been fully inherited by a central workflow. + +The lists above reflect the live classic required-status contexts verified on +2026-09-04. The active organization ruleset separately requires the central +`close-empty-pr.yml`, `opencode-review.yml`, `pr-review-merge-scheduler.yml`, +`security-scan.yml`, `strix.yml`, `sast-semgrep.yml`, and `noema-review.yml` +workflows on the default branch. Keep these two enforcement mechanisms distinct +when changing local triggers: a context must not be removed while classic +protection still requires it. ## GitHub settings baseline diff --git a/docs/workflow/github-bootstrap-execution-policy.md b/docs/workflow/github-bootstrap-execution-policy.md index 736b695aa..cc9958aa4 100644 --- a/docs/workflow/github-bootstrap-execution-policy.md +++ b/docs/workflow/github-bootstrap-execution-policy.md @@ -38,12 +38,12 @@ The expected sequence is: Bootstrap or setup work is not complete unless GitHub-facing supply-chain controls are both committed and, where permissions allow, enforced: - `.github/dependabot.yml` -- `.github/workflows/dependency-review.yml` - `.github/workflows/security-audit.yml` - `.github/workflows/codeql.yml` +- `.github/workflows/bandit.yml` - `.github/workflows/sbom.yml` - `.github/workflows/release.yml` -- branch protection or rulesets for `main` and `develop` that require `ci / build-and-test`, `dependency-review`, `security-audit`, `CodeQL`, `sbom`, `release-preflight`, `gate / build / windows`, and `gate / build / macos` +- branch protection or rulesets for `main` and `develop` that require repository CI, security audit, secret scan, SBOM, release preflight, platform builds, and the organization-required Security Scan, CodeQL/code-quality, SAST, Strix, and review workflows - PR workflow that still requests CodeRabbit review and records its result when the provider responds cleanly - release retention for the generated SBOM and supplemental inventory diff --git a/scripts/checks/verify_supply_chain.py b/scripts/checks/verify_supply_chain.py index 1cd561e5c..4d37443ab 100644 --- a/scripts/checks/verify_supply_chain.py +++ b/scripts/checks/verify_supply_chain.py @@ -18,8 +18,10 @@ Path("apps/desktop/src-tauri/Cargo.lock"), Path(".github/dependabot.yml"), # Dependency review runs via the org-level required workflow in - # ContextualWisdomLab/.github; repo-local CodeQL and Scorecard stay push-only - # so GitHub/Scorecard can still observe SAST and supply-chain security tabs. + # ContextualWisdomLab/.github; repo-local Bandit, CodeQL, Scorecard, and + # Trivy stay push/schedule-only as trusted-branch backstops while central + # required workflows own PR enforcement. + Path(".github/workflows/bandit.yml"), Path(".github/workflows/security-audit.yml"), Path(".github/workflows/codeql.yml"), Path(".github/workflows/sbom.yml"), @@ -1240,6 +1242,17 @@ def _verify_security_audit_coverage(missing: list[str]) -> None: missing.append(f"security audit workflow missing vulnerability audit token: {token}") +def _verify_bandit_coverage(missing: list[str]) -> None: + bandit = read_workflow(Path(".github/workflows/bandit.yml"), "bandit", missing) + for token in ["develop", "main", "push", "bandit"]: + if bandit and token not in bandit: + missing.append(f"bandit workflow missing token: {token}") + if bandit and "pull_request:" in bandit: + missing.append( + "bandit workflow must stay push/manual-only; central SAST owns PR scanning" + ) + + def _verify_codeql_coverage(missing: list[str]) -> None: codeql = read_workflow( Path(".github/workflows/codeql.yml"), "codeql", missing, optional=True @@ -1352,6 +1365,7 @@ def verify_workflow_coverage() -> list[str]: missing: list[str] = [] _verify_ci_coverage(missing) _verify_sbom_coverage(missing) + _verify_bandit_coverage(missing) _verify_security_audit_coverage(missing) _verify_codeql_coverage(missing) _verify_release_coverage(missing) diff --git a/services/analysis-engine/tests/test_supply_chain_policy.py b/services/analysis-engine/tests/test_supply_chain_policy.py index ab43df89f..647fe7215 100644 --- a/services/analysis-engine/tests/test_supply_chain_policy.py +++ b/services/analysis-engine/tests/test_supply_chain_policy.py @@ -1242,7 +1242,12 @@ def test_central_governance_workflows_are_push_only_where_local_signals_remain() assert not (workflows_dir / "dependency-review.yml").exists() - for local_signal in ("codeql.yml", "ossf-scorecard.yml", "trivy.yml"): + for local_signal in ( + "bandit.yml", + "codeql.yml", + "ossf-scorecard.yml", + "trivy.yml", + ): workflow = workflows_dir / local_signal assert workflow.exists(), ( f"{local_signal} keeps repository-local security-tab/SAST signal " @@ -1259,6 +1264,27 @@ def test_central_governance_workflows_are_push_only_where_local_signals_remain() assert ".github/workflows/ossf-scorecard.yml" in required +def test_local_security_workflows_cancel_superseded_runs_with_read_only_defaults() -> None: + """Keep retained local security signals bounded and least-privileged.""" + repo_root = Path(__file__).resolve().parents[3] + workflows_dir = repo_root / ".github" / "workflows" + + for workflow_name in ( + "bandit.yml", + "codeql.yml", + "ossf-scorecard.yml", + "secret-scan-gate.yml", + "security-audit.yml", + "trivy.yml", + ): + workflow = (workflows_dir / workflow_name).read_text(encoding="utf-8") + assert "concurrency:" in workflow, workflow_name + assert "cancel-in-progress: true" in workflow, workflow_name + assert "contents: read" in workflow or "permissions: read-all" in workflow, ( + workflow_name + ) + + def test_opencode_review_declares_top_level_token_permissions() -> None: """Ensure OpenCode token posture is delegated to the central required workflow.""" policy = central_required_workflow_policy_text() From 41bf9208a4cc8b843cc1762e8e875d79f0c4cb46 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 16:10:42 +0900 Subject: [PATCH 2/3] ci: isolate manual security backstops from push runs --- .github/workflows/bandit.yml | 2 +- .github/workflows/codeql.yml | 2 +- services/analysis-engine/tests/test_supply_chain_policy.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml index 7aaf62f54..4e44160cf 100644 --- a/.github/workflows/bandit.yml +++ b/.github/workflows/bandit.yml @@ -11,7 +11,7 @@ on: # workflow. Keep Bandit as a trusted-branch/manual backstop without duplicating # PR checkout, dependency installation, and Python scanning. concurrency: - group: bandit-${{ github.repository }}-${{ github.ref }} + group: bandit-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true permissions: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b8a2aa4ba..140397ff2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: concurrency: - group: codeql-${{ github.repository }}-${{ github.ref }} + group: codeql-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true permissions: diff --git a/services/analysis-engine/tests/test_supply_chain_policy.py b/services/analysis-engine/tests/test_supply_chain_policy.py index 647fe7215..1b232ec89 100644 --- a/services/analysis-engine/tests/test_supply_chain_policy.py +++ b/services/analysis-engine/tests/test_supply_chain_policy.py @@ -1283,6 +1283,8 @@ def test_local_security_workflows_cancel_superseded_runs_with_read_only_defaults assert "contents: read" in workflow or "permissions: read-all" in workflow, ( workflow_name ) + if workflow_name in {"bandit.yml", "codeql.yml", "ossf-scorecard.yml"}: + assert "github.event_name" in workflow, workflow_name def test_opencode_review_declares_top_level_token_permissions() -> None: From bf8571714bece1fef84dead79185901eac1bcd9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:47:40 +0900 Subject: [PATCH 3/3] ci: consolidate repository workflow backstops Signed-off-by: Seongho Bae --- .github/workflows/bandit.yml | 39 ------------- .github/workflows/build-baseline.yml | 22 +++++-- .github/workflows/ci.yml | 6 ++ .github/workflows/codeql.yml | 43 -------------- .github/workflows/ossf-scorecard.yml | 4 +- .github/workflows/release.yml | 8 +-- .github/workflows/sbom.yml | 6 ++ .github/workflows/secret-scan-gate.yml | 35 ----------- .github/workflows/security-audit.yml | 55 ++++++++++++++---- .github/workflows/trivy.yml | 58 ------------------- CHANGELOG.md | 2 +- docs/architecture/overview.md | 2 +- docs/repository/bootstrap-plan.md | 2 - docs/security/code-security.md | 15 ++--- docs/security/github-required-checks.md | 17 +++--- .../github-bootstrap-execution-policy.md | 4 +- scripts/checks/verify_supply_chain.py | 25 +++----- .../tests/test_supply_chain_policy.py | 51 ++++++++-------- 18 files changed, 125 insertions(+), 269 deletions(-) delete mode 100644 .github/workflows/bandit.yml delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/secret-scan-gate.yml delete mode 100644 .github/workflows/trivy.yml diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml deleted file mode 100644 index 4e44160cf..000000000 --- a/.github/workflows/bandit.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: bandit - -on: - push: - branches: - - develop - - main - workflow_dispatch: - -# Pull-request SAST is owned by the organization-required SAST Semgrep -# workflow. Keep Bandit as a trusted-branch/manual backstop without duplicating -# PR checkout, dependency installation, and Python scanning. -concurrency: - group: bandit-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: develop - -jobs: - bandit-scan: - name: Bandit Security Scan - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 - with: - version: "0.8.6" - enable-cache: false - - name: Sync Python dependencies - run: uv sync --project services/analysis-engine --group dev --frozen - - name: Run Bandit - working-directory: services/analysis-engine - run: uv run bandit -c pyproject.toml -r src diff --git a/.github/workflows/build-baseline.yml b/.github/workflows/build-baseline.yml index abec57b6b..13de8e648 100644 --- a/.github/workflows/build-baseline.yml +++ b/.github/workflows/build-baseline.yml @@ -12,6 +12,12 @@ on: tags: - "v*" +concurrency: + group: >- + ${{ github.workflow }}-${{ github.repository }}-${{ + github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read @@ -288,9 +294,11 @@ jobs: - name: Explain non-blocking macOS amd64 artifact upload failure if: ${{ steps.upload-macos-amd64.outcome == 'failure' }} run: | - echo "Artifact upload failed after the macOS amd64 bundle was packaged." >> "$GITHUB_STEP_SUMMARY" - echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." >> "$GITHUB_STEP_SUMMARY" - echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." >> "$GITHUB_STEP_SUMMARY" + { + echo "Artifact upload failed after the macOS amd64 bundle was packaged." + echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." + echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." + } >> "$GITHUB_STEP_SUMMARY" build-macos-arm64: name: build / macos / arm64 @@ -350,9 +358,11 @@ jobs: - name: Explain non-blocking macOS arm64 artifact upload failure if: ${{ steps.upload-macos-arm64.outcome == 'failure' }} run: | - echo "Artifact upload failed after the macOS arm64 bundle was packaged." >> "$GITHUB_STEP_SUMMARY" - echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." >> "$GITHUB_STEP_SUMMARY" - echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." >> "$GITHUB_STEP_SUMMARY" + { + echo "Artifact upload failed after the macOS arm64 bundle was packaged." + echo "Pull request builds keep artifact upload non-blocking because GitHub artifact service or DNS failures do not invalidate the build evidence." + echo "Tag and release builds remain blocking because release publication requires uploaded artifacts." + } >> "$GITHUB_STEP_SUMMARY" gate-macos: name: gate / build / macos diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d17468129..6e743c2ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,12 @@ on: - develop - main +concurrency: + group: >- + ${{ github.workflow }}-${{ github.repository }}-${{ + github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 140397ff2..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: codeql - -on: - push: - branches: - - develop - - main - workflow_dispatch: - -concurrency: - group: codeql-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - actions: read - contents: read - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: develop - -jobs: - analyze: - name: codeql - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - strategy: - fail-fast: false - matrix: - language: - - javascript-typescript - - python - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 - with: - languages: ${{ matrix.language }} - - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 - - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 162ddc0a2..8f5b1bc25 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -10,8 +10,8 @@ on: - main concurrency: - group: ossf-scorecard-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: false permissions: read-all diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34583b414..aa69a973c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,6 @@ name: release on: - pull_request: - branches: - - develop - - main push: branches: - develop @@ -13,6 +9,10 @@ on: - "v*" workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: false + permissions: contents: read diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 38700f773..df77ed859 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -15,6 +15,12 @@ on: types: - published +concurrency: + group: >- + ${{ github.workflow }}-${{ github.repository }}-${{ + github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read diff --git a/.github/workflows/secret-scan-gate.yml b/.github/workflows/secret-scan-gate.yml deleted file mode 100644 index 3ff6873e9..000000000 --- a/.github/workflows/secret-scan-gate.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: secret-scan-gate - -on: - pull_request: - branches: - - develop - - main - push: - branches: - - develop - - main - -concurrency: - group: >- - secret-scan-gate-${{ github.repository }}-${{ - github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: develop - -jobs: - secret-scan: - name: secret-scan-gate - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Scan for common hardcoded secrets - run: | - ! git grep -nE '(g[h]p_|g[h]o_|A[K]IA[0-9A-Z]{16}|A[I]za[0-9A-Za-z\-_]{35}|BEGIN (R[S]A|E[C]|OPENS[S]H|P[G]P) PRIVATE KEY)' -- . ':(exclude)package-lock.json' ':(exclude)node_modules/**' diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index da93100b0..07754a782 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -1,20 +1,15 @@ -name: security-audit +name: security-backstop on: - pull_request: - branches: - - develop - - main push: branches: - develop - main + workflow_dispatch: concurrency: - group: >- - security-audit-${{ github.repository }}-${{ - github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: false permissions: contents: read @@ -25,9 +20,12 @@ env: GIT_CONFIG_VALUE_0: develop jobs: - audit: - name: security-audit + security-backstop: + name: security-backstop runs-on: ubuntu-latest + permissions: + contents: read + security-events: write steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -55,6 +53,9 @@ jobs: run: uv sync --project services/analysis-engine --group dev --frozen - name: Audit Python dependencies run: uv run --project services/analysis-engine --with pip-audit==2.8.0 pip-audit --local --strict + - name: Run Bandit + working-directory: services/analysis-engine + run: uv run bandit -c pyproject.toml -r src - name: Install stable Rust toolchain run: rustup toolchain install stable --profile minimal - name: Install cargo-audit @@ -62,3 +63,35 @@ jobs: - name: Audit Rust dependencies working-directory: apps/desktop/src-tauri run: cargo +stable audit + - name: Scan for common hardcoded secrets + run: | + ! git grep -nE '(g[h]p_|g[h]o_|A[K]IA[0-9A-Z]{16}|A[I]za[0-9A-Za-z\-_]{35}|BEGIN (R[S]A|E[C]|OPENS[S]H|P[G]P) PRIVATE KEY)' -- . ':(exclude)package-lock.json' ':(exclude)node_modules/**' + - name: Run Trivy filesystem scan summary + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: . + version: v0.71.2 + format: table + severity: CRITICAL,HIGH,MEDIUM + exit-code: "0" + skip-dirs: services/analysis-engine/.venv + trivyignores: ./.trivyignore + - name: Run Trivy filesystem scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: . + version: v0.71.2 + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH,MEDIUM + limit-severities-for-sarif: true + exit-code: "1" + skip-dirs: services/analysis-engine/.venv + trivyignores: ./.trivyignore + - name: Upload Trivy scan results to GitHub Security tab + if: always() + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + with: + sarif_file: trivy-results.sarif diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml deleted file mode 100644 index dee8e9b20..000000000 --- a/.github/workflows/trivy.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: trivy - -on: - push: - branches: - - develop - - main - -concurrency: - group: trivy-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -env: - GIT_CONFIG_COUNT: "1" - GIT_CONFIG_KEY_0: init.defaultBranch - GIT_CONFIG_VALUE_0: develop - -jobs: - trivy-fs-scan: - name: trivy-fs-scan - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Run Trivy filesystem scan summary - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0; SHA pinning retained as supply-chain attack mitigation, do not replace with tag. - with: - scan-type: fs - scan-ref: . - version: v0.71.2 - format: table - severity: CRITICAL,HIGH,MEDIUM - exit-code: '0' - skip-dirs: 'services/analysis-engine/.venv' - trivyignores: ./.trivyignore - - name: Run Trivy filesystem scan - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0; SHA pinning retained as supply-chain attack mitigation, do not replace with tag. - with: - scan-type: fs - scan-ref: . - version: v0.71.2 - format: sarif - output: trivy-results.sarif - severity: CRITICAL,HIGH,MEDIUM - limit-severities-for-sarif: true - exit-code: '1' - skip-dirs: 'services/analysis-engine/.venv' - trivyignores: ./.trivyignore - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 peeled commit; SHA pinning retained as supply-chain attack mitigation. - if: always() - with: - sarif_file: trivy-results.sarif diff --git a/CHANGELOG.md b/CHANGELOG.md index 006049730..34331fb86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Changed -- Moved pull-request Bandit execution to the organization-required SAST owner while retaining Bandit, CodeQL, Scorecard, and Trivy as bounded trusted-branch backstops; repository-native dependency audits and the supplemental secret gate remain PR checks because central workflows do not fully replace them. +- Consolidated Bandit, dependency audits, supplemental secret checks, and Trivy into one trusted-branch security backstop, delegated CodeQL to GitHub default setup, and removed duplicate local PR security and release-preflight runs. - Pinned npm `10.9.9` as the approved lockfile generator, activated it through Node-bundled Corepack before dependency consumption, and fail closed unless its bundled `tar` is at least `7.5.19`; primary CI still consumes the committed lock only through frozen `npm ci` validation, rejects mutable npm resolution in the lock gate, requires integrity evidence for public-registry lock entries, and preserves generator-sensitive root `@esbuild/*` peer metadata. ### Fixed diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 303dd3570..e7e56d311 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -41,6 +41,6 @@ GitHub is the source of truth for repository governance, PR review, CI/CD, Code ## CI/CD and release flow -- PRs into `develop` and `main` run repository CI, security audit, secret-scan and SBOM checks alongside organization-required OSV, dependency-review, Trivy, CodeQL/code-quality, Semgrep SAST, Strix, and Noema evidence +- PRs into `develop` and `main` run repository CI, SBOM, and platform builds alongside organization-required OSV, dependency-review, Trivy, CodeQL/code-quality, Semgrep SAST, Strix, and Noema evidence; consolidated local security backstops run after trusted-branch pushes - release flows publish desktop artifacts plus SBOM evidence to GitHub Releases through a tag-driven draft-before-publish path - branch protection connects stable required checks after bootstrap workflows exist diff --git a/docs/repository/bootstrap-plan.md b/docs/repository/bootstrap-plan.md index 72ee651f5..7aedb1bdd 100644 --- a/docs/repository/bootstrap-plan.md +++ b/docs/repository/bootstrap-plan.md @@ -31,9 +31,7 @@ After workflows exist, require these stable checks on `main` and `develop`: - `CodeRabbit` - `ci / build-and-test` - `dependency-review` -- `security-audit` - `sbom` -- `release-preflight` - `gate / build / windows` - `gate / build / macos` - `trivy-fs` diff --git a/docs/security/code-security.md b/docs/security/code-security.md index 93fd24096..472d4d936 100644 --- a/docs/security/code-security.md +++ b/docs/security/code-security.md @@ -9,18 +9,15 @@ BandScope treats GitHub Code Security as part of bootstrap governance. - organization-required CodeQL/code-quality evidence and multi-language SAST on pull requests - organization-required Trivy filesystem and OSV vulnerability scans - organization-required dependency review on pull requests -- repository security audit workflow for npm, Python, and Rust dependencies in scope +- repository trusted-branch security backstop for npm, Python, and Rust dependencies in scope - Dependabot alerts and security updates -- secret scanning in GitHub plus the repository supplemental secret-scan gate workflow +- secret scanning in GitHub plus a supplemental trusted-branch secret check The central Security Scan owns PR OSV, dependency-review, Trivy, and soft -Scorecard evidence. Central Gitleaks currently runs only for -`ContextualWisdomLab/.github`, so BandScope keeps `secret-scan-gate` on pull -requests. The repository `security-audit` also remains a pull-request check: its -native npm, pip, and Cargo audits and documented exception handling are not -fully replaced by the central scanners. Bandit, CodeQL, Scorecard, and Trivy -remain push/schedule/manual backstops only; central SAST and CodeQL evidence own -their pull-request paths. +Scorecard evidence. BandScope combines npm, pip, Cargo, Bandit, supplemental +secret, and Trivy checks into one trusted-branch/manual backstop. GitHub default +setup owns CodeQL, while Scorecard remains separate for its restricted publish +permissions. Central workflows own every pull-request security path. ## Enforcement diff --git a/docs/security/github-required-checks.md b/docs/security/github-required-checks.md index 1dbf554d9..ce74b1af6 100644 --- a/docs/security/github-required-checks.md +++ b/docs/security/github-required-checks.md @@ -8,9 +8,7 @@ These are the merge-gate status checks that should be required on protected bran - `ci / build-and-test` - `dependency-review` -- `security-audit` - `sbom` -- `release-preflight` - `gate / build / windows` - `gate / build / macos` - `trivy-fs` @@ -30,9 +28,7 @@ These are the merge-gate status checks that should be required on protected bran - `ci / build-and-test` - `dependency-review` -- `security-audit` - `sbom` -- `release-preflight` - `gate / build / windows` - `gate / build / macos` - `trivy-fs` @@ -41,18 +37,19 @@ These are the merge-gate status checks that should be required on protected bran The organization required-workflow rule is the authoritative PR owner for `osv-scan`, `dependency-review`, `trivy-fs`, Scorecard visibility, Semgrep SAST, -Strix, and Noema. Repository-local Bandit, CodeQL, Scorecard, and Trivy run only -after trusted-branch pushes, on schedules where applicable, or manually. The -repository `security-audit` and `secret-scan-gate` remain PR-scoped because -their coverage has not been fully inherited by a central workflow. +Strix, and Noema. GitHub default setup owns CodeQL. One repository-local +`security-backstop` job combines dependency audits, Bandit, supplemental secret +checks, and Trivy after trusted-branch pushes or manual dispatch. Scorecard stays +separate because its publishing path has stricter permissions and SARIF handling. The lists above reflect the live classic required-status contexts verified on 2026-09-04. The active organization ruleset separately requires the central `close-empty-pr.yml`, `opencode-review.yml`, `pr-review-merge-scheduler.yml`, `security-scan.yml`, `strix.yml`, `sast-semgrep.yml`, and `noema-review.yml` workflows on the default branch. Keep these two enforcement mechanisms distinct -when changing local triggers: a context must not be removed while classic -protection still requires it. +when changing local triggers. The retired local `security-audit` and +`release-preflight` PR contexts were removed from classic protection with this +workflow consolidation. ## GitHub settings baseline diff --git a/docs/workflow/github-bootstrap-execution-policy.md b/docs/workflow/github-bootstrap-execution-policy.md index cc9958aa4..a88f0cddb 100644 --- a/docs/workflow/github-bootstrap-execution-policy.md +++ b/docs/workflow/github-bootstrap-execution-policy.md @@ -39,11 +39,9 @@ Bootstrap or setup work is not complete unless GitHub-facing supply-chain contro - `.github/dependabot.yml` - `.github/workflows/security-audit.yml` -- `.github/workflows/codeql.yml` -- `.github/workflows/bandit.yml` - `.github/workflows/sbom.yml` - `.github/workflows/release.yml` -- branch protection or rulesets for `main` and `develop` that require repository CI, security audit, secret scan, SBOM, release preflight, platform builds, and the organization-required Security Scan, CodeQL/code-quality, SAST, Strix, and review workflows +- branch protection or rulesets for `main` and `develop` that require repository CI, SBOM, platform builds, and the organization-required Security Scan, CodeQL/code-quality, SAST, Strix, and review workflows - PR workflow that still requests CodeRabbit review and records its result when the provider responds cleanly - release retention for the generated SBOM and supplemental inventory diff --git a/scripts/checks/verify_supply_chain.py b/scripts/checks/verify_supply_chain.py index 4d37443ab..5b87b8bff 100644 --- a/scripts/checks/verify_supply_chain.py +++ b/scripts/checks/verify_supply_chain.py @@ -18,15 +18,11 @@ Path("apps/desktop/src-tauri/Cargo.lock"), Path(".github/dependabot.yml"), # Dependency review runs via the org-level required workflow in - # ContextualWisdomLab/.github; repo-local Bandit, CodeQL, Scorecard, and - # Trivy stay push/schedule-only as trusted-branch backstops while central - # required workflows own PR enforcement. - Path(".github/workflows/bandit.yml"), + # ContextualWisdomLab/.github; one repo-local security backstop and + # Scorecard stay push/schedule-only while central workflows own PR scans. Path(".github/workflows/security-audit.yml"), - Path(".github/workflows/codeql.yml"), Path(".github/workflows/sbom.yml"), Path(".github/workflows/release.yml"), - Path(".github/workflows/secret-scan-gate.yml"), Path(".github/workflows/build-baseline.yml"), Path(".github/workflows/ossf-scorecard.yml"), Path(".trivyignore"), @@ -1220,7 +1216,7 @@ def _verify_dependency_review_coverage(missing: list[str]) -> None: def _verify_security_audit_coverage(missing: list[str]) -> None: audit = read_workflow(Path(".github/workflows/security-audit.yml"), "security audit", missing) - for token in ["develop", "main", "pull_request", "push"]: + for token in ["develop", "main", "push", "bandit", "git grep", "trivy-action"]: if audit and token not in audit: missing.append(f"security audit workflow missing trigger token: {token}") audit_run_commands: list[str] = [] @@ -1243,7 +1239,7 @@ def _verify_security_audit_coverage(missing: list[str]) -> None: def _verify_bandit_coverage(missing: list[str]) -> None: - bandit = read_workflow(Path(".github/workflows/bandit.yml"), "bandit", missing) + bandit = read_workflow(Path(".github/workflows/security-audit.yml"), "bandit", missing) for token in ["develop", "main", "push", "bandit"]: if bandit and token not in bandit: missing.append(f"bandit workflow missing token: {token}") @@ -1254,12 +1250,8 @@ def _verify_bandit_coverage(missing: list[str]) -> None: def _verify_codeql_coverage(missing: list[str]) -> None: - codeql = read_workflow( - Path(".github/workflows/codeql.yml"), "codeql", missing, optional=True - ) - for token in ["develop", "main", "push", "codeql"]: - if codeql and token not in codeql: - missing.append(f"codeql workflow missing token: {token}") + if Path(".github/workflows/codeql.yml").exists(): + missing.append("repo-local codeql workflow duplicates GitHub default setup") def _verify_release_coverage(missing: list[str]) -> None: @@ -1267,7 +1259,6 @@ def _verify_release_coverage(missing: list[str]) -> None: for token in [ "develop", "main", - "pull_request", "push", "tags:", "release-preflight", @@ -1278,9 +1269,9 @@ def _verify_release_coverage(missing: list[str]) -> None: def _verify_secret_scan_coverage(missing: list[str]) -> None: secret_scan = read_workflow( - Path(".github/workflows/secret-scan-gate.yml"), "secret scan", missing + Path(".github/workflows/security-audit.yml"), "secret scan", missing ) - for token in ["develop", "main", "pull_request", "push", "secret-scan-gate"]: + for token in ["develop", "main", "push", "git grep"]: if secret_scan and token not in secret_scan: missing.append(f"secret scan workflow missing token: {token}") diff --git a/services/analysis-engine/tests/test_supply_chain_policy.py b/services/analysis-engine/tests/test_supply_chain_policy.py index 1b232ec89..1d8224c5a 100644 --- a/services/analysis-engine/tests/test_supply_chain_policy.py +++ b/services/analysis-engine/tests/test_supply_chain_policy.py @@ -1235,56 +1235,51 @@ def test_supply_chain_check_accepts_repo_ossf_publish_restrictions( assert not any("ossf scorecard" in violation for violation in violations) -def test_central_governance_workflows_are_push_only_where_local_signals_remain() -> None: - """Ensure central PR governance keeps only repo-local push security signals.""" +def test_central_governance_workflows_are_consolidated_push_backstops() -> None: + """Ensure central PR governance leaves one local push security backstop.""" repo_root = Path(__file__).resolve().parents[3] workflows_dir = repo_root / ".github" / "workflows" assert not (workflows_dir / "dependency-review.yml").exists() - for local_signal in ( - "bandit.yml", - "codeql.yml", - "ossf-scorecard.yml", - "trivy.yml", - ): - workflow = workflows_dir / local_signal - assert workflow.exists(), ( - f"{local_signal} keeps repository-local security-tab/SAST signal " - "while central required workflows handle PR enforcement" - ) - assert "pull_request:" not in workflow.read_text(encoding="utf-8") + security_backstop = workflows_dir / "security-audit.yml" + assert security_backstop.exists() + workflow = security_backstop.read_text(encoding="utf-8") + assert "pull_request:" not in workflow + for retired_workflow in ("bandit.yml", "codeql.yml", "secret-scan-gate.yml", "trivy.yml"): + assert not (workflows_dir / retired_workflow).exists() supply_chain = load_module( "scripts/checks/verify_supply_chain.py", "verify_supply_chain_central" ) required = {path.as_posix() for path in supply_chain.REQUIRED_FILES} assert ".github/workflows/dependency-review.yml" not in required - assert ".github/workflows/codeql.yml" in required + assert ".github/workflows/codeql.yml" not in required + assert ".github/workflows/security-audit.yml" in required assert ".github/workflows/ossf-scorecard.yml" in required -def test_local_security_workflows_cancel_superseded_runs_with_read_only_defaults() -> None: - """Keep retained local security signals bounded and least-privileged.""" +def test_workflow_concurrency_cancels_only_superseded_pr_heads() -> None: + """Cancel same-PR stale heads without cancelling push, release, or schedule work.""" repo_root = Path(__file__).resolve().parents[3] workflows_dir = repo_root / ".github" / "workflows" - for workflow_name in ( - "bandit.yml", - "codeql.yml", - "ossf-scorecard.yml", - "secret-scan-gate.yml", - "security-audit.yml", - "trivy.yml", - ): + for workflow_name in ("build-baseline.yml", "ci.yml", "sbom.yml"): workflow = (workflows_dir / workflow_name).read_text(encoding="utf-8") assert "concurrency:" in workflow, workflow_name - assert "cancel-in-progress: true" in workflow, workflow_name + assert "github.workflow }}-${{ github.repository }}" in workflow, workflow_name + assert "github.event.pull_request.number" in workflow, workflow_name + assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in workflow + + for workflow_name in ("ossf-scorecard.yml", "release.yml", "security-audit.yml"): + workflow = (workflows_dir / workflow_name).read_text(encoding="utf-8") + assert "concurrency:" in workflow, workflow_name + assert "cancel-in-progress: false" in workflow, workflow_name assert "contents: read" in workflow or "permissions: read-all" in workflow, ( workflow_name ) - if workflow_name in {"bandit.yml", "codeql.yml", "ossf-scorecard.yml"}: - assert "github.event_name" in workflow, workflow_name + + assert "pull_request:" not in (workflows_dir / "release.yml").read_text(encoding="utf-8") def test_opencode_review_declares_top_level_token_permissions() -> None: