diff --git a/.github/workflows/ash-security-scan.yml b/.github/workflows/ash-security-scan.yml new file mode 100644 index 000000000..b81eb0e01 --- /dev/null +++ b/.github/workflows/ash-security-scan.yml @@ -0,0 +1,317 @@ +name: ASH Security Scan with PR Comments + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + pull-requests: write + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v41 + with: + files: | + **/*.py + **/*.js + **/*.ts + **/*.java + **/*.go + **/*.rb + **/*.php + **/*.cs + **/*.cpp + **/*.c + **/*.h + **/*.yaml + **/*.yml + **/*.json + **/*.sh + **/*.dockerfile + **/Dockerfile* + **/requirements*.txt + **/package*.json + **/Pipfile* + **/pom.xml + **/build.gradle* + **/*.tf + **/*.tfvars + + - name: Set up Python + if: steps.changed-files.outputs.any_changed == 'true' + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install ASH + if: steps.changed-files.outputs.any_changed == 'true' + run: pip install git+https://github.com/awslabs/automated-security-helper.git@v3.0.0 + + - name: Create temp directory for changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + mkdir -p /tmp/ash-scan + echo "Changed files for security scan:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' + + - name: Copy changed files to temp directory + if: steps.changed-files.outputs.any_changed == 'true' + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + if [ -f "$file" ]; then + mkdir -p "/tmp/ash-scan/$(dirname "$file")" + cp "$file" "/tmp/ash-scan/$file" + echo "Copied for scan: $file" + fi + done + + - name: Run ASH scan on changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + cd /tmp/ash-scan + # Create temporary ASH config to show all findings + cat > .ash_config.yaml << 'EOF' + reporters: + markdown: + enabled: true + options: + include_detailed_findings: true + max_detailed_findings: 1000 + EOF + ash --mode container --config .ash_config.yaml 2>&1 | tee /tmp/ash-output.log + continue-on-error: true + + - name: Process scan results and create summary + if: steps.changed-files.outputs.any_changed == 'true' + id: process-results + run: | + SUMMARY_FILE="/tmp/pr_comment.md" + + if [ -f "/tmp/ash-output.log" ]; then + # Find the table boundaries + TABLE_START=$(grep -n "ASH Scan Results Summary" /tmp/ash-output.log | head -1 | cut -d: -f1 || echo "0") + TABLE_END=$(grep -n "source-dir:" /tmp/ash-output.log | head -1 | cut -d: -f1 || echo "0") + + echo "## Security Scan Results" > "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + + if [ "$TABLE_START" != "0" ] && [ "$TABLE_END" != "0" ] && [ "$TABLE_END" -gt "$TABLE_START" ]; then + # Add scan metadata and explanation + echo "### Scan Metadata" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "- **Project**: ASH" >> "$SUMMARY_FILE" + echo "- **Scan executed**: $(date -u +%Y-%m-%dT%H:%M:%S+00:00)" >> "$SUMMARY_FILE" + echo "- **ASH version**: 3.0.0" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + + echo "### Summary" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "#### Scanner Results" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "The table below shows findings by scanner, with status based on severity thresholds and dependencies:" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Column Explanations:**" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Severity Levels (S/C/H/M/L/I):**" >> "$SUMMARY_FILE" + echo "- **Suppressed (S)**: Security findings that have been explicitly suppressed/ignored and don't affect the scanner's pass/fail status" >> "$SUMMARY_FILE" + echo "- **Critical (C)**: The most severe security vulnerabilities requiring immediate remediation (e.g., SQL injection, remote code execution)" >> "$SUMMARY_FILE" + echo "- **High (H)**: Serious security vulnerabilities that should be addressed promptly (e.g., authentication bypasses, privilege escalation)" >> "$SUMMARY_FILE" + echo "- **Medium (M)**: Moderate security risks that should be addressed in normal development cycles (e.g., weak encryption, input validation issues)" >> "$SUMMARY_FILE" + echo "- **Low (L)**: Minor security concerns with limited impact (e.g., information disclosure, weak recommendations)" >> "$SUMMARY_FILE" + echo "- **Info (I)**: Informational findings for awareness with minimal security risk (e.g., code quality suggestions, best practice recommendations)" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Other Columns:**" >> "$SUMMARY_FILE" + echo "- **Time**: Duration taken by each scanner to complete its analysis" >> "$SUMMARY_FILE" + echo "- **Action**: Total number of actionable findings at or above the configured severity threshold that require attention" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Scanner Results:**" >> "$SUMMARY_FILE" + echo "- **PASSED**: Scanner found no security issues at or above the configured severity threshold - code is clean for this scanner" >> "$SUMMARY_FILE" + echo "- **FAILED**: Scanner found security vulnerabilities at or above the threshold that require attention and remediation" >> "$SUMMARY_FILE" + echo "- **MISSING**: Scanner could not run because required dependencies/tools are not installed or available" >> "$SUMMARY_FILE" + echo "- **SKIPPED**: Scanner was intentionally disabled or excluded from this scan" >> "$SUMMARY_FILE" + echo "- **ERROR**: Scanner encountered an execution error and could not complete successfully" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Severity Thresholds (Thresh Column):**" >> "$SUMMARY_FILE" + echo "- **CRITICAL**: Only Critical severity findings cause scanner to fail" >> "$SUMMARY_FILE" + echo "- **HIGH**: High and Critical severity findings cause scanner to fail" >> "$SUMMARY_FILE" + echo "- **MEDIUM (MED)**: Medium, High, and Critical severity findings cause scanner to fail" >> "$SUMMARY_FILE" + echo "- **LOW**: Low, Medium, High, and Critical severity findings cause scanner to fail" >> "$SUMMARY_FILE" + echo "- **ALL**: Any finding of any severity level causes scanner to fail" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Threshold Source:** Values in parentheses indicate where the threshold is configured:" >> "$SUMMARY_FILE" + echo "- **(g) = global**: Set in the global_settings section of ASH configuration" >> "$SUMMARY_FILE" + echo "- **(c) = config**: Set in the individual scanner configuration section" >> "$SUMMARY_FILE" + echo "- **(s) = scanner**: Default threshold built into the scanner itself" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "**Statistics calculation:**" >> "$SUMMARY_FILE" + echo "- All statistics are calculated from the final aggregated SARIF report" >> "$SUMMARY_FILE" + echo "- Suppressed findings are counted separately and do not contribute to actionable findings" >> "$SUMMARY_FILE" + echo "- Scanner status is determined by comparing actionable findings to the threshold" >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + + # Convert terminal table to markdown table format + echo "| Scanner | S | C | H | M | L | I | Time | Action | Result | Thresh |" >> "$SUMMARY_FILE" + echo "|---------|---|---|---|---|---|---|------|--------|--------|--------|" >> "$SUMMARY_FILE" + # Extract table data, strip ANSI codes, and convert to markdown + sed -n "${TABLE_START},${TABLE_END}p" /tmp/ash-output.log | \ + sed 's/\x1b\[[0-9;]*m//g' | \ + grep "^│" | \ + sed 's/│/|/g' | \ + sed 's/^ *|/|/' | \ + sed 's/| *$/|/' >> "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + + # Add detailed findings + if [ -f "/tmp/ash-scan/.ash/ash_output/reports/ash.summary.md" ]; then + grep -A 1000 "Detailed Findings" "/tmp/ash-scan/.ash/ash_output/reports/ash.summary.md" | \ + grep -v -E '^(Time since scan:|Report generated:)' | \ + grep -v 'Report generated by Automated Security Helper' >> "$SUMMARY_FILE" || true + fi + else + # Fallback to markdown report if terminal extraction fails + if [ -f "/tmp/ash-scan/.ash/ash_output/reports/ash.summary.md" ]; then + grep -v -E '^(Time since scan:|Report generated:)' "/tmp/ash-scan/.ash/ash_output/reports/ash.summary.md" | \ + grep -v 'Report generated by Automated Security Helper' > "$SUMMARY_FILE" + fi + fi + + # Check if findings were detected + if grep -q "Actionable findings detected!" /tmp/ash-output.log; then + echo "has_findings=true" >> $GITHUB_OUTPUT + else + echo "has_findings=false" >> $GITHUB_OUTPUT + fi + else + echo "## Security Scan Results" > "$SUMMARY_FILE" + echo "" >> "$SUMMARY_FILE" + echo "No security scan results found." >> "$SUMMARY_FILE" + echo "has_findings=false" >> $GITHUB_OUTPUT + fi + + + - name: Upload ASH results + if: steps.changed-files.outputs.any_changed == 'true' && always() + uses: actions/upload-artifact@v4 + with: + name: ash-security-results + path: | + /tmp/ash-scan/.ash/ + /tmp/pr_comment.md + retention-days: 30 + + - name: Add PR comment + if: steps.changed-files.outputs.any_changed == 'true' + # Always run this step even if ASH scan failed + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const commentPath = '/tmp/pr_comment.md'; + + if (fs.existsSync(commentPath)) { + const commentBody = fs.readFileSync(commentPath, 'utf8'); + const issueNumber = context.issue.number; + + // Check if we already have an ASH security scan comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + }); + + // Debug: Log all bot comments to understand what we have + const botComments = comments.filter(comment => comment.user.type === 'Bot'); + console.log(`Found ${botComments.length} bot comments`); + botComments.forEach(comment => { + console.log(`Bot comment ${comment.id}: ${comment.body.substring(0, 100)}...`); + }); + + // Find ALL ASH security scan comments + const ashComments = comments.filter(comment => + comment.user.type === 'Bot' && + (comment.body.includes('') || + comment.body.includes('## Security Scan Results') || + comment.body.includes('Latest scan for commit:') || + comment.body.includes('ASH Security Scan Report')) + ); + + console.log(`Found ${ashComments.length} ASH security scan comments`); + + // Use the most recent ASH comment (highest ID = most recent) + const botComment = ashComments.length > 0 ? + ashComments.sort((a, b) => b.id - a.id)[0] : null; + + if (botComment) { + console.log(`Will update most recent ASH comment: ${botComment.id}`); + } + + // Delete any duplicate/older ASH comments (keep only the most recent one) + if (ashComments.length > 1) { + console.log(`Cleaning up ${ashComments.length - 1} duplicate ASH comments`); + for (const comment of ashComments.slice(1)) { // Skip the first (most recent) one + try { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id, + }); + console.log(`Deleted duplicate comment ${comment.id}`); + } catch (error) { + console.log(`Failed to delete comment ${comment.id}: ${error.message}`); + } + } + } + + // Add commit and timestamp info to the body with unique identifier + const commitSha = context.payload.pull_request?.head?.sha || context.sha; + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC'; + const enhancedBody = `**Latest scan for commit:** \`${commitSha.substring(0, 7)}\` **| Updated:** ${timestamp}\n\n${commentBody}\n\n`; + + if (botComment) { + // Update existing comment with latest scan + console.log(`Updating existing comment ${botComment.id}`); + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: enhancedBody + }); + console.log('Successfully updated existing ASH security scan comment'); + } else { + // Create new comment + console.log('No existing ASH comment found, creating new one'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: enhancedBody + }); + console.log('Successfully created new ASH security scan comment'); + } + } + + - name: Security scan summary + if: steps.changed-files.outputs.any_changed == 'true' + run: | + if [ "${{ steps.process-results.outputs.has_findings }}" = "true" ]; then + echo "Security findings detected. Please review the results." + # Note: Currently configured to NOT fail the workflow on security findings + # Uncomment the next line to enforce strict security policy + # exit 1 + else + echo "No security issues found in the changed files." + fi + + - name: Skip message + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant files changed - skipping security scan" \ No newline at end of file diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 000000000..599828838 --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,25 @@ +name: Dependabot fetch metadata + +on: + pull_request: + types: [opened, synchronize] + +permissions: + pull-requests: write + issues: write + contents: read + +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + # The following properties are now available: + # - steps.metadata.outputs.dependency-names + # - steps.metadata.outputs.dependency-type + # - steps.metadata.outputs.update-type \ No newline at end of file diff --git a/.github/workflows/js-lint.yml b/.github/workflows/js-lint.yml new file mode 100644 index 000000000..6f8f85da2 --- /dev/null +++ b/.github/workflows/js-lint.yml @@ -0,0 +1,67 @@ +name: JavaScript/TypeScript Code Quality + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + js-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get changed JS/TS files + id: changed-files + uses: tj-actions/changed-files@v41 + with: + files: | + **/*.js + **/*.ts + **/*.jsx + **/*.tsx + + - name: Set up Node.js + if: steps.changed-files.outputs.any_changed == 'true' + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install ESLint and Prettier globally + if: steps.changed-files.outputs.any_changed == 'true' + run: | + npm install -g eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier + + - name: Run ESLint on changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + echo "Linting changed JS/TS files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' + + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + if [ -f "$file" ]; then + echo "Linting: $file" + npx eslint "$file" + fi + done + continue-on-error: true + + - name: Run Prettier check on changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + echo "Checking format of changed JS/TS files:" + + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + if [ -f "$file" ]; then + echo "Checking format: $file" + npx prettier --check "$file" + fi + done + continue-on-error: true + + - name: Skip message + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "⏭️ No JavaScript/TypeScript files changed - skipping JS/TS linting" \ No newline at end of file diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml new file mode 100644 index 000000000..a5d5b6ffe --- /dev/null +++ b/.github/workflows/python-lint.yml @@ -0,0 +1,53 @@ +name: Python Code Quality + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + python-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get changed Python files + id: changed-files + uses: tj-actions/changed-files@v41 + with: + files: | + **/*.py + + - name: Set up Python + if: steps.changed-files.outputs.any_changed == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + if: steps.changed-files.outputs.any_changed == 'true' + uses: astral-sh/setup-uv@v3 + + - name: Install ruff + if: steps.changed-files.outputs.any_changed == 'true' + run: uv tool install ruff + + - name: Run ruff check on changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + echo "Checking changed Python files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' + uv tool run ruff check --output-format=github ${{ steps.changed-files.outputs.all_changed_files }} + + - name: Run ruff format check on changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + echo "Checking format of changed Python files:" + uv tool run ruff format --check ${{ steps.changed-files.outputs.all_changed_files }} + + - name: Skip message + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "⏭️ No Python files changed - skipping Python linting" \ No newline at end of file