diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2df162d89..8221219aa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -38,4 +38,4 @@ updates: open-pull-requests-limit: 10 labels: - "dependencies" - - "github-actions" + - "area: ci-cd" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 27c5b540f..54616541d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,8 +32,8 @@ jobs: - python steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 with: languages: ${{ matrix.language }} - - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 - - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + - uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 2a4b6eaa9..dd7c2270c 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -8,6 +8,10 @@ on: branches: - develop - main + pull_request: + branches: + - develop + - main permissions: read-all @@ -27,13 +31,13 @@ jobs: with: persist-credentials: false - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 - if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + if: github.event_name == 'pull_request' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) with: results_file: results.sarif results_format: sarif publish_results: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + if: github.event_name == 'pull_request' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) with: name: ossf-scorecard-results path: results.sarif @@ -41,7 +45,7 @@ jobs: scorecard-sarif-upload: name: scorecard-sarif-upload needs: analysis - if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + if: github.event_name == 'pull_request' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) runs-on: ubuntu-latest permissions: actions: read @@ -63,7 +67,7 @@ jobs: with: persist-credentials: false path: trusted-scorecard-scripts - ref: ${{ github.ref_name }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.ref_name }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ossf-scorecard-results @@ -79,6 +83,6 @@ jobs: python3 trusted-scorecard-scripts/scripts/checks/normalize_scorecard_sarif.py scorecard-sarif/results.sarif normalized-scorecard-results.sarif - - uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 peeled commit; SHA pinning retained as supply-chain attack mitigation. + - uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 peeled commit; SHA pinning retained as supply-chain attack mitigation. with: sarif_file: normalized-scorecard-results.sarif diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index d79ec32e1..db6c3db87 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -48,7 +48,7 @@ jobs: 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. + uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 peeled commit; SHA pinning retained as supply-chain attack mitigation. if: always() with: sarif_file: trivy-results.sarif diff --git a/services/analysis-engine/tests/test_codeql_action_version_contract.py b/services/analysis-engine/tests/test_codeql_action_version_contract.py new file mode 100644 index 000000000..6e225686d --- /dev/null +++ b/services/analysis-engine/tests/test_codeql_action_version_contract.py @@ -0,0 +1,22 @@ +"""Regression contract for coordinated CodeQL Action component upgrades.""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +CODEQL_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "codeql.yml" +CODEQL_ACTION_PATTERN = re.compile( + r"github/codeql-action/(?:init|autobuild|analyze)@([0-9a-f]{40})\s+#\s+(v[0-9.]+)" +) + + +def test_codeql_job_uses_one_action_release() -> None: + """Require init, autobuild, and analyze to use one reviewed CodeQL release.""" + workflow = CODEQL_WORKFLOW.read_text(encoding="utf-8") + action_refs = CODEQL_ACTION_PATTERN.findall(workflow) + + assert len(action_refs) == 3 + assert len({sha for sha, _version in action_refs}) == 1 + assert len({version for _sha, version in action_refs}) == 1 diff --git a/services/analysis-engine/tests/test_codeql_upload_sarif_version_contract.py b/services/analysis-engine/tests/test_codeql_upload_sarif_version_contract.py new file mode 100644 index 000000000..523c548b1 --- /dev/null +++ b/services/analysis-engine/tests/test_codeql_upload_sarif_version_contract.py @@ -0,0 +1,23 @@ +"""Regression contracts for CodeQL SARIF uploader provenance comments.""" + +from __future__ import annotations + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +EXPECTED_SHA = "db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28" +EXPECTED_VERSION = "v4.37.8" +UPLOAD_SARIF_REF = ( + f"github/codeql-action/upload-sarif@{EXPECTED_SHA} # {EXPECTED_VERSION} peeled commit;" +) +WORKFLOWS = ( + REPO_ROOT / ".github" / "workflows" / "ossf-scorecard.yml", + REPO_ROOT / ".github" / "workflows" / "trivy.yml", +) + + +def test_upload_sarif_sha_and_version_comment_move_together() -> None: + """Keep each immutable SARIF uploader pin paired with its reviewed release label.""" + for workflow in WORKFLOWS: + contents = workflow.read_text(encoding="utf-8") + assert contents.count(UPLOAD_SARIF_REF) == 1 diff --git a/services/analysis-engine/tests/test_dependabot_label_contract.py b/services/analysis-engine/tests/test_dependabot_label_contract.py new file mode 100644 index 000000000..4fad611ee --- /dev/null +++ b/services/analysis-engine/tests/test_dependabot_label_contract.py @@ -0,0 +1,17 @@ +"""Regression contract for Dependabot's configured repository labels.""" + +from __future__ import annotations + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +DEPENDABOT_CONFIG = REPO_ROOT / ".github" / "dependabot.yml" + + +def test_github_actions_updates_use_repository_ci_cd_taxonomy() -> None: + """Keep GitHub Actions updates on an existing CI/CD taxonomy label.""" + config = DEPENDABOT_CONFIG.read_text(encoding="utf-8") + github_actions = config.split('package-ecosystem: "github-actions"', maxsplit=1)[1] + + assert '- "area: ci-cd"' in github_actions + assert '- "github-actions"' not in github_actions diff --git a/services/analysis-engine/tests/test_scorecard_pr_code_scanning_contract.py b/services/analysis-engine/tests/test_scorecard_pr_code_scanning_contract.py new file mode 100644 index 000000000..3d03fd0df --- /dev/null +++ b/services/analysis-engine/tests/test_scorecard_pr_code_scanning_contract.py @@ -0,0 +1,30 @@ +"""Regression contract for OpenSSF Scorecard evidence on pull-request heads.""" + +from __future__ import annotations + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +WORKFLOW = REPO_ROOT / ".github" / "workflows" / "ossf-scorecard.yml" +DEFAULT_BRANCH_GUARD = "github.ref == format('refs/heads/{0}', github.event.repository.default_branch)" +PR_OR_DEFAULT_BRANCH = f"github.event_name == 'pull_request' || {DEFAULT_BRANCH_GUARD}" +PR_SAFE_TRUSTED_REF = ( + "ref: ${{ github.event_name == 'pull_request' && " + "github.event.pull_request.base.sha || github.ref_name }}" +) + + +def test_scorecard_produces_pr_code_scanning_evidence_without_pr_publishing() -> None: + """Keep PR SARIF coverage while publishing only trusted default-branch runs.""" + contents = WORKFLOW.read_text(encoding="utf-8") + + assert "pull_request_target:" not in contents + assert " pull_request:\n branches:\n - develop\n - main\n" in contents + assert contents.count(f"if: {PR_OR_DEFAULT_BRANCH}") == 3 + publish_lines = [ + line.strip() + for line in contents.splitlines() + if line.strip().startswith("publish_results:") + ] + assert publish_lines == [f"publish_results: ${{{{ {DEFAULT_BRANCH_GUARD} }}}}"] + assert PR_SAFE_TRUSTED_REF in contents