Security Scan #3475
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Security scan workflow triggered after CI completes successfully. | |
| # Uses workflow_run to run in base repo context, giving access to secrets for fork PRs. | |
| # | |
| # !!!! PLEASE be extra careful here and treat forked repository as untrusted user input !!!! | |
| name: Security Scan | |
| on: | |
| workflow_run: | |
| workflows: ["CI"] | |
| types: [completed] | |
| # pull_request: # uncomment to test this workflow in PR | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.head_ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| checks: write # to create check run on related PR | |
| # Normalizes the parent-CI context into a single set of env vars so downstream | |
| # jobs don't need to branch on `github.event_name`. On `workflow_run` these come | |
| # from the triggering run; on `pull_request` (used for testing this workflow | |
| # in-place) they're synthesized from the PR payload. | |
| env: | |
| PARENT_WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion || github.event.pull_request && 'success' }} | |
| PARENT_WORKFLOW_EVENT: ${{ github.event.workflow_run.event || github.event_name }} | |
| PARENT_WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha }} | |
| jobs: | |
| create-check-run: | |
| runs-on: [self-hosted, akash] | |
| outputs: | |
| run_scan: ${{ steps.decide.outputs.run_scan }} | |
| event: ${{ env.PARENT_WORKFLOW_EVENT }} | |
| check_run_id: ${{ steps.create_check.outputs.check_run_id }} | |
| skip_reason: ${{ steps.decide.outputs.skip_reason }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: .github | |
| - name: Get associated PR | |
| if: ${{ env.PARENT_WORKFLOW_EVENT == 'pull_request' }} | |
| id: associated_pr | |
| uses: ./.github/actions/associated-pr | |
| with: | |
| ref: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| display-pr-details: 'true' | |
| - name: Decide whether to run scan | |
| id: decide | |
| run: | # shell | |
| if [ "${PARENT_WORKFLOW_CONCLUSION}" != "success" ]; then | |
| echo "run_scan=false" >> "$GITHUB_OUTPUT" | |
| echo "skip_reason=Upstream CI conclusion is '${PARENT_WORKFLOW_CONCLUSION}'" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ "${PARENT_WORKFLOW_EVENT}" != "pull_request" && "${PARENT_WORKFLOW_EVENT}" != "push" ]]; then | |
| echo "run_scan=false" >> "$GITHUB_OUTPUT" | |
| echo "skip_reason=Upstream CI was not triggered by pull_request or push (event=${PARENT_WORKFLOW_EVENT})" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "run_scan=true" >> "$GITHUB_OUTPUT" | |
| echo "skip_reason=" >> "$GITHUB_OUTPUT" | |
| - name: Create check run | |
| id: create_check | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | # js | |
| const sha = process.env.PARENT_WORKFLOW_HEAD_SHA; | |
| const workflowRunUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; | |
| const res = await github.rest.checks.create({ | |
| name: "security-scan", | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| head_sha: sha, | |
| status: "in_progress", | |
| details_url: workflowRunUrl, | |
| output: { | |
| title: "Security scan", | |
| summary: `Initializing... Managed in [this workflow run](${workflowRunUrl})` | |
| } | |
| }); | |
| core.setOutput("check_run_id", String(res.data.id)); | |
| setup-static-analyses: | |
| needs: [create-check-run] | |
| if: ${{ needs.create-check-run.outputs.run_scan == 'true' && needs.create-check-run.outputs.event == 'pull_request' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.matrix.outputs.value }} | |
| should_run: ${{ !contains(fromJSON(steps.associated_pr.outputs.labels), 'ignore-static-analyses') }} | |
| pr_head_ref: ${{ steps.associated_pr.outputs.head_ref }} | |
| pr_head_repo: ${{ steps.associated_pr.outputs.head_repo }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Get associated PR | |
| id: associated_pr | |
| uses: ./.github/actions/associated-pr | |
| with: | |
| ref: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| - name: Build matrix from changed files | |
| if: ${{ !contains(fromJSON(steps.associated_pr.outputs.labels), 'ignore-static-security-analysis') }} | |
| id: matrix | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ steps.associated_pr.outputs.number }} | |
| run: | # shell | |
| changed_files=$(gh api "/repos/${{ github.repository }}/pulls/$PR_NUMBER/files" \ | |
| --paginate --jq '.[].filename') | |
| all_apps=$(find apps/* -maxdepth 0 -type d -print0 | xargs -0 -n1 basename) | |
| changed_apps=() | |
| for app in $all_apps; do | |
| if echo "$changed_files" | grep -q "^apps/$app/"; then | |
| changed_apps+=("$app") | |
| fi | |
| done | |
| if [ ${#changed_apps[@]} -eq 0 ]; then | |
| echo "No changed apps detected, skipping static analyses" | |
| echo "value=" >> "$GITHUB_OUTPUT" | |
| else | |
| workspaces=$(printf '"%s",' "${changed_apps[@]}") | |
| workspaces="[${workspaces%,}]" | |
| echo "Changed apps detected: ${changed_apps[*]}" | |
| echo 'value={"workspace":'"$workspaces"'}' >> "$GITHUB_OUTPUT" | |
| fi | |
| static-analyses: | |
| needs: [setup-static-analyses] | |
| if: ${{ needs.setup-static-analyses.outputs.should_run == 'true' && needs.setup-static-analyses.outputs.matrix != '' }} | |
| strategy: | |
| matrix: ${{ fromJSON(needs.setup-static-analyses.outputs.matrix) }} | |
| fail-fast: false | |
| uses: ./.github/workflows/reusable-validate-app-unsafe.yml | |
| secrets: | |
| gh-token: ${{ secrets.GITHUB_TOKEN }} | |
| snyk-token: ${{ secrets.SNYK_TOKEN }} | |
| with: | |
| path: apps/${{ matrix.workspace }} | |
| pr_head_ref: ${{ needs.setup-static-analyses.outputs.pr_head_ref }} | |
| pr_head_repo: ${{ needs.setup-static-analyses.outputs.pr_head_repo }} | |
| apps-deps-scan: | |
| needs: [create-check-run] | |
| if: ${{ needs.create-check-run.outputs.run_scan == 'true' && needs.create-check-run.outputs.event == 'pull_request' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: | | |
| .github | |
| - name: Get associated PR | |
| id: associated_pr | |
| uses: ./.github/actions/associated-pr | |
| with: | |
| ref: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| export-file-changes: package-lock.json | |
| - name: Check snyk status | |
| if: >- | |
| contains(fromJSON(steps.associated_pr.outputs.changes), 'package-lock.json') | |
| && !contains(fromJSON(steps.associated_pr.outputs.labels), 'ignore-apps-deps-scan') | |
| uses: ./.github/actions/poll-check-status | |
| with: | |
| name: "security/snyk (corysturtevant)" | |
| sha: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| check_type: status | |
| - name: Check socketio status | |
| if: >- | |
| contains(fromJSON(steps.associated_pr.outputs.changes), 'package-lock.json') | |
| && !contains(fromJSON(steps.associated_pr.outputs.labels), 'ignore-apps-deps-scan') | |
| uses: ./.github/actions/poll-check-status | |
| with: | |
| name: "Socket Security: Pull Request Alerts" | |
| sha: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| codeql-scan: | |
| if: needs.create-check-run.outputs.run_scan == 'true' | |
| needs: [create-check-run] | |
| name: Analyze (${{ matrix.language }}) | |
| # Runner size impacts CodeQL analysis time. To learn more, please see: | |
| # - https://gh.io/recommended-hardware-resources-for-running-codeql | |
| # - https://gh.io/supported-runners-and-hardware-resources | |
| # - https://gh.io/using-larger-runners (GitHub.com only) | |
| # Consider using larger runners or machines with greater resources for possible analysis time improvements. | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # required for all workflows | |
| security-events: write | |
| # required to fetch internal or private CodeQL packs | |
| packages: read | |
| # only required for workflows in private repositories | |
| actions: read | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - language: actions | |
| - language: javascript-typescript | |
| wait-for-check: true | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| - name: Get associated PR | |
| if: ${{ env.PARENT_WORKFLOW_EVENT == 'pull_request' }} | |
| id: associated_pr | |
| uses: ./.github/actions/associated-pr | |
| with: | |
| ref: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| - name: Initialize CodeQL | |
| if: ${{ steps.associated_pr.outcome == 'skipped' || !contains(fromJSON(steps.associated_pr.outputs.labels), 'ignore-codeql-security') }} | |
| uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 | |
| with: | |
| languages: ${{ matrix.language }} | |
| build-mode: none | |
| - name: Perform CodeQL Analysis | |
| if: ${{ steps.associated_pr.outcome == 'skipped' || !contains(fromJSON(steps.associated_pr.outputs.labels), 'ignore-codeql-security') }} | |
| uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 | |
| with: | |
| category: "/language:${{matrix.language}}" | |
| # Override GITHUB_SHA/REF (default branch under workflow_run) so the | |
| # "CodeQL" check lands on the PR head or main. Ignored for fork PRs. | |
| sha: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| ref: ${{ env.PARENT_WORKFLOW_EVENT == 'pull_request' && format('refs/pull/{0}/head', steps.associated_pr.outputs.number) || 'refs/heads/main' }} | |
| - name: Wait for CodeQL status | |
| if: ${{ matrix.wait-for-check && steps.associated_pr.outputs.labels && !contains(fromJSON(steps.associated_pr.outputs.labels), 'ignore-codeql-security') }} | |
| uses: ./.github/actions/poll-check-status | |
| with: | |
| name: "CodeQL" | |
| sha: ${{ env.PARENT_WORKFLOW_HEAD_SHA }} | |
| max_retries: 10 | |
| finalize: | |
| runs-on: ubuntu-latest | |
| needs: [create-check-run, static-analyses, codeql-scan, apps-deps-scan] | |
| if: ${{ always() && needs.create-check-run.result == 'success' }} | |
| steps: | |
| - name: Final conclusion and summary | |
| id: final | |
| env: | |
| RUN_SCAN: ${{ needs.create-check-run.outputs.run_scan }} | |
| SKIP_REASON: ${{ needs.create-check-run.outputs.skip_reason }} | |
| RELATED_WORKFLOW_CONCLUSION: ${{ env.PARENT_WORKFLOW_CONCLUSION }} | |
| STATIC_ANALYSES_RESULT: ${{ needs.static-analyses.result }} | |
| APPS_DEPS_SCAN_RESULT: ${{ needs.apps-deps-scan.result }} | |
| CODEQL_SCAN_RESULT: ${{ needs.codeql-scan.result }} | |
| run: | # shell | |
| emit_output() { | |
| local conclusion="$1" | |
| local summary="$2" | |
| local workflow_run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| summary="$summary"$'\n\n'"Managed in [this workflow run]($workflow_run_url)" | |
| { | |
| echo "conclusion=$conclusion" | |
| echo "summary<<SUMMARY_EOF" | |
| echo "$summary" | |
| echo "SUMMARY_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| } | |
| if [[ "$RUN_SCAN" != "true" ]]; then | |
| if [[ "$RELATED_WORKFLOW_CONCLUSION" == "success" ]]; then | |
| emit_output "success" "Skipped (treated as pass): $SKIP_REASON" | |
| else | |
| emit_output "failure" "Blocked: $SKIP_REASON" | |
| fi | |
| exit 0 | |
| fi | |
| scans=( | |
| "Static analysis|$STATIC_ANALYSES_RESULT" | |
| "Apps deps scan|$APPS_DEPS_SCAN_RESULT" | |
| "CodeQL scan|$CODEQL_SCAN_RESULT" | |
| ) | |
| conclusion="success" | |
| details=() | |
| for scan in "${scans[@]}"; do | |
| label="${scan%%|*}" | |
| result="${scan#*|}" | |
| if [[ "$result" == "success" || "$result" == "skipped" ]]; then | |
| details+=("$label: passed (result=$result)") | |
| else | |
| conclusion="failure" | |
| details+=("$label: failed (result=$result)") | |
| fi | |
| done | |
| summary=$(printf '%s\n' "${details[@]}") | |
| emit_output "$conclusion" "$summary" | |
| - name: Complete check run | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| CHECK_RUN_ID: ${{ needs.create-check-run.outputs.check_run_id }} | |
| CONCLUSION: ${{ steps.final.outputs.conclusion }} | |
| SUMMARY: ${{ steps.final.outputs.summary }} | |
| with: | |
| script: | # js | |
| const sha = process.env.PARENT_WORKFLOW_HEAD_SHA; | |
| const checkRunId = Number(process.env.CHECK_RUN_ID); | |
| const workflowRunUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; | |
| await github.rest.checks.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| check_run_id: checkRunId, | |
| head_sha: sha, | |
| status: "completed", | |
| details_url: workflowRunUrl, | |
| conclusion: process.env.CONCLUSION, | |
| output: { | |
| title: "Security scan", | |
| summary: process.env.SUMMARY | |
| } | |
| }); |