Skip to content

Security Scan

Security Scan #3475

Workflow file for this run

# 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
}
});