Skip to content

Commit a3e43f4

Browse files
feat(ci): add scheduled trivy scan with Linear ticket creation
Add a scheduled Trivy vulnerability scan that runs twice a week (Monday and Thursday at 09:00 UTC). Scans the Docker image and uv.lock dependencies for CRITICAL/HIGH vulnerabilities and automatically creates Linear tickets when findings are detected.
1 parent 918efa9 commit a3e43f4

1 file changed

Lines changed: 269 additions & 0 deletions

File tree

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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+
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
138+
echo ""
139+
fi
140+
141+
if [ "$DEPS_VULNS" -gt 0 ]; then
142+
echo "### Dependency Vulnerabilities ($DEPS_VULNS)"
143+
echo ""
144+
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
145+
echo ""
146+
fi
147+
148+
echo 'VULN_EOF'
149+
} >> "$GITHUB_OUTPUT"
150+
else
151+
echo "vuln_summary=No vulnerabilities found." >> "$GITHUB_OUTPUT"
152+
fi
153+
154+
echo "Image vulns: $IMAGE_VULNS, Deps vulns: $DEPS_VULNS, Total: $TOTAL_VULNS"
155+
156+
# ── Publish summary ──
157+
158+
- name: Publish workflow summary
159+
if: always()
160+
shell: bash
161+
run: |
162+
{
163+
echo "## Scheduled Trivy Scan -- pyatlan"
164+
echo ""
165+
echo "**Total vulnerabilities:** ${{ steps.parse.outputs.total_vulns }} (CRITICAL,HIGH)"
166+
echo ""
167+
echo "### Image Scan (pyatlan:trivy-scan)"
168+
echo ""
169+
if [ -f trivy-image.txt ]; then
170+
echo '```'
171+
cat trivy-image.txt
172+
echo '```'
173+
else
174+
echo "No image scan output."
175+
fi
176+
echo ""
177+
echo "### Dependency Scan (uv.lock)"
178+
echo ""
179+
if [ -f trivy-deps.txt ]; then
180+
echo '```'
181+
cat trivy-deps.txt
182+
echo '```'
183+
else
184+
echo "No dependency scan output."
185+
fi
186+
} >> "$GITHUB_STEP_SUMMARY"
187+
188+
# ── Create Linear ticket ──
189+
190+
- name: Create Linear ticket
191+
if: ${{ steps.parse.outputs.total_vulns != '0' }}
192+
shell: bash
193+
env:
194+
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
195+
TOTAL_VULNS: ${{ steps.parse.outputs.total_vulns }}
196+
IMAGE_VULNS: ${{ steps.parse.outputs.image_vulns }}
197+
DEPS_VULNS: ${{ steps.parse.outputs.deps_vulns }}
198+
VULN_SUMMARY: ${{ steps.parse.outputs.vuln_summary }}
199+
TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
200+
run: |
201+
REPO="${{ github.repository }}"
202+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
203+
DATE=$(date -u +%Y-%m-%d)
204+
SERVICE="pyatlan"
205+
206+
TITLE="[Security] $SERVICE — $TOTAL_VULNS CRITICAL,HIGH vulnerabilities ($DATE)"
207+
208+
DESCRIPTION="## Scheduled Trivy Scan Results
209+
210+
**Service:** $SERVICE
211+
**Repository:** [$REPO](${{ github.server_url }}/${{ github.repository }})
212+
**Scan date:** $DATE
213+
**Workflow run:** [View logs]($RUN_URL)
214+
215+
---
216+
217+
**Image vulnerabilities:** $IMAGE_VULNS
218+
**Dependency vulnerabilities:** $DEPS_VULNS
219+
**Total:** $TOTAL_VULNS
220+
221+
| Severity | Package | Installed | Fixed | CVE |
222+
|----------|---------|-----------|-------|-----|
223+
$VULN_SUMMARY
224+
225+
---
226+
227+
*This ticket was automatically created by the scheduled Trivy scan workflow.*"
228+
229+
# Build GraphQL mutation
230+
MUTATION=$(jq -n \
231+
--arg title "$TITLE" \
232+
--arg description "$DESCRIPTION" \
233+
--arg teamId "$TEAM_ID" \
234+
--argjson priority 2 \
235+
'{
236+
query: "mutation CreateIssue($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url title } } }",
237+
variables: {
238+
input: {
239+
title: $title,
240+
description: $description,
241+
teamId: $teamId,
242+
priority: $priority
243+
}
244+
}
245+
}')
246+
247+
# Call Linear API
248+
RESPONSE=$(curl -s -X POST \
249+
-H "Content-Type: application/json" \
250+
-H "Authorization: $LINEAR_API_KEY" \
251+
-d "$MUTATION" \
252+
https://api.linear.app/graphql)
253+
254+
# Check response
255+
SUCCESS=$(echo "$RESPONSE" | jq -r '.data.issueCreate.success // false')
256+
if [ "$SUCCESS" = "true" ]; then
257+
ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.identifier')
258+
ISSUE_URL=$(echo "$RESPONSE" | jq -r '.data.issueCreate.issue.url')
259+
echo "Linear ticket created: $ISSUE_ID -- $ISSUE_URL"
260+
echo "### Linear Ticket Created" >> "$GITHUB_STEP_SUMMARY"
261+
echo "**$ISSUE_ID**: [$TITLE]($ISSUE_URL)" >> "$GITHUB_STEP_SUMMARY"
262+
else
263+
echo "Failed to create Linear ticket"
264+
echo "$RESPONSE" | jq .
265+
echo "### Linear Ticket Creation Failed" >> "$GITHUB_STEP_SUMMARY"
266+
echo '```json' >> "$GITHUB_STEP_SUMMARY"
267+
echo "$RESPONSE" | jq . >> "$GITHUB_STEP_SUMMARY"
268+
echo '```' >> "$GITHUB_STEP_SUMMARY"
269+
fi

0 commit comments

Comments
 (0)