Skip to content

Commit 7511b8b

Browse files
authored
chore: add security workflows (#9)
* feat: add GitHub Actions workflow for dependency management Signed-off-by: Catarina Paralta <clouropa@cisco.com> * feat: enhance container security scan workflow with summary generation and issue creation for critical CVEs Signed-off-by: Catarina Paralta <clouropa@cisco.com> --------- Signed-off-by: Catarina Paralta <clouropa@cisco.com>
1 parent e3fcb0d commit 7511b8b

4 files changed

Lines changed: 259 additions & 20 deletions

File tree

.github/workflows/container-security-scan.yml

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ name: Container Security Scan
77
on:
88
workflow_dispatch:
99
schedule:
10-
- cron: "0 3 * * *"
10+
- cron: "0 3 * * *" # Daily at 03:00 UTC
1111

1212
permissions:
1313
contents: read
14-
security-events: write
14+
security-events: write # for uploading SARIF
1515
actions: read
16-
packages: read
16+
issues: write # create issues for critical CVEs
1717

1818
jobs:
1919
trivy-scan:
@@ -26,36 +26,72 @@ jobs:
2626
- dir-runtime-discovery
2727
- dir-runtime-server
2828
steps:
29-
- name: Checkout code
30-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
31-
32-
- name: Login to GHCR
33-
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
34-
with:
35-
registry: ghcr.io
36-
username: ${{ github.actor }}
37-
password: ${{ secrets.GITHUB_TOKEN }}
38-
3929
- name: Scan image
4030
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
4131
with:
4232
image-ref: ghcr.io/agntcy/${{ matrix.image }}:latest
4333
github-pat: ${{ secrets.GITHUB_TOKEN }}
4434
format: sarif
45-
output: trivy-results-${{ matrix.image }}.sarif
35+
output: trivy-${{ matrix.image }}.sarif
4636
vuln-type: "os,library"
4737
severity: "CRITICAL,HIGH,MEDIUM"
4838
ignore-unfixed: true
4939

40+
- name: Export image metadata
41+
run: echo "ghcr.io/agntcy/${{ matrix.image }}:latest" > trivy-${{ matrix.image }}.meta
42+
5043
- name: Upload SARIF
51-
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
44+
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
5245
with:
53-
sarif_file: trivy-results-${{ matrix.image }}.sarif
46+
sarif_file: trivy-${{ matrix.image }}.sarif
5447
category: trivy-${{ matrix.image }}
5548

56-
- name: Fail on critical vulnerabilities
49+
- name: Upload report artifacts
50+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
51+
with:
52+
name: trivy-report-${{ matrix.image }}
53+
path: |
54+
trivy-${{ matrix.image }}.sarif
55+
trivy-${{ matrix.image }}.meta
56+
retention-days: 7
57+
58+
summarize:
59+
name: Summarize Results
60+
needs: [trivy-scan]
61+
runs-on: ubuntu-latest
62+
if: always()
63+
steps:
64+
- name: Checkout
65+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
66+
67+
- name: Download artifacts
68+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
69+
with:
70+
pattern: trivy-report-*
71+
path: trivy-artifacts
72+
73+
- name: Generate summary
5774
run: |
58-
critical_count=$(grep -c '"level": "error"' trivy-results-${{ matrix.image }}.sarif || true)
59-
if [ "${critical_count}" != "0" ]; then
60-
echo "::warning::Critical vulnerabilities detected in ${{ matrix.image }} container image."
75+
chmod +x .github/workflows/scripts/security/generate_trivy_summary.sh
76+
.github/workflows/scripts/security/generate_trivy_summary.sh
77+
78+
- name: Fail if critical vulns found (optional gate)
79+
if: ${{ github.event_name != 'pull_request' }}
80+
run: |
81+
set -e
82+
found=$(grep -R "CRITICAL" -c trivy-artifacts || true)
83+
if [ "${found}" != "0" ]; then
84+
echo "Critical vulnerabilities detected. (Gate currently informational.)" >&2
6185
fi
86+
87+
- name: Create GitHub issues for critical CVEs
88+
if: ${{ github.event_name != 'pull_request' }}
89+
env:
90+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
91+
GITHUB_REPOSITORY: ${{ github.repository }}
92+
run: |
93+
set -euo pipefail
94+
echo "Installing dependencies for issue creation script";
95+
npm init -y >/dev/null 2>&1 || true
96+
npm install @octokit/rest@21 glob >/dev/null 2>&1
97+
node .github/workflows/scripts/security/create_critical_cve_issues.js
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
---
5+
name: Dependencies
6+
7+
on:
8+
workflow_dispatch:
9+
schedule:
10+
- cron: "0 */6 * * *"
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
renovate:
17+
name: Renovate Sync
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Authenticate with GitHub App Bot
21+
id: app-token
22+
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
23+
with:
24+
app-id: ${{ secrets.PROJECT_APP_ID }}
25+
private-key: ${{ secrets.PROJECT_APP_KEY }}
26+
27+
- name: Checkout
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29+
with:
30+
token: ${{ steps.app-token.outputs.token }}
31+
ref: ${{ github.head_ref }}
32+
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
33+
persist-credentials: false
34+
35+
- name: Self-hosted Renovate
36+
uses: renovatebot/github-action@f66d8679fcfcfa051abde6e7a623007173bf5164 # v46.1.12
37+
with:
38+
token: ${{ steps.app-token.outputs.token }}
39+
env:
40+
## Discovery
41+
RENOVATE_AUTODISCOVER: "true"
42+
RENOVATE_AUTODISCOVER_FILTER: "${{ github.repository }}"
43+
## Project sync
44+
RENOVATE_PLATFORM: "github"
45+
RENOVATE_PLATFORM_COMMIT: "enabled"
46+
GITHUB_COM_TOKEN: ${{ steps.app-token.outputs.token }}
47+
# Remove unused fields from PR description
48+
RENOVATE_PR_BODY_TEMPLATE: "{{{header}}}{{{table}}}{{{warnings}}}{{{notes}}}{{{changelogs}}}"
49+
RENOVATE_DEPENDENCY_DASHBOARD_HEADER: ""
50+
RENOVATE_DEPENDENCY_DASHBOARD_O_S_V_VULNERABILITY_SUMMARY: "all"
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env node
2+
// Copyright AGNTCY Contributors (https://github.com/agntcy)
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
/**
6+
* create_critical_cve_issues.js
7+
* Scans downloaded Trivy SARIF artifacts and creates GitHub issues for unique CRITICAL CVEs.
8+
*/
9+
const fs = require('fs');
10+
const glob = require('glob');
11+
const { Octokit } = require('@octokit/rest');
12+
13+
function main() {
14+
const token = process.env.GITHUB_TOKEN;
15+
if (!token) {
16+
console.error('GITHUB_TOKEN not set');
17+
process.exit(1);
18+
}
19+
const repoSlug = process.env.GITHUB_REPOSITORY;
20+
if (!repoSlug) {
21+
console.error('GITHUB_REPOSITORY not set');
22+
process.exit(1);
23+
}
24+
const [owner, repo] = repoSlug.split('/');
25+
const octokit = new Octokit({ auth: token });
26+
const sarifFiles = glob.sync('trivy-artifacts/*/trivy-*.sarif');
27+
if (sarifFiles.length === 0) {
28+
console.log('No SARIF files found');
29+
return;
30+
}
31+
const criticalFindings = [];
32+
for (const file of sarifFiles) {
33+
try {
34+
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
35+
for (const run of sarif.runs || []) {
36+
const rules = (run.tool && run.tool.driver && run.tool.driver.rules) || [];
37+
for (const result of run.results || []) {
38+
const rule = rules[result.ruleIndex];
39+
if (!rule) continue;
40+
const tags = ((rule.properties || {}).tags) || [];
41+
if (tags.includes('CRITICAL')) {
42+
const cve = rule.id || result.ruleId;
43+
const level = result.level;
44+
const message = (result.message && result.message.text) || '';
45+
const pkgMatch = message.match(/Package:\s([^\n]+)/);
46+
const fixMatch = message.match(/Fixed Version:\s([^\n]+)/);
47+
criticalFindings.push({ cve, level, package: pkgMatch ? pkgMatch[1] : 'unknown', fixed: fixMatch ? fixMatch[1] : 'unknown', file });
48+
}
49+
}
50+
}
51+
} catch (e) {
52+
console.error(`Failed to parse ${file}:`, e);
53+
}
54+
}
55+
if (criticalFindings.length === 0) {
56+
console.log('No critical CVEs detected');
57+
return;
58+
}
59+
const unique = Object.values(criticalFindings.reduce((acc, f) => { acc[f.cve] = acc[f.cve] || f; return acc; }, {}));
60+
console.log(`Unique critical CVEs: ${unique.map(u => u.cve).join(', ')}`);
61+
62+
octokit.paginate(octokit.issues.listForRepo, { owner, repo, state: 'open', per_page: 100 })
63+
.then(existing => {
64+
const existingTitles = new Set(existing.map(i => i.title));
65+
return Promise.all(unique.map(finding => {
66+
const title = `CRITICAL CVE ${finding.cve} in image scan`;
67+
if (existingTitles.has(title)) {
68+
console.log(`Issue already exists for ${finding.cve}`);
69+
return null;
70+
}
71+
const body = [
72+
'Automated security scan detected a CRITICAL vulnerability.',
73+
'',
74+
`CVE: ${finding.cve}`,
75+
`Package: ${finding.package}`,
76+
`Fixed Version: ${finding.fixed}`,
77+
`Severity Level (SARIF level): ${finding.level}`,
78+
`Source SARIF file: ${finding.file}`,
79+
'Generated by nightly container security scan workflow.',
80+
'',
81+
'Action items:',
82+
'- [ ] Assess exploitability for our deployment context',
83+
'- [ ] Upgrade to fixed version or apply mitigation',
84+
'- [ ] Verify remediation and close issue',
85+
'',
86+
'This issue was created automatically. Edit as needed.'
87+
].join('\n');
88+
return octokit.issues.create({ owner, repo, title, body, labels: ['security', 'critical', 'cve'] })
89+
.then(r => console.log(`Created issue: ${r.data.html_url}`));
90+
}));
91+
})
92+
.catch(err => {
93+
console.error('Failed to list existing issues:', err);
94+
process.exit(1);
95+
});
96+
}
97+
98+
if (require.main === module) {
99+
main();
100+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
# Copyright AGNTCY Contributors (https://github.com/agntcy)
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
set -euo pipefail
6+
# generate_trivy_summary.sh
7+
# Reads Trivy SARIF artifacts and appends a Markdown table to $GITHUB_STEP_SUMMARY.
8+
9+
if [[ -z "${GITHUB_STEP_SUMMARY:-}" ]]; then
10+
echo "GITHUB_STEP_SUMMARY not set; aborting." >&2
11+
exit 1
12+
fi
13+
14+
echo '## Container Security Scan Summary' >> "$GITHUB_STEP_SUMMARY"
15+
echo '' >> "$GITHUB_STEP_SUMMARY"
16+
17+
files=$(ls trivy-artifacts/*/trivy-*.sarif 2>/dev/null || true)
18+
if [[ -z "$files" ]]; then
19+
echo 'No SARIF files found in artifacts (check previous job logs).' >> "$GITHUB_STEP_SUMMARY"
20+
exit 0
21+
fi
22+
23+
echo '| Image | Version | Critical | High | Medium | Total | File |' >> "$GITHUB_STEP_SUMMARY"
24+
echo '|-------|---------|----------|------|--------|-------|------|' >> "$GITHUB_STEP_SUMMARY"
25+
26+
for f in $files; do
27+
img=$(basename "$f" .sarif | sed 's/^trivy-//')
28+
# Read full image reference from metadata file
29+
meta_file="${f%.sarif}.meta"
30+
if [[ -f "$meta_file" ]]; then
31+
full_ref=$(cat "$meta_file")
32+
# Extract version (after @) or tag (after last :)
33+
if [[ "$full_ref" == *"@"* ]]; then
34+
version="${full_ref##*@}"
35+
# Truncate hash to 12 characters + prefix
36+
version="${version:0:19}"
37+
elif [[ "$full_ref" == *":"* ]]; then
38+
version="${full_ref##*:}"
39+
else
40+
version="-"
41+
fi
42+
else
43+
version="-"
44+
fi
45+
critical=$(jq -r '.runs[] as $run | [ $run.results[] | select(($run.tool.driver.rules[.ruleIndex].properties.tags // []) | index("CRITICAL")) ] | length' "$f" 2>/dev/null || echo 0)
46+
high=$(jq -r '.runs[] as $run | [ $run.results[] | select(($run.tool.driver.rules[.ruleIndex].properties.tags // []) | index("HIGH")) ] | length' "$f" 2>/dev/null || echo 0)
47+
medium=$(jq -r '.runs[] as $run | [ $run.results[] | select(($run.tool.driver.rules[.ruleIndex].properties.tags // []) | index("MEDIUM")) ] | length' "$f" 2>/dev/null || echo 0)
48+
total=$(jq -r '[.runs[].results[]] | length' "$f" 2>/dev/null || echo 0)
49+
echo "| $img | $version | $critical | $high | $medium | $total | $(basename "$f") |" >> "$GITHUB_STEP_SUMMARY"
50+
done
51+
52+
echo '' >> "$GITHUB_STEP_SUMMARY"
53+
echo 'Severity counts derived from rule tags (CRITICAL/HIGH/MEDIUM) mapped via result.ruleIndex.' >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)