Add security scanning Github Action #18
Workflow file for this run
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
name: Security Scan | |
env: | |
CODE_EDITOR_TARGETS: '["code-editor-sagemaker-server"]' | |
on: | |
# Trigger 1: PR created on main or version branches (*.*) | |
pull_request: | |
branches: | |
- main | |
- '*.*' | |
types: [opened, reopened, synchronize] | |
# Trigger 2: Daily scheduled run at 00:13 UTC | |
# Schedule it a random minute because most Github Actions are scheduled | |
# at the start of the hour and their invocation can get delayed. | |
# Ref: https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule | |
schedule: | |
- cron: '13 0 * * *' | |
# Trigger 3: Manual trigger | |
workflow_dispatch: | |
jobs: | |
get-branches-to-scan: | |
runs-on: ubuntu-latest | |
outputs: | |
security-scan-branches: ${{ steps.determine-pr-branches.outputs.branches || steps.determine-scheduled-security-scan-branches.outputs.branches }} | |
global-dependencies-branches: ${{ steps.determine-pr-branches.outputs.branches || steps.determine-scheduled-global-dependencies-branches.outputs.branches }} | |
output-branch-name: ${{ steps.determine-pr-branches.outputs.output-branch-name || steps.get-upstream-branches.outputs.output-branch-name }} | |
steps: | |
- name: Checkout repository | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Determine branches for PR events | |
id: determine-pr-branches | |
if: github.event_name == 'pull_request' | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
# For PR events, validate base branch and use head ref if valid | |
base_ref="${{ github.base_ref }}" | |
head_ref="${{ github.head_ref }}" | |
echo "Base branch: $base_ref" | |
echo "Head branch: $head_ref" | |
if [[ "$base_ref" =~ ^[0-9]+\.[0-9]+$ ]] || [[ "$base_ref" == "main" ]]; then | |
echo "Base branch matches allowed pattern (main or digit.digit)" | |
echo "branches=[\"$head_ref\"]" >> $GITHUB_OUTPUT | |
echo "output-branch-name=$base_ref" >> $GITHUB_OUTPUT | |
echo "Branches to scan: [$head_ref]" | |
echo "Output files will use branch name: $base_ref" | |
else | |
echo "Base branch does not match allowed pattern - no branches to scan" | |
echo "branches=[]" >> $GITHUB_OUTPUT | |
echo "output-branch-name=" >> $GITHUB_OUTPUT | |
fi | |
- name: Get all upstream branches | |
id: get-upstream-branches | |
if: github.event_name != 'pull_request' | |
run: | | |
# Get main branch and all version branches (*.*) | |
branches=$(git branch -r | grep -E 'origin/(main|[0-9]+\.[0-9]+)' | sed 's/origin\///' | tr '\n' ' ') | |
echo "Found upstream branches: $branches" | |
echo "upstream-branches=$branches" >> $GITHUB_OUTPUT | |
echo "output-branch-name=scheduled" >> $GITHUB_OUTPUT | |
- name: Get completed workflows from previous day | |
id: get-completed-workflows | |
if: github.event_name != 'pull_request' | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
workflow_name="Security Scan" | |
# Get workflows from previous day (00:00 UTC to 23:59 UTC) | |
previous_day_start=$(date -d 'yesterday' -u +%Y-%m-%dT00:00:00Z) | |
previous_day_end=$(date -d 'yesterday' -u +%Y-%m-%dT23:59:59Z) | |
echo "Getting completed workflows from previous day: $previous_day_start to $previous_day_end" | |
# Get all completed workflow runs from previous day | |
completed_runs=$(gh run list --workflow="$workflow_name" --json databaseId,startedAt,conclusion,headBranch --status completed --limit 100) | |
recent_runs=$(echo "$completed_runs" | jq --arg start "$previous_day_start" --arg end "$previous_day_end" '.[] | select(.startedAt >= $start and .startedAt <= $end)') | |
echo "Found completed workflow runs from previous day:" | |
echo "$recent_runs" | jq -r '.databaseId' | |
# Store workflow run IDs for artifact checking | |
run_ids=$(echo "$recent_runs" | jq -r '.databaseId' | tr '\n' ' ') | |
echo "workflow-run-ids=$run_ids" >> $GITHUB_OUTPUT | |
- name: Check for successful scan artifacts from previous day | |
id: check-scan-artifacts | |
if: github.event_name != 'pull_request' | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
run_ids="${{ steps.get-completed-workflows.outputs.workflow-run-ids }}" | |
successful_security_scan_branches="" | |
successful_global_dependencies_branches="" | |
echo "Checking for successful scan artifacts from workflow runs: $run_ids" | |
for run_id in $run_ids; do | |
if [ -n "$run_id" ]; then | |
echo "Checking artifacts for run ID: $run_id" | |
# Get artifacts for this run | |
artifacts=$(gh api /repos/${{ github.repository }}/actions/runs/$run_id/artifacts --jq '.artifacts[].name') | |
# Check for scan-success-branch-* artifacts | |
security_scan_artifacts=$(echo "$artifacts" | grep "^scan-success-branch-" || true) | |
global_dependencies_artifacts=$(echo "$artifacts" | grep "^global-scan-success-" || true) | |
# Extract branch names from artifact names | |
for artifact in $security_scan_artifacts; do | |
branch_name=$(echo "$artifact" | sed 's/scan-success-branch-files//' | sed 's/scan-success-branch-//') | |
if [ -n "$branch_name" ]; then | |
successful_security_scan_branches="$successful_security_scan_branches $branch_name" | |
fi | |
done | |
for artifact in $global_dependencies_artifacts; do | |
branch_name=$(echo "$artifact" | sed 's/global-scan-success-//') | |
if [ -n "$branch_name" ]; then | |
successful_global_dependencies_branches="$successful_global_dependencies_branches $branch_name" | |
fi | |
done | |
fi | |
done | |
# Remove duplicates and clean up | |
successful_security_scan_branches=$(echo $successful_security_scan_branches | tr ' ' '\n' | sort -u | tr '\n' ' ') | |
successful_global_dependencies_branches=$(echo $successful_global_dependencies_branches | tr ' ' '\n' | sort -u | tr '\n' ' ') | |
echo "Branches with successful security scans from previous day: $successful_security_scan_branches" | |
echo "Branches with successful global dependency scans from previous day: $successful_global_dependencies_branches" | |
echo "successful-security-scan-branches=$successful_security_scan_branches" >> $GITHUB_OUTPUT | |
echo "successful-global-dependencies-branches=$successful_global_dependencies_branches" >> $GITHUB_OUTPUT | |
- name: Determine security scan branches for scheduled runs | |
id: determine-scheduled-security-scan-branches | |
if: github.event_name != 'pull_request' | |
run: | | |
upstream_branches="${{ steps.get-upstream-branches.outputs.upstream-branches }}" | |
successful_branches="${{ steps.check-scan-artifacts.outputs.successful-security-scan-branches }}" | |
branches_to_scan="" | |
echo "Upstream branches: $upstream_branches" | |
echo "Successfully scanned branches from previous day: $successful_branches" | |
# Check each upstream branch | |
for branch in $upstream_branches; do | |
branch=$(echo $branch | xargs) # trim whitespace | |
if [ -n "$branch" ]; then | |
# Check if this branch was successfully scanned in the previous day | |
if echo "$successful_branches" | grep -q "\b$branch\b"; then | |
echo "Skipping branch $branch - found successful scan from previous day" | |
else | |
echo "Adding branch $branch to security scan list - no successful scan from previous day" | |
branches_to_scan="$branches_to_scan $branch" | |
fi | |
fi | |
done | |
# Clean up and convert to JSON array | |
branches_to_scan=$(echo $branches_to_scan | xargs) | |
if [ -n "$branches_to_scan" ]; then | |
json_branches=$(echo "$branches_to_scan" | tr ' ' '\n' | jq -R . | jq -s -c .) | |
echo "branches=$json_branches" >> $GITHUB_OUTPUT | |
echo "Security scan branches to scan: $json_branches" | |
else | |
echo "branches=[]" >> $GITHUB_OUTPUT | |
echo "No security scan branches to scan - all have successful scans from previous day" | |
fi | |
- name: Determine global dependencies branches for scheduled runs | |
id: determine-scheduled-global-dependencies-branches | |
if: github.event_name != 'pull_request' | |
run: | | |
upstream_branches="${{ steps.get-upstream-branches.outputs.upstream-branches }}" | |
successful_branches="${{ steps.check-scan-artifacts.outputs.successful-global-dependencies-branches }}" | |
branches_to_scan="" | |
echo "Upstream branches: $upstream_branches" | |
echo "Successfully scanned global dependencies branches from previous day: $successful_branches" | |
# Check each upstream branch | |
for branch in $upstream_branches; do | |
branch=$(echo $branch | xargs) # trim whitespace | |
if [ -n "$branch" ]; then | |
# Check if this branch was successfully scanned in the previous day | |
if echo "$successful_branches" | grep -q "\b$branch\b"; then | |
echo "Skipping branch $branch - found successful global dependencies scan from previous day" | |
else | |
echo "Adding branch $branch to global dependencies scan list - no successful scan from previous day" | |
branches_to_scan="$branches_to_scan $branch" | |
fi | |
fi | |
done | |
# Clean up and convert to JSON array | |
branches_to_scan=$(echo $branches_to_scan | xargs) | |
if [ -n "$branches_to_scan" ]; then | |
json_branches=$(echo "$branches_to_scan" | tr ' ' '\n' | jq -R . | jq -s -c .) | |
echo "branches=$json_branches" >> $GITHUB_OUTPUT | |
echo "Global dependencies branches to scan: $json_branches" | |
else | |
echo "branches=[]" >> $GITHUB_OUTPUT | |
echo "No global dependencies branches to scan - all have successful scans from previous day" | |
fi | |
security-scan: | |
runs-on: ubuntu-latest | |
needs: [get-branches-to-scan] | |
if: needs.get-branches-to-scan.outputs.security-scan-branches != '[]' && needs.get-branches-to-scan.outputs.security-scan-branches != '' | |
environment: security-scanning-workflow-env | |
permissions: | |
id-token: write # Required for OIDC | |
strategy: | |
fail-fast: false | |
matrix: | |
target: [code-editor-sagemaker-server] | |
branch: ${{ fromJson(needs.get-branches-to-scan.outputs.security-scan-branches) }} | |
steps: | |
- name: Assume IAM Role | |
id: assume-aws-iam-role | |
uses: aws-actions/configure-aws-credentials@v4 | |
with: | |
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} | |
aws-region: us-east-1 | |
role-session-name: security-scan-${{ matrix.target }}-${{matrix.branch}} | |
- name: Publish Scan Invoked metric | |
run: | | |
aws cloudwatch put-metric-data \ | |
--namespace "GitHub/Workflows" \ | |
--metric-name "SecurityScanInvoked" \ | |
--dimensions "Repository=${{ github.repository }},Workflow=SecurityScan,Target=${{ matrix.target }},Branch=${{matrix.branch}}" \ | |
--value 1 | |
- name: Checkout branch | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ matrix.branch }} | |
submodules: recursive | |
- name: Update security scan script from main | |
run: | | |
# Older branches may not have the latest versions of the | |
# security scan scripts. So we download the latest one from main | |
echo "Downloading latest security-scan.sh script from main branch" | |
curl -sSL "https://raw.githubusercontent.com/${{ github.repository }}/main/scripts/security-scan.sh" -o scripts/security-scan.sh | |
echo "Updated security-scan.sh to latest version from main" | |
- name: Set up environment | |
run: | | |
echo "Installing required dependencies" | |
sudo apt-get update | |
sudo apt-get install -y quilt libkrb5-dev libx11-dev libxkbfile-dev libxml2-utils | |
- name: Run patches script | |
run: | | |
./scripts/prepare-src.sh ${{ matrix.target }} | |
- name: Set up Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: '22' | |
cache: 'npm' | |
cache-dependency-path: 'code-editor-src/package-lock.json' | |
- name: Install Code Editor Dependencies | |
run: | | |
cd code-editor-src | |
echo "Installing Dependencies" | |
npm ci | |
- name: Install Security Scan Dependencies | |
run: | | |
echo "Installing CycloneDX SBOM for npm" | |
npm i -g @cyclonedx/cyclonedx-npm | |
- name: Run Security Scan | |
run: | | |
./scripts/security-scan.sh scan-main-dependencies "${{ matrix.target }}" "${{ matrix.branch }}" | |
- name: Upload SBOM Files | |
uses: actions/upload-artifact@v4 | |
with: | |
name: sbom-files-${{ matrix.target }}-${{ matrix.branch }} | |
path: | | |
code-editor-src/*-sbom.json | |
code-editor-src/remote/*-sbom.json | |
code-editor-src/extensions/*-sbom.json | |
code-editor-src/remote/web/*-sbom.json | |
retention-days: 90 | |
if-no-files-found: error | |
- name: Upload Scan Result Files | |
uses: actions/upload-artifact@v4 | |
with: | |
name: scan-results-${{ matrix.target }}-${{ matrix.branch }} | |
path: | | |
code-editor-src/*-scan-result.json | |
code-editor-src/remote/*-scan-result.json | |
code-editor-src/extensions/*-scan-result.json | |
code-editor-src/remote/web/*-scan-result.json | |
retention-days: 90 | |
if-no-files-found: error | |
- name: Analyze SBOM Scan Results | |
run: | | |
./scripts/security-scan.sh analyze-results "${{ matrix.target }}" "scan_results_paths.txt" | |
- name: Create Success Indicator File | |
run: | | |
# For PR events, use base_ref as output branch name, otherwise use actual branch | |
if [ "${{ github.event_name }}" = "pull_request" ]; then | |
output_branch="${{ needs.get-branches-to-scan.outputs.output-branch-name }}" | |
else | |
output_branch="${{ matrix.branch }}" | |
fi | |
echo "PASS" > scan-success-${{ matrix.target }}-${output_branch}.txt | |
- name: Upload Success Indicator File | |
uses: actions/upload-artifact@v4 | |
with: | |
name: scan-success-${{ matrix.target }}-${{ github.event_name == 'pull_request' && needs.get-branches-to-scan.outputs.output-branch-name || matrix.branch }} | |
path: scan-success-${{ matrix.target }}-${{ github.event_name == 'pull_request' && needs.get-branches-to-scan.outputs.output-branch-name || matrix.branch }}.txt | |
retention-days: 90 | |
- name: Publish Scan Successful Metric | |
run: | | |
aws cloudwatch put-metric-data \ | |
--namespace "GitHub/Workflows" \ | |
--metric-name "SecurityScanSuccessful" \ | |
--dimensions "Repository=${{ github.repository }},Workflow=SecurityScan,Target=${{ matrix.target }},Branch=${{matrix.branch}}" \ | |
--value 1 | |
- name: Publish Failure Metrics | |
if: failure() && github.event_name == 'schedule' | |
run: | | |
echo "Job failed - publishing failure metrics" | |
# Publish workflow failure metric | |
aws cloudwatch put-metric-data \ | |
--namespace "GitHub/Workflows" \ | |
--metric-name "SecurityScanFailed" \ | |
--dimensions "Repository=${{ github.repository }},Workflow=SecurityScan,Target=${{ matrix.target }},Branch=${{matrix.branch}}" \ | |
--value 1 | |
generate-security-scan-output: | |
runs-on: ubuntu-latest | |
needs: [get-branches-to-scan, security-scan] | |
if: always() && needs.get-branches-to-scan.outputs.security-scan-branches != '[]' && needs.get-branches-to-scan.outputs.security-scan-branches != '' | |
strategy: | |
fail-fast: false | |
matrix: | |
branch: ${{ fromJson(needs.get-branches-to-scan.outputs.security-scan-branches) }} | |
steps: | |
- name: Download all scan success files | |
uses: actions/download-artifact@v4 | |
with: | |
pattern: scan-success-* | |
merge-multiple: true | |
- name: Check if branch was successful for all targets | |
run: | | |
# Parse targets from environment variable | |
targets_json='${{ env.CODE_EDITOR_TARGETS }}' | |
targets=($(echo "$targets_json" | jq -r '.[]')) | |
# For PR events, use base_ref as output branch name, otherwise use actual branch | |
if [ "${{ github.event_name }}" = "pull_request" ]; then | |
check_branch="${{ needs.get-branches-to-scan.outputs.output-branch-name }}" | |
else | |
check_branch="${{ matrix.branch }}" | |
fi | |
all_success=true | |
echo "Checking success for branch: $check_branch (matrix branch: ${{ matrix.branch }})" | |
echo "Targets to check: ${targets[@]}" | |
# Check if all target success files exist for this branch | |
for target in "${targets[@]}"; do | |
success_file="scan-success-${target}-${check_branch}.txt" | |
echo "Checking for file: $success_file" | |
if [ -f "$success_file" ]; then | |
echo "✓ Found success file for target $target on branch $check_branch" | |
else | |
echo "✗ Missing success file for target $target on branch $check_branch" | |
all_success=false | |
break | |
fi | |
done | |
# Create branch success file only if all targets succeeded | |
if [ "$all_success" = true ]; then | |
echo "✓ All scans successful for branch $check_branch - creating branch success file" | |
echo "PASS" > scan-success-branch-${check_branch}.txt | |
else | |
echo "✗ Some scans failed for branch $check_branch - not creating branch success file" | |
exit 1 | |
fi | |
- name: Upload Branch Success File | |
if: success() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: scan-success-branch-${{ github.event_name == 'pull_request' && needs.get-branches-to-scan.outputs.output-branch-name || matrix.branch }} | |
path: scan-success-branch-${{ github.event_name == 'pull_request' && needs.get-branches-to-scan.outputs.output-branch-name || matrix.branch }}.txt | |
retention-days: 90 | |
security-scan-global-dependencies: | |
runs-on: ubuntu-latest | |
needs: [get-branches-to-scan] | |
if: needs.get-branches-to-scan.outputs.global-dependencies-branches != '[]' && needs.get-branches-to-scan.outputs.global-dependencies-branches != '' | |
environment: security-scanning-workflow-env | |
permissions: | |
id-token: write # Required for OIDC | |
strategy: | |
fail-fast: false | |
matrix: | |
branch: ${{ fromJson(needs.get-branches-to-scan.outputs.global-dependencies-branches) }} | |
steps: | |
- name: Assume IAM Role | |
id: assume-aws-iam-role | |
uses: aws-actions/configure-aws-credentials@v4 | |
with: | |
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} | |
aws-region: us-east-1 | |
role-session-name: security-scan-global-dependencies-${{matrix.branch}} | |
- name: Publish Scan Invoked metric | |
run: | | |
aws cloudwatch put-metric-data \ | |
--namespace "GitHub/Workflows" \ | |
--metric-name "GlobalDependenciesSecurityScanInvoked" \ | |
--dimensions "Repository=${{ github.repository }},Workflow=GlobalDependenciesSecurityScan,Branch=${{matrix.branch}}" \ | |
--value 1 | |
- name: Checkout branch | |
uses: actions/checkout@v4 | |
with: | |
ref: ${{ matrix.branch }} | |
submodules: recursive | |
- name: Update security scan script from main | |
run: | | |
# Older branches may not have the latest versions of the | |
# security scan scripts. So we download the latest one from main | |
echo "Downloading latest security-scan.sh script from main branch" | |
curl -sSL "https://raw.githubusercontent.com/${{ github.repository }}/main/scripts/security-scan.sh" -o scripts/security-scan.sh | |
echo "Updated security-scan.sh to latest version from main" | |
- name: Install Security Scan Dependencies | |
run: | | |
echo "Installing CycloneDX SBOM for npm" | |
npm i -g @cyclonedx/cyclonedx-npm | |
echo "Installing OSS Attribution Generator" | |
source .packageversionrc | |
npm i -g @electrovir/oss-attribution-generator@$oss_attribution_generator_version | |
echo "Installing semver" | |
npm i -g semver@$semver_version | |
echo "Installing Syft for SBOM generation" | |
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin | |
echo "Syft installation completed" | |
syft version | |
- name: Prepare Additional Node JS Dependencies for Scanning | |
run: | | |
./scripts/security-scan.sh scan-additional-dependencies | |
- name: Upload Additional Node.js SBOMs | |
uses: actions/upload-artifact@v4 | |
with: | |
name: additional-nodejs-sboms-${{ matrix.branch }} | |
path: additional-node-js-sboms/ | |
retention-days: 90 | |
if-no-files-found: error | |
- name: Upload Additional Inspector Scan Results | |
uses: actions/upload-artifact@v4 | |
with: | |
name: additional-inspector-results-${{ matrix.branch }} | |
path: additional-scan-results/ | |
retention-days: 90 | |
if-no-files-found: error | |
- name: Analyze Additional SBOM Scan Results | |
run: | | |
./scripts/security-scan.sh analyze-results "Global Dependencies" "additional_scan_results_paths.txt" | |
- name: Scan GitHub Security Advisories | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
./scripts/security-scan.sh scan-github-advisories | |
- name: Create Global Success Indicator File | |
run: | | |
# For PR events, use base_ref as output branch name, otherwise use actual branch | |
if [ "${{ github.event_name }}" = "pull_request" ]; then | |
output_branch="${{ needs.get-branches-to-scan.outputs.output-branch-name }}" | |
else | |
output_branch="${{ matrix.branch }}" | |
fi | |
echo "PASS" > global-scan-success-${output_branch}.txt | |
- name: Upload Global Success Indicator File | |
uses: actions/upload-artifact@v4 | |
with: | |
name: global-scan-success-${{ github.event_name == 'pull_request' && needs.get-branches-to-scan.outputs.output-branch-name || matrix.branch }} | |
path: global-scan-success-${{ github.event_name == 'pull_request' && needs.get-branches-to-scan.outputs.output-branch-name || matrix.branch }}.txt | |
retention-days: 90 | |
- name: Publish Failure Metrics | |
if: failure() && github.event_name == 'schedule' | |
run: | | |
echo "Job failed - publishing failure metrics" | |
# Publish workflow failure metric | |
aws cloudwatch put-metric-data \ | |
--namespace "GitHub/Workflows" \ | |
--metric-name "SecurityScanFailed" \ | |
--dimensions "Repository=${{ github.repository }},Workflow=GlobalDependenciesSecurityScan,Branch=${{matrix.branch}}" \ | |
--value 1 | |
handle-failures: | |
name: Handle Failures | |
runs-on: ubuntu-latest | |
needs: [get-branches-to-scan, generate-security-scan-output] | |
environment: security-scanning-workflow-env | |
if: failure() && github.event_name == 'schedule' | |
permissions: | |
id-token: write # Required for OIDC | |
env: | |
REPOSITORY: ${{ github.repository }} | |
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }} | |
steps: | |
- name: Use role credentials for metrics | |
id: aws-creds | |
uses: aws-actions/configure-aws-credentials@v4 | |
with: | |
role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }} | |
aws-region: us-east-1 | |
- name: Report failure | |
run: | | |
aws cloudwatch put-metric-data \ | |
--namespace "GitHub/Workflows" \ | |
--metric-name "ExecutionsFailed" \ | |
--dimensions "Repository=${{ env.REPOSITORY }},Workflow=SecurityScan" \ | |
--value 1 |