chore(deps): bump github/codeql-action from cb4e075f119f8bccbc942d49655b2cd4dc6e615a to a899987af240c0578ed84ce13c02319a693e168f #91
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 | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - '.github/*.md' | |
| pull_request: | |
| branches: [main] | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - '.github/*.md' | |
| schedule: | |
| - cron: '0 6 * * 3' # Weekly on Wednesday 6am UTC | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false # Security scans should always complete | |
| permissions: {} | |
| jobs: | |
| # Scan dependencies for known vulnerabilities on PRs | |
| dependency-review: | |
| name: Dependency Review | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 | |
| with: | |
| egress-policy: block | |
| allowed-endpoints: > | |
| api.github.com:443 | |
| api.securityscorecards.dev:443 | |
| github.com:443 | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Dependency Review | |
| uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.7.1 | |
| with: | |
| config-file: .github/dependency-review-config.yml | |
| # Secret scanning using Gitleaks | |
| secret-scanning: | |
| name: Secret Scanning | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 | |
| with: | |
| egress-policy: block | |
| allowed-endpoints: > | |
| api.github.com:443 | |
| github.com:443 | |
| github-releases.githubusercontent.com:443 | |
| objects.githubusercontent.com:443 | |
| release-assets.githubusercontent.com:443 | |
| uploads.github.com:443 | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 # Full history for comprehensive secret scanning | |
| - name: Install Gitleaks | |
| run: | | |
| GITLEAKS_VERSION="8.24.0" | |
| GITLEAKS_CHECKSUM="cb49b7de5ee986510fe8666ca0273a6cc15eb82571f2f14832c9e8920751f3a4" | |
| curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -o gitleaks.tar.gz | |
| echo "${GITLEAKS_CHECKSUM} gitleaks.tar.gz" | sha256sum -c - | |
| tar -xzf gitleaks.tar.gz | |
| chmod +x gitleaks | |
| sudo mv gitleaks /usr/local/bin/ | |
| rm gitleaks.tar.gz | |
| - name: Run Gitleaks | |
| run: | | |
| set +e | |
| gitleaks detect --source . --report-format sarif --report-path gitleaks-results.sarif --exit-code 1 | |
| exit_code=$? | |
| set -e | |
| if [[ $exit_code -eq 1 ]]; then | |
| echo "::error::Gitleaks detected secrets in the repository" | |
| exit 1 | |
| elif [[ $exit_code -ne 0 ]]; then | |
| echo "::error::Gitleaks encountered an error (exit code: $exit_code)" | |
| exit $exit_code | |
| fi | |
| echo "No secrets detected" | |
| - name: Upload SARIF results | |
| uses: github/codeql-action/upload-sarif@34950e1b113b30df4edee1a6d3a605242df0c40b # v4 | |
| if: always() | |
| with: | |
| sarif_file: gitleaks-results.sarif | |
| category: gitleaks | |
| continue-on-error: true # Don't fail if SARIF upload fails (e.g., on forks) | |
| # Static analysis for bash scripts | |
| script-security: | |
| name: Script Security Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 | |
| with: | |
| egress-policy: block | |
| allowed-endpoints: > | |
| api.github.com:443 | |
| github.com:443 | |
| uploads.github.com:443 | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Initialize findings directory | |
| run: mkdir -p findings | |
| - name: Check for hardcoded secrets | |
| id: hardcoded-secrets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| declare -A patterns=( | |
| ["password\s*=\s*['\"][^'\"]+['\"]"]="Hardcoded password" | |
| ["api[_-]?key\s*=\s*['\"][^'\"]+['\"]"]="Hardcoded API key" | |
| ["secret\s*=\s*['\"][^'\"]+['\"]"]="Hardcoded secret" | |
| ["token\s*=\s*['\"][^'\"]+['\"]"]="Hardcoded token" | |
| ["AKIA[0-9A-Z]{16}"]="AWS Access Key ID" | |
| ["-----BEGIN\s+(RSA|DSA|EC|OPENSSH|PGP)?\s*PRIVATE KEY-----"]="Private key" | |
| ["ghp_[0-9a-zA-Z]{36}"]="GitHub personal access token" | |
| ["gho_[0-9a-zA-Z]{36}"]="GitHub OAuth access token" | |
| ["ghu_[0-9a-zA-Z]{36}"]="GitHub user-to-server token" | |
| ["ghs_[0-9a-zA-Z]{36}"]="GitHub server-to-server token" | |
| ["ghr_[0-9a-zA-Z]{36}"]="GitHub refresh token" | |
| ["github_pat_[0-9a-zA-Z]{22}_[0-9a-zA-Z]{59}"]="GitHub fine-grained PAT" | |
| ["xox[baprs]-[0-9a-zA-Z-]+"]="Slack token" | |
| ["https://[^/]*\.webhook\.office\.com/webhookb2/[a-zA-Z0-9-]+"]="Teams webhook URL" | |
| ) | |
| issues=0 | |
| findings="[]" | |
| for pattern in "${!patterns[@]}"; do | |
| description="${patterns[$pattern]}" | |
| # Search in workflow files, excluding this security workflow's pattern definitions | |
| while IFS=: read -r file line_num _; do | |
| if [[ -n "$file" && -n "$line_num" ]]; then | |
| # Skip this file's pattern definitions | |
| if [[ "$file" == ".github/workflows/security.yml" ]]; then | |
| continue | |
| fi | |
| echo "::warning file=$file,line=$line_num::$description" | |
| ((issues++)) || true | |
| # Add to findings | |
| findings=$(echo "$findings" | jq --arg file "$file" \ | |
| --arg line "$line_num" \ | |
| --arg desc "$description" \ | |
| --arg rule "hardcoded-secret" \ | |
| '. += [{ | |
| "ruleId": $rule, | |
| "level": "error", | |
| "message": {"text": $desc}, | |
| "locations": [{ | |
| "physicalLocation": { | |
| "artifactLocation": {"uri": $file}, | |
| "region": {"startLine": ($line | tonumber)} | |
| } | |
| }] | |
| }]') | |
| fi | |
| done < <(grep -rEn "$pattern" .github/workflows/ --include="*.yml" --include="*.yaml" 2>/dev/null || true) | |
| done | |
| echo "$findings" > findings/hardcoded-secrets.json | |
| echo "issues=$issues" >> "$GITHUB_OUTPUT" | |
| if [[ $issues -gt 0 ]]; then | |
| echo "::error::Found $issues potential hardcoded secret(s)" | |
| else | |
| echo "No hardcoded secrets detected" | |
| fi | |
| - name: Check for unsafe bash patterns | |
| id: unsafe-patterns | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Patterns that indicate potentially unsafe bash code | |
| # Note: eval pattern matches only when eval is a command, not a subcommand (e.g., 'yq eval' is safe) | |
| declare -A unsafe_patterns=( | |
| ['(^|[;|&(]|\$\()\s*eval\s']="eval can execute arbitrary code" | |
| ['\bcurl\s.*\|\s*(bash|sh)']="Piping curl to shell is dangerous" | |
| ['\bwget\s.*\|\s*(bash|sh)']="Piping wget to shell is dangerous" | |
| ['source\s+<\(']="Process substitution with source can be unsafe" | |
| ['\$\(\s*cat\s']="Command substitution with cat may indicate code injection" | |
| ['rm\s+-rf\s+/[^.]']="Dangerous recursive delete from root" | |
| ['chmod\s+777']="World-writable permissions are insecure" | |
| ['\bdd\s+.*of=/dev/']="Direct device writes are dangerous" | |
| ) | |
| # Allow-list: file:pattern pairs for known-safe usages | |
| declare -A allow_list=( | |
| # Add exceptions here as needed | |
| # [".github/workflows/example.yml:eval"]="1" | |
| ) | |
| issues=0 | |
| findings="[]" | |
| for pattern in "${!unsafe_patterns[@]}"; do | |
| description="${unsafe_patterns[$pattern]}" | |
| while IFS=: read -r file line_num _; do | |
| if [[ -n "$file" && -n "$line_num" ]]; then | |
| # Check allow-list | |
| allow_key="$file:$(echo "$pattern" | sed 's/\\b//g' | cut -c1-20)" | |
| if [[ -n "${allow_list[$allow_key]:-}" ]]; then | |
| continue | |
| fi | |
| # Skip this file's pattern definitions | |
| if [[ "$file" == ".github/workflows/security.yml" ]]; then | |
| continue | |
| fi | |
| echo "::error file=$file,line=$line_num::$description" | |
| ((issues++)) || true | |
| # Add to findings | |
| findings=$(echo "$findings" | jq --arg file "$file" \ | |
| --arg line "$line_num" \ | |
| --arg desc "$description" \ | |
| --arg rule "unsafe-bash-pattern" \ | |
| '. += [{ | |
| "ruleId": $rule, | |
| "level": "warning", | |
| "message": {"text": $desc}, | |
| "locations": [{ | |
| "physicalLocation": { | |
| "artifactLocation": {"uri": $file}, | |
| "region": {"startLine": ($line | tonumber)} | |
| } | |
| }] | |
| }]') | |
| fi | |
| done < <(grep -rEn "$pattern" .github/workflows/ --include="*.yml" --include="*.yaml" 2>/dev/null || true) | |
| done | |
| echo "$findings" > findings/unsafe-patterns.json | |
| echo "issues=$issues" >> "$GITHUB_OUTPUT" | |
| if [[ $issues -gt 0 ]]; then | |
| echo "::error::Found $issues potentially unsafe pattern(s)" | |
| echo "If these are intentional, add them to the allow-list in this workflow." | |
| else | |
| echo "No unsafe patterns detected" | |
| fi | |
| - name: Run ShellCheck | |
| id: shellcheck | |
| run: | | |
| set -euo pipefail | |
| echo "Running ShellCheck on shell scripts..." | |
| shopt -s nullglob globstar | |
| scripts=(./**/*.sh) | |
| if [[ ${#scripts[@]} -eq 0 ]]; then | |
| echo "No standalone shell scripts found" | |
| echo "[]" > findings/shellcheck.json | |
| echo "issues=0" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Found ${#scripts[@]} shell script(s)" | |
| # Run shellcheck with JSON output, capture results even on findings | |
| set +e | |
| shellcheck --severity=warning --format=json "${scripts[@]}" > shellcheck-raw.json 2>/dev/null | |
| set -e | |
| # Convert ShellCheck JSON to our findings format | |
| if [[ -s shellcheck-raw.json ]]; then | |
| jq '[.[] | { | |
| ruleId: ("SC" + (.code | tostring)), | |
| level: (if .level == "error" then "error" elif .level == "warning" then "warning" else "note" end), | |
| message: {text: .message}, | |
| locations: [{ | |
| physicalLocation: { | |
| artifactLocation: {uri: .file}, | |
| region: { | |
| startLine: .line, | |
| startColumn: .column, | |
| endLine: .endLine, | |
| endColumn: .endColumn | |
| } | |
| } | |
| }] | |
| }]' shellcheck-raw.json > findings/shellcheck.json | |
| issues=$(jq 'length' findings/shellcheck.json) | |
| else | |
| echo "[]" > findings/shellcheck.json | |
| issues=0 | |
| fi | |
| echo "issues=$issues" >> "$GITHUB_OUTPUT" | |
| rm -f shellcheck-raw.json | |
| if [[ $issues -gt 0 ]]; then | |
| echo "::warning::ShellCheck found $issues issue(s)" | |
| else | |
| echo "ShellCheck analysis complete - no issues" | |
| fi | |
| - name: Generate combined SARIF report | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| # Combine all findings from individual checks | |
| all_results="[]" | |
| for f in findings/*.json; do | |
| if [[ -f "$f" ]]; then | |
| all_results=$(jq -s '.[0] + .[1]' <(echo "$all_results") "$f") | |
| fi | |
| done | |
| # Generate final SARIF with all rules and results | |
| jq -n --argjson results "$all_results" '{ | |
| "$schema": "https://json.schemastore.org/sarif-2.1.0.json", | |
| "version": "2.1.0", | |
| "runs": [{ | |
| "tool": { | |
| "driver": { | |
| "name": "Bash Security Scanner", | |
| "version": "1.0.0", | |
| "informationUri": "https://github.com/koalaman/shellcheck", | |
| "rules": [ | |
| { | |
| "id": "hardcoded-secret", | |
| "name": "HardcodedSecret", | |
| "shortDescription": {"text": "Potential hardcoded secret detected"}, | |
| "defaultConfiguration": {"level": "error"}, | |
| "helpUri": "https://owasp.org/www-community/vulnerabilities/Use_of_hard-coded_password" | |
| }, | |
| { | |
| "id": "unsafe-bash-pattern", | |
| "name": "UnsafeBashPattern", | |
| "shortDescription": {"text": "Potentially unsafe bash pattern detected"}, | |
| "defaultConfiguration": {"level": "warning"} | |
| } | |
| ] | |
| } | |
| }, | |
| "results": $results | |
| }] | |
| }' > security-results.sarif | |
| echo "Generated SARIF with $(echo "$all_results" | jq 'length') finding(s)" | |
| - name: Check for failures | |
| if: always() | |
| run: | | |
| # Fail the job if any security issues were found | |
| secrets="${{ steps.hardcoded-secrets.outputs.issues }}" | |
| patterns="${{ steps.unsafe-patterns.outputs.issues }}" | |
| if [[ "${secrets:-0}" -gt 0 ]]; then | |
| echo "::error::Hardcoded secrets check failed with $secrets issue(s)" | |
| exit 1 | |
| fi | |
| if [[ "${patterns:-0}" -gt 0 ]]; then | |
| echo "::error::Unsafe patterns check failed with $patterns issue(s)" | |
| exit 1 | |
| fi | |
| echo "All security checks passed" | |
| - name: Upload SARIF results | |
| uses: github/codeql-action/upload-sarif@34950e1b113b30df4edee1a6d3a605242df0c40b # v4 | |
| if: always() | |
| with: | |
| sarif_file: security-results.sarif | |
| category: bash-security | |
| continue-on-error: true # Don't fail if SARIF upload fails (e.g., on forks) | |
| - name: Upload security report artifact | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| if: always() | |
| with: | |
| name: bash-security-report | |
| path: security-results.sarif | |
| if-no-files-found: ignore |