diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..9b9c3ba --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,57 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: '0 6 * * 1' + +permissions: + contents: read + +jobs: + codeql: + name: CodeQL (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-codeql-${{ matrix.language }} + cancel-in-progress: true + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [python, actions] + steps: + - uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + - name: Analyze + uses: github/codeql-action/analyze@v4 + + dependency-review: + name: Dependency Review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v6 + + - name: Review dependency changes + uses: actions/dependency-review-action@v5 + with: + fail-on-severity: high + comment-summary-in-pr: on-failure diff --git a/changelog.d/20260809_issue_48_security_scanning.md b/changelog.d/20260809_issue_48_security_scanning.md new file mode 100644 index 0000000..789c322 --- /dev/null +++ b/changelog.d/20260809_issue_48_security_scanning.md @@ -0,0 +1,4 @@ +### Security + +- Add CodeQL scanning for Python code and GitHub Actions workflows, plus + dependency review for pull requests. diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 725b5b5..ab62e58 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -105,6 +105,35 @@ def test_workflow_run_blocks_do_not_interpolate_untrusted_inputs() -> None: ) +def test_security_workflow_scans_code_actions_and_dependencies() -> None: + """Security checks must cover pushes, pull requests, and scheduled scans.""" + workflow = read_workflow("security.yml") + codeql_job = workflow_job_block(workflow, "codeql") + dependency_job = workflow_job_block(workflow, "dependency-review") + + assert "branches: [main]" in workflow + assert "pull_request:" in workflow + assert "schedule:" in workflow + assert "cron: '0 6 * * 1'" in workflow + assert "permissions:\n contents: read" in workflow + + assert "timeout-minutes: 30" in codeql_job + assert "security-events: write" in codeql_job + assert "language: [python, actions]" in codeql_job + assert "languages: ${{ matrix.language }}" in codeql_job + assert "uses: github/codeql-action/init@v4" in codeql_job + assert "uses: github/codeql-action/autobuild@v4" in codeql_job + assert "uses: github/codeql-action/analyze@v4" in codeql_job + assert "cancel-in-progress: true" in codeql_job + + assert "if: github.event_name == 'pull_request'" in dependency_job + assert "timeout-minutes: 10" in dependency_job + assert "pull-requests: write" in dependency_job + assert "uses: actions/dependency-review-action@v5" in dependency_job + assert "fail-on-severity: high" in dependency_job + assert "comment-summary-in-pr: on-failure" in dependency_job + + def test_changelog_check_safely_requires_a_fragment() -> None: """Source-changing pull requests must fail safely without a fragment.""" workflow = read_workflow("release.yml")