Skip to content

fix(file-service): restore configured S3 uploads #525

fix(file-service): restore configured S3 uploads

fix(file-service): restore configured S3 uploads #525

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