diff --git a/.github/workflows/gitsecrets.yml b/.github/workflows/gitsecrets.yml index 8e18976..411a3a5 100644 --- a/.github/workflows/gitsecrets.yml +++ b/.github/workflows/gitsecrets.yml @@ -1,6 +1,11 @@ name: GitSecretsScan -on: [push, pull_request] +on: + pull_request: + branches: + - main + - '*.*' + types: [opened, reopened, synchronize] jobs: git-secret-check: diff --git a/.github/workflows/security-scan.yaml b/.github/workflows/security-scan.yaml new file mode 100644 index 0000000..ea41e2c --- /dev/null +++ b/.github/workflows/security-scan.yaml @@ -0,0 +1,566 @@ +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 \ No newline at end of file diff --git a/.packageversionrc b/.packageversionrc new file mode 100644 index 0000000..8fbe179 --- /dev/null +++ b/.packageversionrc @@ -0,0 +1,2 @@ +oss_attribution_generator_version=2.0.0 +semver_version=7.7.2 \ No newline at end of file diff --git a/scripts/generate-oss-attribution.sh b/scripts/generate-oss-attribution.sh index 87207f0..9a1c0da 100755 --- a/scripts/generate-oss-attribution.sh +++ b/scripts/generate-oss-attribution.sh @@ -92,7 +92,10 @@ generate_oss_attribution() { check_unapproved_licenses "$target" "$BUILD_SRC_DIR" fi - npx --yes --package @electrovir/oss-attribution-generator@2.0.0 -- generate-attribution --baseDir "$BUILD_SRC_DIR" --outputDir "$oss_attribution_dir" + # Read OSS attribution generator version from packageversionrc + source "$ROOT_DIR/.packageversionrc" + + npx --yes --package @electrovir/oss-attribution-generator@$oss_attribution_generator_version -- generate-attribution --baseDir "$BUILD_SRC_DIR" --outputDir "$oss_attribution_dir" attribution_licenses=$(cat "$oss_attribution_dir/attribution.txt") read_status=0 @@ -167,7 +170,10 @@ generate_unified_oss_attribution() { echo "Generating unified OSS attribution for all targets" mkdir -p "$BUILD_DIR/private/oss-attribution" - npx --yes --package @electrovir/oss-attribution-generator@2.0.0 -- generate-attribution \ + # Read OSS attribution generator version from packageversionrc + source "$ROOT_DIR/.packageversionrc" + + npx --yes --package @electrovir/oss-attribution-generator@$oss_attribution_generator_version -- generate-attribution \ -b "${target_dirs[0]}" "${target_dirs[1]}" "${target_dirs[2]}" "${target_dirs[3]}" \ --outputDir "$BUILD_DIR/private/oss-attribution" diff --git a/scripts/security-scan.sh b/scripts/security-scan.sh new file mode 100755 index 0000000..76e4cba --- /dev/null +++ b/scripts/security-scan.sh @@ -0,0 +1,487 @@ +#!/usr/bin/env bash + +set -e + +# Function to scan main application dependencies +scan_main_dependencies() { + local target="$1" + local head_ref="$2" + + echo "Security Scanning Started" + echo "Target: $target" + echo "PR Branch (code being scanned): $head_ref" + + # Define directories to scan with their specific configurations + local scan_configs=( + "code-editor-src::root" + "remote::subdir_ignore_errors" + "extensions::subdir_ignore_errors" + "remote/web::subdir_ignore_errors" + ) + local scan_results=() + + # Scan each directory + for config in "${scan_configs[@]}"; do + local dir=$(echo "$config" | cut -d':' -f1) + local scan_type=$(echo "$config" | cut -d':' -f3) + + echo "=== Scanning directory: $dir ===" + + # For the first scan (code-editor-src), we need to check the root directory + # For others, we need to check subdirectories within code-editor-src + local check_dir + if [ "$scan_type" = "root" ]; then + check_dir="$dir" + else + check_dir="code-editor-src/$dir" + fi + + # Check if directory exists and has package-lock.json + if [ ! -d "$check_dir" ]; then + echo "Error: Directory $check_dir does not exist." + exit 1 + fi + + if [ ! -f "$check_dir/package-lock.json" ]; then + echo "Error: No package-lock.json found in $check_dir." + exit 1 + fi + + # Generate SBOM for this directory + echo "Generating SBOM for $dir" + + # Create a safe filename for the SBOM + local safe_dir_name=$(echo "$dir" | sed 's/\//_/g') + local sbom_file="${safe_dir_name}-sbom.json" + local result_file="${safe_dir_name}-scan-result.json" + + # Handle different scan types + if [ "$scan_type" = "root" ]; then + # First scan: cd into code-editor-src and run scan there + echo "Scanning root directory: $dir" + cd "$dir" + cyclonedx-npm --omit dev --output-reproducible --spec-version 1.5 -o "$sbom_file" + + elif [ "$scan_type" = "subdir_ignore_errors" ]; then + # Subdirectory scans with npm error handling: cd into directory and add --ignore-npm-errors flag + # This is to ignore extraneous npm errors that don't affect the security scan + # This behaviour is same for internal scanning. + echo "Scanning subdirectory: $dir (ignoring npm errors)" + cd "$check_dir" + cyclonedx-npm --omit dev --output-reproducible --spec-version 1.5 --ignore-npm-errors -o "$sbom_file" + fi + + echo "Invoking Inspector's ScanSbom API for $dir" + aws inspector-scan scan-sbom --sbom "file://$sbom_file" > "$result_file" + + # Store the result file path for later analysis + scan_results+=("$PWD/$result_file") + + # Return to root directory for next iteration + cd - > /dev/null + + echo "Completed scan for $dir" + done + + # Store scan results paths in a file for the analyze step + printf '%s\n' "${scan_results[@]}" > scan_results_paths.txt +} + +# Function to generate SBOMs for additional dependencies +generate_additional_sboms() { + echo "Generating SBOMs for additional dependencies" + + # Store current working directory + local root_dir=$(pwd) + + # Create directory for additional SBOMs + mkdir -p additional-node-js-sboms + + # 1. Generate SBOM for @electrovir/oss-attribution-generator + echo "Generating SBOM for @electrovir/oss-attribution-generator" + + # Find the global npm modules directory + global_npm_dir=$(npm list -g | head -1) + oss_attribution_dir="$global_npm_dir/node_modules/@electrovir/oss-attribution-generator" + + echo "Found OSS attribution generator at: $oss_attribution_dir" + cd "$oss_attribution_dir" + cyclonedx-npm --omit dev --output-reproducible --spec-version 1.5 -o "$root_dir/additional-node-js-sboms/oss-attribution-generator-sbom.json" + cd - > /dev/null + echo "Generated SBOM for OSS attribution generator" + + # 2. Generate SBOM for semver package + echo "Generating SBOM for semver package" + + semver_dir="$global_npm_dir/node_modules/semver" + + echo "Found semver package at: $semver_dir" + cd "$semver_dir" + npm install + cyclonedx-npm --omit dev --output-reproducible --spec-version 1.5 -o "$root_dir/additional-node-js-sboms/semver-sbom.json" + cd - > /dev/null + echo "Generated SBOM for semver package" + + # 3. Generate SBOM for Node.js linux-x64 binary + echo "Generating SBOM for Node.js linux-x64 binary" + + # Read Node.js version from .npmrc file + NODE_VERSION=$(grep 'target=' third-party-src/remote/.npmrc | cut -d'"' -f2) + + node_x64_dir="nodejs-binaries/node-v$NODE_VERSION-linux-x64" + echo "Found Node.js x64 binary at: $node_x64_dir" + syft "$node_x64_dir" -o cyclonedx-json@1.5="$root_dir/additional-node-js-sboms/nodejs-x64-sbom.json" + echo "Generated SBOM for Node.js x64 binary" + + # 4. Generate SBOM for Node.js linux-arm64 binary + echo "Generating SBOM for Node.js linux-arm64 binary" + + node_arm64_dir="nodejs-binaries/node-v$NODE_VERSION-linux-arm64" + echo "Found Node.js ARM64 binary at: $node_arm64_dir" + syft "$node_arm64_dir" -o cyclonedx-json@1.5="$root_dir/additional-node-js-sboms/nodejs-arm64-sbom.json" + echo "Generated SBOM for Node.js ARM64 binary" + + # List generated SBOMs + echo "Generated additional SBOMs:" + ls -la additional-node-js-sboms/ + + echo "Additional SBOM generation completed successfully" +} + +# Function to scan additional SBOMs using AWS Inspector +scan_additional_sboms() { + echo "Scanning additional SBOMs with AWS Inspector" + + echo "Downloading Node.js binaries..." + download_nodejs_binaries + + echo "Generating additional SBOMs..." + generate_additional_sboms + + # Create directory for additional scan results + mkdir -p additional-scan-results + + # Check if additional SBOMs directory exists (should exist after generation) + if [ ! -d "additional-node-js-sboms" ]; then + echo "Error: additional-node-js-sboms directory not found after generation" + exit 1 + fi + + # Array to store scan result files for later analysis + local additional_scan_results=() + + # Scan each SBOM file in the additional-node-js-sboms directory + for sbom_file in additional-node-js-sboms/*.json; do + if [ ! -f "$sbom_file" ]; then + echo "Warning: No SBOM files found in additional-node-js-sboms directory" + continue + fi + + # Extract base filename without path and extension + local base_name=$(basename "$sbom_file" .json) + local result_file="additional-scan-results/${base_name}-scan-result.json" + + echo "Scanning SBOM: $sbom_file" + echo "Output will be saved to: $result_file" + + # Run AWS Inspector scan on the SBOM + aws inspector-scan scan-sbom --sbom "file://$sbom_file" > "$result_file" + + # Store the result file path for later analysis + additional_scan_results+=("$PWD/$result_file") + + echo "Completed scan for $base_name" + done + + # Store additional scan results paths in a file for the analyze step + printf '%s\n' "${additional_scan_results[@]}" > additional_scan_results_paths.txt + + echo "Additional SBOM scanning completed successfully" + echo "Scan results saved in additional-scan-results/ directory" + ls -la additional-scan-results/ +} + +# Function to download Node.js binaries for scanning +download_nodejs_binaries() { + echo "Downloading Node.js prebuilt binaries for scanning" + + # Create directory for Node.js binaries + mkdir -p nodejs-binaries + cd nodejs-binaries + + # Read Node.js version from .npmrc file + if [ -f "../third-party-src/remote/.npmrc" ]; then + NODE_VERSION=$(grep 'target=' ../third-party-src/remote/.npmrc | cut -d'"' -f2) + echo "Found Node.js version $NODE_VERSION in .npmrc" + else + echo "ERROR: Unable to determine NODE_VERSION" + exit 1 + fi + + echo "Downloading Node.js v$NODE_VERSION for linux-x64" + curl -sSL "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" -o "node-v$NODE_VERSION-linux-x64.tar.xz" + + echo "Downloading Node.js v$NODE_VERSION for linux-arm64" + curl -sSL "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-arm64.tar.xz" -o "node-v$NODE_VERSION-linux-arm64.tar.xz" + + echo "Extracting Node.js binaries" + tar -xf "node-v$NODE_VERSION-linux-x64.tar.xz" + tar -xf "node-v$NODE_VERSION-linux-arm64.tar.xz" + + echo "Node.js binaries downloaded and extracted:" + ls -la + + # Return to root directory + cd - > /dev/null + + echo "Node.js dependencies preparation completed" +} + +# Function to analyze SBOM scan results +analyze_sbom_results() { + local target="$1" + local results_file="$2" + + if [ -z "$results_file" ]; then + echo "Error: Results file path is required as second parameter" + exit 1 + fi + + if [ ! -f "$results_file" ]; then + echo "Error: Scan results paths file '$results_file' not found" + exit 1 + fi + + # Initialize totals + local total_critical=0 + local total_high=0 + local total_medium=0 + local total_other=0 + local total_low=0 + + echo "=== SBOM Security Scan Results for $target ===" + + # Process each scan result file + while IFS= read -r result_file; do + if [ ! -f "$result_file" ]; then + echo "Warning: Scan result file $result_file not found, skipping..." + continue + fi + + # Extract directory name from result file path + local dir_name=$(basename "$result_file" | sed 's/-scan-result\.json$//' | sed 's/_/\//g') + + echo "" + echo "--- Results for $dir_name ---" + + # Extract vulnerability counts from this scan result + local critical=$(jq -r '.sbom.vulnerability_count.critical // 0' "$result_file") + local high=$(jq -r '.sbom.vulnerability_count.high // 0' "$result_file") + local medium=$(jq -r '.sbom.vulnerability_count.medium // 0' "$result_file") + local other=$(jq -r '.sbom.vulnerability_count.other // 0' "$result_file") + local low=$(jq -r '.sbom.vulnerability_count.low // 0' "$result_file") + + echo "Critical: $critical, High: $high, Medium: $medium, Other: $other, Low: $low" + + # Add to totals + total_critical=$((total_critical + critical)) + total_high=$((total_high + high)) + total_medium=$((total_medium + medium)) + total_other=$((total_other + other)) + total_low=$((total_low + low)) + + # Check for concerning vulnerabilities in this directory + local dir_concerning=$((critical + high + medium + other)) + if [ $dir_concerning -gt 0 ]; then + echo "⚠️ Found $dir_concerning concerning vulnerabilities in $dir_name" + else + echo "✅ No concerning vulnerabilities in $dir_name" + fi + + done < "$results_file" + + echo "" + echo "=== TOTAL SCAN RESULTS ===" + echo "Total Critical vulnerabilities: $total_critical" + echo "Total High vulnerabilities: $total_high" + echo "Total Medium vulnerabilities: $total_medium" + echo "Total Other vulnerabilities: $total_other" + echo "Total Low vulnerabilities: $total_low" + echo "==================================================" + + # Calculate total concerning vulnerabilities (excluding low) + local total_concerning=$((total_critical + total_high + total_medium + total_other)) + + if [ $total_concerning -gt 0 ]; then + echo "❌ Security scan FAILED: Found $total_concerning concerning vulnerabilities across all directories" + echo "Critical: $total_critical, High: $total_high, Medium: $total_medium, Other: $total_other" + exit 1 + else + echo "✅ Security scan PASSED: No concerning vulnerabilities found across all directories" + echo "Total Low vulnerabilities: $total_low (acceptable)" + fi +} + +# Function to scan GitHub security advisories for microsoft/vscode +scan_github_advisories() { + echo "Scanning GitHub security advisories for microsoft/vscode" + + local repo_owner="microsoft" + local repo_name="vscode" + local vscode_version=$(jq -r '.version' third-party-src/package.json) + + echo "Found VS Code version: $vscode_version" + + echo "Fetching security advisories from GitHub API for $repo_owner/$repo_name" + + # Fetch security advisories using GitHub CLI + local temp_file=$(mktemp) + + # Make API request using gh cli with proper headers + if ! gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$repo_owner/$repo_name/security-advisories" > "$temp_file"; then + echo "Error: Failed to fetch GitHub security advisories using GitHub CLI" + echo "Make sure GitHub CLI is installed and authenticated" + rm -f "$temp_file" + exit 1 + fi + + # Check if the response is valid JSON and not an error + if ! jq empty "$temp_file" 2>/dev/null; then + echo "Error: Invalid JSON response from GitHub API" + cat "$temp_file" + rm -f "$temp_file" + exit 1 + fi + + # Count total advisories + local total_advisories=$(jq length "$temp_file") + echo "Found $total_advisories total advisories for $repo_owner/$repo_name" + + if [ "$total_advisories" -eq 0 ]; then + echo "✅ No security advisories found for microsoft/vscode" + rm -f "$temp_file" + return 0 + fi + + # Process advisories + local concerning_advisories=0 + + echo "" + echo "=== GITHUB SECURITY ADVISORIES ANALYSIS ===" + + # Process each advisory + local advisory_index=0 + local total_advisories_count=$(jq length "$temp_file") + + while [ $advisory_index -lt $total_advisories_count ]; do + local advisory=$(jq -c ".[$advisory_index]" "$temp_file") + local ghsa_id=$(echo "$advisory" | jq -r '.ghsa_id // "N/A"') + local cve_id=$(echo "$advisory" | jq -r '.cve_id // "N/A"') + local severity=$(echo "$advisory" | jq -r '.severity // "unknown"') + local summary=$(echo "$advisory" | jq -r '.summary // "No summary available"') + local published_at=$(echo "$advisory" | jq -r '.published_at // "N/A"') + + echo "" + echo "Advisory: $ghsa_id" + [ "$cve_id" != "N/A" ] && echo "CVE: $cve_id" + echo "Severity: $severity" + echo "Published: $published_at" + echo "Summary: $summary" + + local is_version_affected=false + + # Check if current version is affected using semver + local vulnerable_ranges=$(echo "$advisory" | jq -r '.vulnerabilities[].vulnerable_version_range // empty') + + if [ -n "$vulnerable_ranges" ]; then + # Process each vulnerable range + local ranges_array=() + + # Convert vulnerable ranges to array + while IFS= read -r range; do + if [ -n "$range" ]; then + ranges_array+=("$range") + fi + done <<< "$vulnerable_ranges" + + # Check each range + for vulnerable_range in "${ranges_array[@]}"; do + echo "Vulnerable versions: $vulnerable_range" + + # Use semver range to check if current version is in the vulnerable range + if semver --range "$vulnerable_range" "$vscode_version" >/dev/null 2>&1; then + echo "⚠️ Version $vscode_version is affected by this advisory (in range: $vulnerable_range)" + is_version_affected=true + else + echo "✅ Version $vscode_version is not in vulnerable range: $vulnerable_range" + fi + done + + + else + echo "⚠️ No version range specified - assuming potentially affected" + is_version_affected=true + fi + + # Count concerning advisories based on combined criteria + # Advisory is concerning if BOTH conditions are met: + # 1. Version is affected AND 2. Severity is medium/high/critical + if [ "$is_version_affected" = true ] && ([ "$severity" = "medium" ] || [ "$severity" = "high" ] || [ "$severity" = "critical" ]); then + echo "Incrementing count" + concerning_advisories=$((concerning_advisories + 1)) + fi + + advisory_index=$((advisory_index + 1)) + done + + echo "" + echo "=== GITHUB ADVISORIES SUMMARY ===" + echo "Total advisories found: $total_advisories" + echo "Concerning advisories: $concerning_advisories" + echo "==================================================" + + # Clean up temp files + rm -f "$temp_file" + + # Determine if we should fail based on concerning advisories + if [ "$concerning_advisories" -gt 0 ]; then + echo "⚠️ Found $concerning_advisories concerning GitHub security advisories for microsoft/vscode" + echo "Review the advisories above to determine if they affect your VS Code integration" + exit 1 + else + echo "✅ No concerning GitHub security advisories found for microsoft/vscode" + return 0 + fi +} + +# Main function to handle command line arguments +main() { + case "$1" in + "scan-main-dependencies") + scan_main_dependencies "$2" "$3" + ;; + "analyze-results") + analyze_sbom_results "$2" "$3" + ;; + "scan-additional-dependencies") + scan_additional_sboms + ;; + "scan-github-advisories") + scan_github_advisories + ;; + *) + echo "Usage: $0 {scan-main-dependencies|analyze-results|scan-additional-dependencies|scan-github-advisories}" + echo " scan-main-dependencies: Generate SBOMs and scan main application dependencies" + echo " analyze-results: Analyze SBOM scan results and fail if vulnerabilities found" + echo " scan-additional-dependencies: Download, generate SBOMs, and scan additional Node.js dependencies" + echo " scan-github-advisories: Scan GitHub security advisories for microsoft/vscode" + exit 1 + ;; + esac +} + +# Call main function with all arguments +main "$@" \ No newline at end of file