-
Notifications
You must be signed in to change notification settings - Fork 0
Create trivy workflow #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6c14a24
1ae19cc
1d698e1
206600b
2041713
0fc9e1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Git and tooling | ||
| .git | ||
| .github | ||
| .justscripts | ||
| .vscode | ||
|
|
||
| # Local environment and secrets | ||
| .venv | ||
| .env | ||
| .env.* | ||
|
|
||
| # Python caches and output | ||
| **/__pycache__ | ||
| **/*.py[cod] | ||
| .pytest_cache | ||
| .ruff_cache | ||
| *.egg-info | ||
| build | ||
| dist | ||
| .coverage | ||
| coverage.xml | ||
|
|
||
| # Not used by this image | ||
| docs | ||
| prototype | ||
| packages/cli |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| const fs = require("fs"); | ||
|
|
||
| /** | ||
| * Parse Trivy scan results and count vulnerabilities | ||
| */ | ||
| function parseScanResults(images) { | ||
| let totalCritical = 0; | ||
| let totalHigh = 0; | ||
| let totalMedium = 0; | ||
| let totalLow = 0; | ||
| const imageResults = []; | ||
|
|
||
| for (const image of images) { | ||
| try { | ||
| const results = JSON.parse( | ||
| fs.readFileSync(`trivy-${image}-results.json`, "utf8"), | ||
| ); | ||
|
|
||
| let imageCritical = 0, | ||
| imageHigh = 0, | ||
| imageMedium = 0, | ||
| imageLow = 0; | ||
|
|
||
| if (results.Results) { | ||
| for (const result of results.Results) { | ||
| if (result.Vulnerabilities) { | ||
| for (const vuln of result.Vulnerabilities) { | ||
| switch (vuln.Severity) { | ||
| case "CRITICAL": | ||
| imageCritical++; | ||
| break; | ||
| case "HIGH": | ||
| imageHigh++; | ||
| break; | ||
| case "MEDIUM": | ||
| imageMedium++; | ||
| break; | ||
| case "LOW": | ||
| imageLow++; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| totalCritical += imageCritical; | ||
| totalHigh += imageHigh; | ||
| totalMedium += imageMedium; | ||
| totalLow += imageLow; | ||
|
|
||
| imageResults.push({ | ||
| name: image, | ||
| critical: imageCritical, | ||
| high: imageHigh, | ||
| medium: imageMedium, | ||
| low: imageLow, | ||
| total: imageCritical + imageHigh + imageMedium + imageLow, | ||
| vulnerabilities: | ||
| results.Results?.flatMap((r) => r.Vulnerabilities ?? []) ?? [], | ||
| }); | ||
| } catch (error) { | ||
| console.error(`Error parsing ${image}:`, error); | ||
| imageResults.push({ | ||
| name: image, | ||
| error: true, | ||
| errorMessage: error.message, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| totalCritical, | ||
| totalHigh, | ||
| totalMedium, | ||
| totalLow, | ||
| totalVulns: totalCritical + totalHigh + totalMedium + totalLow, | ||
| imageResults, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Format results as GitHub markdown comment | ||
| */ | ||
| function formatGitHubComment(scanResults, repoOwner, repoName) { | ||
| const { | ||
| totalCritical, | ||
| totalHigh, | ||
| totalMedium, | ||
| totalLow, | ||
| totalVulns, | ||
| imageResults, | ||
| } = scanResults; | ||
|
|
||
| let message = `## 🔒 Security Scan Results\n\n`; | ||
| const errors = imageResults.filter((result) => result.error); | ||
|
|
||
| if (errors.length) { | ||
| message += `### ⚠ Scan incomplete: failed to parse ${errors.length} Trivy result(s).\n\n` | ||
| } | ||
|
Comment on lines
+96
to
+100
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added this small change. If |
||
|
|
||
| // Summary at the top | ||
| if (totalVulns > 0) { | ||
| message += `### ⚠️ Found ${totalVulns} vulnerabilities\n\n`; | ||
| message += `| Severity | Total |\n|----------|-------|\n`; | ||
| if (totalCritical > 0) message += `| 🔴 Critical | ${totalCritical} |\n`; | ||
| if (totalHigh > 0) message += `| 🟠 High | ${totalHigh} |\n`; | ||
| if (totalMedium > 0) message += `| 🟡 Medium | ${totalMedium} |\n`; | ||
| if (totalLow > 0) message += `| ⚪ Low | ${totalLow} |\n`; | ||
| message += `\n`; | ||
| } else if (!errors.length) { | ||
| message += `### ✅ No vulnerabilities found!\n\n`; | ||
| } | ||
|
|
||
| // Per-image breakdown | ||
| for (const result of imageResults) { | ||
| message += `### 📦 ${result.name}\n\n`; | ||
|
|
||
| if (result.error) { | ||
| message += `⚠️ Could not parse results for ${result.name}\n\n`; | ||
| } else if (result.total === 0) { | ||
| message += `✅ **No vulnerabilities found**\n\n`; | ||
| } else { | ||
| message += `| Severity | Count |\n|----------|-------|\n`; | ||
| if (result.critical > 0) | ||
| message += `| 🔴 Critical | ${result.critical} |\n`; | ||
| if (result.high > 0) message += `| 🟠 High | ${result.high} |\n`; | ||
| if (result.medium > 0) message += `| 🟡 Medium | ${result.medium} |\n`; | ||
| if (result.low > 0) message += `| ⚪ Low | ${result.low} |\n`; | ||
| message += `\n`; | ||
| } | ||
| } | ||
|
|
||
| message += `\n---\n`; | ||
| message += `**View detailed results**: [Security tab](https://github.com/${repoOwner}/${repoName}/security/code-scanning)\n`; | ||
| message += `*Last updated: ${new Date() | ||
| .toISOString() | ||
| .replace("T", " ") | ||
| .replace(/\.\d{3}Z$/, " UTC")}*`; | ||
|
|
||
| return message; | ||
| } | ||
|
|
||
| /** | ||
| * Post or update PR comment | ||
| */ | ||
| async function postGitHubComment(github, context, message) { | ||
| const prNumber = context.payload.pull_request?.number; | ||
|
|
||
| if (!prNumber) { | ||
| console.log("Not a PR, skipping GitHub comment"); | ||
| return; | ||
| } | ||
|
|
||
| const comments = await github.rest.issues.listComments({ | ||
| issue_number: prNumber, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| }); | ||
|
|
||
| const botComment = comments.data.find( | ||
| (comment) => | ||
| comment.user.login === "github-actions[bot]" && | ||
| comment.body.includes("Security Scan Results"), | ||
| ); | ||
|
|
||
| if (botComment) { | ||
| await github.rest.issues.updateComment({ | ||
| comment_id: botComment.id, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| body: message, | ||
| }); | ||
| console.log("Updated existing security scan comment"); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| issue_number: prNumber, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| body: message, | ||
| }); | ||
| console.log("Created new security scan comment"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * For PR scans - posts comment to PR | ||
| */ | ||
| async function generatePRSummary(github, context, core, images = [], isLocalActRun = false) { | ||
| if (!images.length) return; // no images to generate results for | ||
|
|
||
| // Parse all scan results | ||
| const scanResults = parseScanResults(images); | ||
|
|
||
| const message = formatGitHubComment( | ||
| scanResults, | ||
| context.repo.owner, | ||
| context.repo.repo, | ||
| ); | ||
|
|
||
| if (isLocalActRun) { | ||
| // log the message | ||
| core.info(message); | ||
| return; | ||
| } | ||
|
Comment on lines
+201
to
+205
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this script is run locally, only log the message, as local act runs won't be making comments to any PRs. |
||
|
|
||
| // Warn if critical or high vulnerabilities found | ||
| if (scanResults.totalCritical > 0 || scanResults.totalHigh > 0) { | ||
| core.warning( | ||
| `Found ${scanResults.totalCritical} critical and ${scanResults.totalHigh} high severity vulnerabilities`, | ||
| ); | ||
| } | ||
|
|
||
| // Post GitHub comment | ||
| await postGitHubComment(github, context, message); | ||
| } | ||
|
|
||
| module.exports = { | ||
| generatePRSummary | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| name: PR Security Scan | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| # TODO: uncomment this trigger block when we want this workflow to run on PRs | ||
| # pull_request: | ||
| # branches: | ||
| # - "**" | ||
| # merge_group: | ||
| # types: | ||
| # - checks_requested | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| env: | ||
| TRIVY_SEVERITY: "CRITICAL,HIGH,MEDIUM,LOW" | ||
|
|
||
| jobs: | ||
| build-and-scan: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| permissions: | ||
| contents: read | ||
| security-events: write | ||
|
|
||
| strategy: | ||
| matrix: | ||
| image: | ||
| - name: "did-lambda" | ||
| dockerfile: "docker/lambda.Dockerfile" | ||
| fail-fast: false | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | ||
|
|
||
| - name: Build image locally (no push) | ||
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | ||
| env: | ||
| DOCKER_BUILD_RECORD_UPLOAD: ${{ !env.ACT }} | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disable uploading the build record when running locally with |
||
| with: | ||
| context: . | ||
| file: ${{ matrix.image.dockerfile }} | ||
| load: true # Load locally for scanning only | ||
| tags: local-scan/${{ matrix.image.name }}:${{ github.sha }} | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
|
|
||
| - name: Run Trivy vulnerability scanner (SARIF) | ||
| uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 | ||
| with: | ||
| image-ref: local-scan/${{ matrix.image.name }}:${{ github.sha }} | ||
| format: "sarif" | ||
| output: "trivy-${{ matrix.image.name }}-results.sarif" | ||
| severity: ${{ env.TRIVY_SEVERITY }} | ||
| ignore-unfixed: false | ||
| timeout: "10m" | ||
|
|
||
| - name: Run Trivy vulnerability scanner (JSON for PR comment) | ||
| uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 | ||
| with: | ||
| image-ref: local-scan/${{ matrix.image.name }}:${{ github.sha }} | ||
| format: "json" | ||
| output: "trivy-${{ matrix.image.name }}-results.json" | ||
| severity: ${{ env.TRIVY_SEVERITY }} | ||
| ignore-unfixed: false | ||
| timeout: "10m" | ||
|
|
||
| - name: Display Trivy results as table | ||
| uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 | ||
| with: | ||
| image-ref: local-scan/${{ matrix.image.name }}:${{ github.sha }} | ||
| format: "table" | ||
|
|
||
| - name: Upload Trivy scan results to GitHub Security tab | ||
| uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 | ||
| if: ${{ always() && !env.ACT }} | ||
| with: | ||
| sarif_file: "trivy-${{ matrix.image.name }}-results.sarif" | ||
| category: "pr-scan-${{ matrix.image.name }}" | ||
|
|
||
| - name: Upload JSON results as artifact | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| if: ${{ always() && !env.ACT }} | ||
|
Comment on lines
+82
to
+89
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trivy uploads also disabled when running the workflow locally with Once nektos/act is updated to support new fields set by |
||
| with: | ||
| name: trivy-results-${{ matrix.image.name }} | ||
| path: trivy-${{ matrix.image.name }}-results.json | ||
| retention-days: 5 | ||
|
|
||
| - name: Generate scan summary | ||
| if: always() | ||
| run: | | ||
| echo "## Security Scan Results for ${{ matrix.image.name }}" >> $GITHUB_STEP_SUMMARY | ||
| echo "" >> $GITHUB_STEP_SUMMARY | ||
| if [ -f "trivy-${{ matrix.image.name }}-results.sarif" ]; then | ||
| echo "Vulnerability scan completed successfully" >> $GITHUB_STEP_SUMMARY | ||
| echo "Results uploaded to Security tab" >> $GITHUB_STEP_SUMMARY | ||
| echo "Category: pr-scan-${{ matrix.image.name }}" >> $GITHUB_STEP_SUMMARY | ||
| else | ||
| echo "Scan failed or no results generated" >> $GITHUB_STEP_SUMMARY | ||
| fi | ||
|
|
||
| scan-summary: | ||
| runs-on: ubuntu-latest | ||
| needs: build-and-scan | ||
| if: ${{ always() && github.event_name == 'pull_request' }} | ||
|
|
||
| env: | ||
| SCAN_IMAGES_CSV: "did-lambda" | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
|
|
||
| - name: Download scan results | ||
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||
| with: | ||
| pattern: trivy-results-* | ||
| merge-multiple: true | ||
|
|
||
| - name: PR Security Summary with Details | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| script: | | ||
| const script = require('./.github/scripts/security-summary.js'); | ||
|
|
||
| // parse `SCAN_IMAGES_CSV` to array | ||
| // ex: 'did-lambda,did-api' -> ['did-lambda', 'did-api'] | ||
| const images = (process.env.SCAN_IMAGES_CSV ?? '') | ||
| .split(',') | ||
| .map((x) => x.trim()) | ||
| .filter(Boolean); | ||
|
Comment on lines
+136
to
+141
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parses the This was changed from the original refiner script because the refiner image names were hard-coded in |
||
|
|
||
| const isLocalActRun = process.env.ACT === "true"; | ||
|
|
||
| // generate PR summary and add PR comment | ||
| await script.generatePRSummary(github, context, core, images, isLocalActRun); | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mostly copied from refiner's security-summary.js, but with only functions relevant to the scope of this ticket of work (trivy workflow).