fix(file-service): restore configured S3 uploads #525
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: SAST Auto-Remediation | |
| # Event-driven security pipeline with two trigger paths: | |
| # | |
| # 1. Trivy (pull_request): | |
| # PR opened by human → Trivy scan → findings? → Devin webhook → auto-fix → re-scan | |
| # | |
| # 2. SonarCloud (pull_request): | |
| # PR opened by human → SonarCloud scan → quality gate fails → Devin webhook → auto-fix | |
| # | |
| # Bot-loop prevention: skips PRs authored by devin-ai-integration[bot]. | |
| # Escalation: after MAX_FIX_ATTEMPTS, opens an issue for human review. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize] | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| checks: read | |
| env: | |
| TRIVY_VERSION: "0.71.0" | |
| MAX_FIX_ATTEMPTS: 2 | |
| SEVERITY_THRESHOLD: "CRITICAL,HIGH" | |
| # ───────────────────────────────────────────────────────────── | |
| # Path 1: Trivy dependency scan (pull_request trigger) | |
| # ───────────────────────────────────────────────────────────── | |
| jobs: | |
| sast-scan: | |
| if: | | |
| github.event_name == 'pull_request' && | |
| github.event.pull_request.user.login != 'devin-ai-integration[bot]' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_findings: ${{ steps.parse.outputs.has_findings }} | |
| findings_summary: ${{ steps.parse.outputs.findings_summary }} | |
| attempt_count: ${{ steps.counter.outputs.attempt_count }} | |
| exceeded_max: ${{ steps.counter.outputs.exceeded_max }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Count prior fix attempts | |
| id: counter | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| ATTEMPT_COUNT=$(gh api \ | |
| "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/commits?per_page=100" \ | |
| --jq '[.[] | select(.author.login == "devin-ai-integration[bot]")] | length') | |
| echo "attempt_count=${ATTEMPT_COUNT}" >> "$GITHUB_OUTPUT" | |
| if [ "$ATTEMPT_COUNT" -ge "${{ env.MAX_FIX_ATTEMPTS }}" ]; then | |
| echo "exceeded_max=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exceeded_max=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Install Trivy | |
| run: | | |
| curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ | |
| | sh -s -- -b /usr/local/bin v${{ env.TRIVY_VERSION }} | |
| - name: Run Trivy filesystem scan | |
| run: | | |
| trivy fs . \ | |
| --severity "${{ env.SEVERITY_THRESHOLD }}" \ | |
| --format json \ | |
| --output trivy-results.json \ | |
| --skip-dirs services/report-service \ | |
| --ignorefile .trivyignore \ | |
| || true | |
| - name: Parse findings | |
| id: parse | |
| run: | | |
| VULN_COUNT=$(jq '[.Results[]? | .Vulnerabilities // [] | length] | add // 0' trivy-results.json) | |
| echo "Total HIGH/CRITICAL findings: $VULN_COUNT" | |
| if [ "$VULN_COUNT" -eq 0 ]; then | |
| echo "has_findings=false" >> "$GITHUB_OUTPUT" | |
| echo "findings_summary=No HIGH or CRITICAL vulnerabilities found." >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "has_findings=true" >> "$GITHUB_OUTPUT" | |
| # Build a compact summary (service → CVE list) for the PR comment | |
| # and the Devin prompt. Truncate to 3000 chars for GHA output limits. | |
| SUMMARY=$(jq -r ' | |
| [.Results[] | | |
| select((.Vulnerabilities // []) | length > 0) | | |
| { | |
| target: .Target, | |
| vulns: [.Vulnerabilities[] | { | |
| id: .VulnerabilityID, | |
| pkg: .PkgName, | |
| installed: .InstalledVersion, | |
| fixed: (.FixedVersion // "n/a"), | |
| severity: .Severity, | |
| title: .Title | |
| }] | |
| } | |
| ] | | |
| map("### \(.target)\n" + | |
| (.vulns | map("- **\(.severity)** `\(.id)`: \(.pkg) \(.installed) → \(.fixed) — \(.title)") | join("\n")) | |
| ) | join("\n\n") | |
| ' trivy-results.json | head -c 3000) | |
| EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) | |
| echo "findings_summary<<$EOF" >> "$GITHUB_OUTPUT" | |
| echo "$SUMMARY" >> "$GITHUB_OUTPUT" | |
| echo "$EOF" >> "$GITHUB_OUTPUT" | |
| - name: Upload scan artifact | |
| if: steps.parse.outputs.has_findings == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: trivy-results | |
| path: trivy-results.json | |
| retention-days: 7 | |
| comment-findings: | |
| needs: sast-scan | |
| if: needs.sast-scan.outputs.has_findings == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Post findings to PR | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FINDINGS_SUMMARY: ${{ needs.sast-scan.outputs.findings_summary }} | |
| ATTEMPT_COUNT: ${{ needs.sast-scan.outputs.attempt_count }} | |
| MAX_ATTEMPTS: ${{ env.MAX_FIX_ATTEMPTS }} | |
| PR_NUM: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| BODY="## :shield: SAST Scan — Findings Detected | |
| Trivy found **HIGH/CRITICAL** vulnerabilities on this PR. | |
| ${FINDINGS_SUMMARY} | |
| --- | |
| **Remediation attempt:** ${ATTEMPT_COUNT} / ${MAX_ATTEMPTS}" | |
| gh api \ | |
| "repos/${REPO}/issues/${PR_NUM}/comments" \ | |
| -f body="$BODY" | |
| trigger-devin: | |
| needs: sast-scan | |
| if: | | |
| needs.sast-scan.outputs.has_findings == 'true' && | |
| needs.sast-scan.outputs.exceeded_max == 'false' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Trigger Devin remediation via webhook (Trivy findings) | |
| env: | |
| DEVIN_WEBHOOK_SECRET: ${{ secrets.DEVIN_WEBHOOK_SECRET }} | |
| BRANCH: ${{ github.head_ref }} | |
| PR_NUM: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| FINDINGS_SUMMARY: ${{ needs.sast-scan.outputs.findings_summary }} | |
| run: | | |
| WEBHOOK_URL="https://partner-workshops.devinenterprise.com/api/webhooks/automations/org-9d35f03b68c64253b53de2a8ea69a691/auto-ea3b9981a9c84d6ba476ec498b40ced5" | |
| REQUEST_BODY=$(jq -n \ | |
| --arg branch "$BRANCH" \ | |
| --arg pr_number "$PR_NUM" \ | |
| --arg repo "$REPO" \ | |
| --arg findings "$FINDINGS_SUMMARY" \ | |
| '{ | |
| source: "trivy", | |
| branch: $branch, | |
| pr_number: $pr_number, | |
| repository: $repo, | |
| findings_summary: $findings | |
| }') | |
| HTTP_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST \ | |
| -H "X-Webhook-Secret: ${DEVIN_WEBHOOK_SECRET}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$REQUEST_BODY" \ | |
| "${WEBHOOK_URL}") | |
| HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '/^HTTP_STATUS:/d') | |
| HTTP_CODE=$(echo "$HTTP_RESPONSE" | grep '^HTTP_STATUS:' | cut -d: -f2) | |
| if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then | |
| echo "Devin automation webhook triggered successfully (HTTP ${HTTP_CODE})" | |
| SESSION_URL=$(echo "$HTTP_BODY" | jq -r '.url // empty') | |
| { | |
| echo "## Devin SAST Auto-Fix Triggered (Trivy)" | |
| echo "" | |
| echo "A Devin automation has been triggered to remediate dependency vulnerabilities." | |
| if [ -n "$SESSION_URL" ]; then | |
| echo "" | |
| echo "**Session:** ${SESSION_URL}" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "::error::Failed to trigger Devin webhook (HTTP ${HTTP_CODE})" | |
| echo "$HTTP_BODY" | |
| exit 1 | |
| fi | |
| escalate: | |
| needs: sast-scan | |
| if: | | |
| needs.sast-scan.outputs.has_findings == 'true' && | |
| needs.sast-scan.outputs.exceeded_max == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Open escalation issue | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUM: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| FINDINGS_SUMMARY: ${{ needs.sast-scan.outputs.findings_summary }} | |
| FIX_ATTEMPTS: ${{ env.MAX_FIX_ATTEMPTS }} | |
| run: | | |
| BODY="## Automated remediation exhausted | |
| Devin attempted **${FIX_ATTEMPTS}** fix cycles on | |
| PR #${PR_NUM} but HIGH/CRITICAL findings remain. | |
| ### Remaining findings | |
| ${FINDINGS_SUMMARY} | |
| ### Next steps | |
| - Review the findings manually | |
| - Check if the fixes require breaking API changes | |
| - Consider adding justified suppressions to .trivyignore | |
| with proper documentation | |
| _Opened automatically by the SAST auto-remediation pipeline._" | |
| gh api "repos/${REPO}/issues" \ | |
| -f title="[Security] Unresolved SAST findings on PR #${PR_NUM}" \ | |
| -f body="$BODY" \ | |
| -f "labels[]=security" \ | |
| -f "labels[]=needs-human-review" | |
| - name: Comment on PR about escalation | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUM: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| FIX_ATTEMPTS: ${{ env.MAX_FIX_ATTEMPTS }} | |
| run: | | |
| gh api \ | |
| "repos/${REPO}/issues/${PR_NUM}/comments" \ | |
| -f body=":rotating_light: **Escalation:** Automated remediation has been attempted ${FIX_ATTEMPTS} times without resolving all findings. A GitHub Issue has been opened for manual review." | |
| # ───────────────────────────────────────────────────────────── | |
| # Path 2: SonarCloud quality gate (CI-based analysis) | |
| # ───────────────────────────────────────────────────────────── | |
| sonarcloud-scan: | |
| name: SonarCloud Scan | |
| if: | | |
| github.event_name == 'pull_request' && | |
| github.event.pull_request.user.login != 'devin-ai-integration[bot]' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| gate_failed: ${{ steps.gate.outputs.gate_failed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: SonarCloud Scan | |
| uses: SonarSource/sonarqube-scan-action@v5 | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| args: > | |
| -Dsonar.host.url=https://sonarcloud.io | |
| -Dsonar.projectKey=Cognition-Partner-Workshops_otterworks | |
| -Dsonar.organization=cognition-partner-workshops | |
| -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} | |
| -Dsonar.pullrequest.branch=${{ github.head_ref }} | |
| -Dsonar.pullrequest.base=${{ github.base_ref }} | |
| - name: Check quality gate | |
| id: gate | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| PR_NUM: ${{ github.event.pull_request.number }} | |
| run: | | |
| # Wait for the SonarCloud compute engine task to finish before | |
| # checking the quality gate. This avoids the fail-open scenario | |
| # where the gate status is polled before analysis completes. | |
| echo "Waiting for SonarCloud compute engine task to finish..." | |
| CE_TIMED_OUT=false | |
| for i in $(seq 1 30); do | |
| CE_STATUS=$(curl -s -u "${SONAR_TOKEN}:" \ | |
| "https://sonarcloud.io/api/ce/activity?component=Cognition-Partner-Workshops_otterworks&ps=1&status=PENDING,IN_PROGRESS" \ | |
| | jq -r '.tasks | length') | |
| if [ "$CE_STATUS" = "0" ]; then | |
| echo "No pending/in-progress tasks — analysis complete." | |
| break | |
| fi | |
| echo "Attempt ${i}: compute engine still processing (${CE_STATUS} task(s))..." | |
| sleep 10 | |
| done | |
| if [ "$i" -eq 30 ] && [ "$CE_STATUS" != "0" ]; then | |
| echo "::warning::SonarCloud compute engine task did not complete within 5 minutes. Failing closed to avoid missing security findings." | |
| echo "gate_failed=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| GATE_STATUS=$(curl -s -u "${SONAR_TOKEN}:" \ | |
| "https://sonarcloud.io/api/qualitygates/project_status?projectKey=Cognition-Partner-Workshops_otterworks&pullRequest=${PR_NUM}" \ | |
| | jq -r '.projectStatus.status // "NONE"') | |
| echo "Quality gate status: ${GATE_STATUS}" | |
| if [ "$GATE_STATUS" = "ERROR" ]; then | |
| echo "gate_failed=true" >> "$GITHUB_OUTPUT" | |
| echo "Quality gate FAILED" | |
| elif [ "$GATE_STATUS" = "OK" ]; then | |
| echo "gate_failed=false" >> "$GITHUB_OUTPUT" | |
| echo "Quality gate PASSED" | |
| else | |
| echo "gate_failed=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::Quality gate status is '${GATE_STATUS}' — could not determine result. Defaulting to pass." | |
| fi | |
| sonarcloud-trigger-devin: | |
| name: Trigger Devin SAST Auto-Fix (SonarCloud) | |
| needs: sonarcloud-scan | |
| if: needs.sonarcloud-scan.outputs.gate_failed == 'true' | |
| concurrency: | |
| group: sast-sonar-fix-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check for prior remediation attempts | |
| id: pr_check | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| DEVIN_COMMENTS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \ | |
| --jq '[.[] | select(.body | contains("Devin SAST Auto-Fix"))] | length') | |
| if [ "$DEVIN_COMMENTS" -gt 0 ]; then | |
| echo "Devin has already attempted a SonarCloud fix on this PR. Skipping." | |
| echo "should_run=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "should_run=true" >> "$GITHUB_OUTPUT" | |
| - name: Trigger Devin remediation via webhook (SonarCloud findings) | |
| if: steps.pr_check.outputs.should_run == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| DEVIN_SONAR_WEBHOOK_SECRET: ${{ secrets.DEVIN_SONAR_WEBHOOK_SECRET }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_BRANCH: ${{ github.head_ref }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| WEBHOOK_URL="https://partner-workshops.devinenterprise.com/api/webhooks/automations/org-9d35f03b68c64253b53de2a8ea69a691/auto-0dacfd8d718245e686f2f03d8917ac52" | |
| REQUEST_BODY=$(jq -n \ | |
| --arg branch "$PR_BRANCH" \ | |
| --arg pr_number "$PR_NUMBER" \ | |
| --arg repo "$REPO" \ | |
| --arg dashboard "https://sonarcloud.io/dashboard?id=Cognition-Partner-Workshops_otterworks&pullRequest=${PR_NUMBER}" \ | |
| '{ | |
| source: "sonarcloud", | |
| branch: $branch, | |
| pr_number: $pr_number, | |
| repository: $repo, | |
| sonarcloud_dashboard: $dashboard | |
| }') | |
| HTTP_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" -X POST \ | |
| -H "X-Webhook-Secret: ${DEVIN_SONAR_WEBHOOK_SECRET}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$REQUEST_BODY" \ | |
| "${WEBHOOK_URL}") | |
| HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '/^HTTP_STATUS:/d') | |
| HTTP_CODE=$(echo "$HTTP_RESPONSE" | grep '^HTTP_STATUS:' | cut -d: -f2) | |
| if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then | |
| echo "Devin automation webhook triggered successfully (HTTP ${HTTP_CODE})" | |
| SESSION_URL=$(echo "$HTTP_BODY" | jq -r '.url // empty') | |
| { | |
| echo "## Devin SAST Auto-Fix Triggered (SonarCloud)" | |
| echo "" | |
| echo "SonarCloud quality gate failed on PR #${PR_NUMBER}." | |
| echo "A Devin automation has been triggered for remediation." | |
| if [ -n "$SESSION_URL" ]; then | |
| echo "" | |
| echo "**Session:** ${SESSION_URL}" | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| COMMENT="## Devin SAST Auto-Fix — Remediation In Progress | |
| SonarCloud quality gate failed on this branch. A Devin automation | |
| has been triggered for a **one-time remediation attempt**. | |
| **[SonarCloud Dashboard](https://sonarcloud.io/dashboard?id=Cognition-Partner-Workshops_otterworks&pullRequest=${PR_NUMBER})** | |
| Devin will commit the fix directly to branch \`${PR_BRANCH}\`. | |
| SonarCloud will re-analyze once the fix is pushed." | |
| gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" -f body="$COMMENT" | |
| else | |
| echo "::error::Failed to trigger Devin webhook (HTTP ${HTTP_CODE})" | |
| echo "$HTTP_BODY" | |
| exit 1 | |
| fi |