Skip to content

Commit d52cc3d

Browse files
authored
Merge pull request #810 from atlanhq/chore/add-scheduled-trivy-scan
chore(ci): add scheduled trivy scan with Linear ticket creation
2 parents d4d4f1a + 71da91d commit d52cc3d

1 file changed

Lines changed: 275 additions & 0 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
name: 'Scheduled Trivy Scan'
2+
3+
on:
4+
schedule:
5+
# Runs twice a week: Monday and Thursday at 09:00 UTC
6+
- cron: '0 9 * * 1,4'
7+
workflow_dispatch: # Allow manual trigger
8+
9+
permissions:
10+
contents: read
11+
actions: read
12+
13+
jobs:
14+
scan-and-ticket:
15+
name: Trivy Scan + Linear Ticket
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 20
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Docker Buildx
23+
uses: docker/setup-buildx-action@v3
24+
25+
- name: Log in to container registry
26+
uses: docker/login-action@v3
27+
with:
28+
registry: cgr.dev
29+
username: ${{ secrets.CHAINGUARD_USERNAME }}
30+
password: ${{ secrets.CHAINGUARD_PASSWORD }}
31+
32+
- name: Build image
33+
uses: docker/build-push-action@v6
34+
with:
35+
context: '.'
36+
file: './Dockerfile'
37+
push: false
38+
load: true
39+
tags: 'pyatlan:trivy-scan'
40+
41+
# ── Image scan ──
42+
43+
- name: Trivy image scan (JSON)
44+
uses: aquasecurity/trivy-action@0.33.1
45+
with:
46+
image-ref: 'pyatlan:trivy-scan'
47+
scanners: 'vuln'
48+
version: 'v0.69.0'
49+
ignore-unfixed: true
50+
format: 'json'
51+
output: 'trivy-image.json'
52+
severity: 'CRITICAL,HIGH'
53+
exit-code: '0'
54+
env:
55+
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2
56+
TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db:1
57+
58+
- name: Trivy image scan (table)
59+
uses: aquasecurity/trivy-action@0.33.1
60+
with:
61+
image-ref: 'pyatlan:trivy-scan'
62+
scanners: 'vuln'
63+
version: 'v0.69.0'
64+
ignore-unfixed: true
65+
format: 'table'
66+
output: 'trivy-image.txt'
67+
severity: 'CRITICAL,HIGH'
68+
exit-code: '0'
69+
env:
70+
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2
71+
TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db:1
72+
73+
# ── Dependency scan ──
74+
75+
- name: Trivy dependency scan (JSON)
76+
uses: aquasecurity/trivy-action@0.33.1
77+
with:
78+
scan-type: fs
79+
input: 'uv.lock'
80+
scanners: 'vuln'
81+
version: 'v0.69.0'
82+
ignore-unfixed: true
83+
format: 'json'
84+
output: 'trivy-deps.json'
85+
severity: 'CRITICAL,HIGH'
86+
exit-code: '0'
87+
env:
88+
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2
89+
90+
- name: Trivy dependency scan (table)
91+
uses: aquasecurity/trivy-action@0.33.1
92+
with:
93+
scan-type: fs
94+
input: 'uv.lock'
95+
scanners: 'vuln'
96+
version: 'v0.69.0'
97+
ignore-unfixed: true
98+
format: 'table'
99+
output: 'trivy-deps.txt'
100+
severity: 'CRITICAL,HIGH'
101+
exit-code: '0'
102+
env:
103+
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2
104+
105+
# ── Parse results ──
106+
107+
- name: Parse scan results
108+
id: parse
109+
shell: bash
110+
run: |
111+
# Count image vulnerabilities
112+
IMAGE_VULNS=0
113+
if [ -f trivy-image.json ]; then
114+
IMAGE_VULNS=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' trivy-image.json)
115+
fi
116+
117+
# Count dependency vulnerabilities
118+
DEPS_VULNS=0
119+
if [ -f trivy-deps.json ]; then
120+
DEPS_VULNS=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' trivy-deps.json)
121+
fi
122+
123+
TOTAL_VULNS=$((IMAGE_VULNS + DEPS_VULNS))
124+
125+
echo "image_vulns=$IMAGE_VULNS" >> "$GITHUB_OUTPUT"
126+
echo "deps_vulns=$DEPS_VULNS" >> "$GITHUB_OUTPUT"
127+
echo "total_vulns=$TOTAL_VULNS" >> "$GITHUB_OUTPUT"
128+
129+
# Build vulnerability summary for Linear ticket
130+
if [ "$TOTAL_VULNS" -gt 0 ]; then
131+
{
132+
echo 'vuln_summary<<VULN_EOF'
133+
134+
if [ "$IMAGE_VULNS" -gt 0 ]; then
135+
echo "### Docker Image Vulnerabilities ($IMAGE_VULNS)"
136+
echo ""
137+
echo "| Severity | Package | Installed | Fixed | CVE |"
138+
echo "|----------|---------|-----------|-------|-----|"
139+
jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH") | "| \(.Severity) | \(.PkgName) | \(.InstalledVersion) | \(.FixedVersion // "N/A") | \(.VulnerabilityID) |"' trivy-image.json | sort -t'|' -k2,2 | head -50
140+
echo ""
141+
fi
142+
143+
if [ "$DEPS_VULNS" -gt 0 ]; then
144+
echo "### Dependency Vulnerabilities ($DEPS_VULNS)"
145+
echo ""
146+
echo "| Severity | Package | Installed | Fixed | CVE |"
147+
echo "|----------|---------|-----------|-------|-----|"
148+
jq -r '.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH") | "| \(.Severity) | \(.PkgName) | \(.InstalledVersion) | \(.FixedVersion // "N/A") | \(.VulnerabilityID) |"' trivy-deps.json | sort -t'|' -k2,2 | head -50
149+
echo ""
150+
fi
151+
152+
echo 'VULN_EOF'
153+
} >> "$GITHUB_OUTPUT"
154+
else
155+
echo "vuln_summary=No vulnerabilities found." >> "$GITHUB_OUTPUT"
156+
fi
157+
158+
echo "Image vulns: $IMAGE_VULNS, Deps vulns: $DEPS_VULNS, Total: $TOTAL_VULNS"
159+
160+
# ── Publish summary ──
161+
162+
- name: Publish workflow summary
163+
if: always()
164+
shell: bash
165+
run: |
166+
{
167+
echo "## Scheduled Trivy Scan -- PyAtlan"
168+
echo ""
169+
echo "**Total vulnerabilities:** ${{ steps.parse.outputs.total_vulns }} (CRITICAL,HIGH)"
170+
echo ""
171+
echo "### Image Scan (pyatlan:trivy-scan)"
172+
echo ""
173+
if [ -f trivy-image.txt ]; then
174+
echo '```'
175+
cat trivy-image.txt
176+
echo '```'
177+
else
178+
echo "No image scan output."
179+
fi
180+
echo ""
181+
echo "### Dependency Scan (uv.lock)"
182+
echo ""
183+
if [ -f trivy-deps.txt ]; then
184+
echo '```'
185+
cat trivy-deps.txt
186+
echo '```'
187+
else
188+
echo "No dependency scan output."
189+
fi
190+
} >> "$GITHUB_STEP_SUMMARY"
191+
192+
# ── Create Linear ticket ──
193+
194+
- name: Create Linear ticket
195+
if: ${{ steps.parse.outputs.total_vulns != '0' }}
196+
shell: bash
197+
env:
198+
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
199+
TOTAL_VULNS: ${{ steps.parse.outputs.total_vulns }}
200+
IMAGE_VULNS: ${{ steps.parse.outputs.image_vulns }}
201+
DEPS_VULNS: ${{ steps.parse.outputs.deps_vulns }}
202+
VULN_SUMMARY: ${{ steps.parse.outputs.vuln_summary }}
203+
TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
204+
run: |
205+
REPO="${{ github.repository }}"
206+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
207+
DATE=$(date -u +%Y-%m-%d)
208+
SERVICE="PyAtlan"
209+
210+
TITLE="[Security] $SERVICE — $TOTAL_VULNS CRITICAL,HIGH vulnerabilities ($DATE)"
211+
212+
read -r -d '' DESCRIPTION << DESC_EOF || true
213+
## Scheduled Trivy Scan Results
214+
215+
**Service:** $SERVICE
216+
**Repository:** [$REPO](${{ github.server_url }}/${{ github.repository }})
217+
**Scan date:** $DATE
218+
**Workflow run:** [View logs]($RUN_URL)
219+
220+
---
221+
222+
| Metric | Count |
223+
|--------|-------|
224+
| Image vulnerabilities | $IMAGE_VULNS |
225+
| Dependency vulnerabilities | $DEPS_VULNS |
226+
| **Total** | **$TOTAL_VULNS** |
227+
228+
$VULN_SUMMARY
229+
230+
---
231+
232+
*This ticket was automatically created by the scheduled Trivy scan workflow.*
233+
DESC_EOF
234+
235+
# Build GraphQL mutation
236+
MUTATION=$(jq -n \
237+
--arg title "$TITLE" \
238+
--arg description "$DESCRIPTION" \
239+
--arg teamId "$TEAM_ID" \
240+
--argjson priority 2 \
241+
'{
242+
query: "mutation CreateIssue($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url title } } }",
243+
variables: {
244+
input: {
245+
title: $title,
246+
description: $description,
247+
teamId: $teamId,
248+
priority: $priority
249+
}
250+
}
251+
}')
252+
253+
# Call Linear API
254+
RESPONSE=$(curl -s -X POST \
255+
-H "Content-Type: application/json" \
256+
-H "Authorization: $LINEAR_API_KEY" \
257+
-d "$MUTATION" \
258+
https://api.linear.app/graphql)
259+
260+
# Check response
261+
SUCCESS=$(echo "$RESPONSE" | jq -r '.data.issueCreate.success // false')
262+
if [ "$SUCCESS" = "true" ]; then
263+
ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.identifier')
264+
ISSUE_URL=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.url')
265+
echo "Linear ticket created: $ISSUE_ID -- $ISSUE_URL"
266+
echo "### Linear Ticket Created" >> "$GITHUB_STEP_SUMMARY"
267+
echo "**$ISSUE_ID**: [$TITLE]($ISSUE_URL)" >> "$GITHUB_STEP_SUMMARY"
268+
else
269+
echo "Failed to create Linear ticket"
270+
echo "$RESPONSE" | jq .
271+
echo "### Linear Ticket Creation Failed" >> "$GITHUB_STEP_SUMMARY"
272+
echo '```json' >> "$GITHUB_STEP_SUMMARY"
273+
echo "$RESPONSE" | jq . >> "$GITHUB_STEP_SUMMARY"
274+
echo '```' >> "$GITHUB_STEP_SUMMARY"
275+
fi

0 commit comments

Comments
 (0)