-
Notifications
You must be signed in to change notification settings - Fork 1
267 lines (227 loc) · 11.6 KB
/
govulncheck-github-issues.yml
File metadata and controls
267 lines (227 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
name: Govulncheck to GitHub Issues
on:
schedule:
- cron: '0 0 * * *' # Runs daily at midnight
workflow_dispatch:
jobs:
# Job 1: Load list of repositories
load-targets:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout Configuration
uses: actions/checkout@v6
- name: Load Repository List
id: set-matrix
run: |
if [ ! -f repositories.json ]; then
echo "::error::repositories.json not found."
exit 1
fi
# Read the JSON file and output it as a compact string for the matrix
MATRIX_JSON=$(jq -c . repositories.json)
echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
# Job 2: Scan repositories in parallel (Fan-Out)
scan-repository:
needs: load-targets
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
repo: ${{ fromJson(needs.load-targets.outputs.matrix) }}
steps:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: 'stable'
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Prepare Artifact Name
run: |
SAFE_NAME=$(echo "${{ matrix.repo.name }}/${{ matrix.repo.branch }}" | tr '/' '-')
echo "REPO_SAFE_NAME=$SAFE_NAME" >> $GITHUB_ENV
- name: Scan Repository and Generate Report
env:
TARGET_REPO: ${{ matrix.repo.name }}
TARGET_BRANCH: ${{ matrix.repo.branch }}
run: |
echo "------------------------------------------------------"
echo "Scanning: $TARGET_REPO (Branch: $TARGET_BRANCH)"
echo "------------------------------------------------------"
# 1. Clone the target repository on the specified branch
git clone --depth 1 --branch "$TARGET_BRANCH" "https://github.com/$TARGET_REPO.git" target-source || { echo "Failed to clone $TARGET_REPO on branch $TARGET_BRANCH"; exit 1; }
pushd target-source > /dev/null
govulncheck -format json ./... > ../govuln-stream.json || true
popd > /dev/null
# Prepare default empty report if govulncheck produced no output
if [ ! -s govuln-stream.json ]; then
echo "{\"repo\": \"$TARGET_REPO\", \"findings\": []}" > "report-${REPO_SAFE_NAME}.json"
exit 0
fi
# 2. Consolidate and Pre-process Data
# Merge streaming output into a structured list of unique vulnerabilities
jq -s '
# Create a lookup map for OSV definitions { "GO-202X-XXXX": { ... } }
([ .[] | select(.osv != null) | {key: .osv.id, value: .osv} ] | from_entries) as $osv_defs
|
# Filter findings that have a trace with a position
[ .[] | select(.finding.trace[]?.position != null) ]
# Group by Vulnerability ID to combine multiple traces for the same issue
| group_by(.finding.osv)
| map({
id: .[0].finding.osv,
# Associate the fixed version with the vulnerable dependency module
fixed_versions: ([ .[] | select(.finding.fixed_version != null and .finding.fixed_version != "") | "\(.finding.fixed_version) (module: \(.finding.trace[0].module // "unknown"))" ] | if length > 0 then unique | join(", ") else "N/A" end),
summary: ($osv_defs[.[0].finding.osv].summary // "No summary available"),
details: ($osv_defs[.[0].finding.osv].details // "No details available"),
# Extract vulnerable dependency module and version
dependencies: (map(.finding.trace[]? | select(.version != null and .version != "") | "\(.module)@\(.version)") | unique | join(", ")),
# Collect unique traces (file, line, function)
traces: (map(.finding.trace[]? | select(.position != null) | {
file: .position.filename,
line: .position.line,
func: .function
}) | unique)
})
' govuln-stream.json > vulnerabilities.json
# 3. Generate Findings Stream (Optimized Loop)
# Initialize the final findings array
echo "[]" > findings.json
# Iterate over vulnerabilities and verify file existence
jq -c '.[]' vulnerabilities.json | while read -r VULN; do
ID=$(echo "$VULN" | jq -r '.id')
FIXED_VERSIONS=$(echo "$VULN" | jq -r '.fixed_versions')
SUMMARY=$(echo "$VULN" | jq -r '.summary')
DETAILS=$(echo "$VULN" | jq -r '.details')
DEPENDENCIES=$(echo "$VULN" | jq -r '.dependencies')
if [ -z "$DEPENDENCIES" ] || [ "$DEPENDENCIES" = "null" ]; then
DEPENDENCIES="Unknown"
fi
POSITIONS=""
# Check if referenced files exist in the cloned repo
# We use a separate stream processing here to avoid subshell variable loss
POSITIONS=$(echo "$VULN" | jq -c '.traces[]' | while read -r TRACE; do
FNAME=$(echo "$TRACE" | jq -r '.file')
LINE=$(echo "$TRACE" | jq -r '.line')
FUNC=$(echo "$TRACE" | jq -r '.func')
if [ -f "target-source/$FNAME" ]; then
echo "- \`$FNAME:$LINE\` (Function: \`$FUNC\`)"
fi
done)
# If no positions were found within the repo source (e.g. only in deps), skip or mark generic
if [ -z "$POSITIONS" ]; then
POSITIONS="_No specific lines in the scanned source code were identified (vulnerability likely in dependencies)._"
fi
TITLE="Security: $ID in $TARGET_REPO/$TARGET_BRANCH"
# Construct the Markdown Body
BODY=$(cat <<EOF
### govulncheck Finding (REACHABLE)
- **Target Repository:** [$TARGET_REPO](https://github.com/$TARGET_REPO) (Branch: \`$TARGET_BRANCH\`)
- **Vulnerability ID:** [$ID](https://pkg.go.dev/vuln/$ID)
- **Vulnerable Dependency:** \`$DEPENDENCIES\`
- **Fixed In:** \`$FIXED_VERSIONS\`
### Summary
$SUMMARY
### Details
$DETAILS
### Affected Locations
$POSITIONS
---
*Note: govulncheck has confirmed that the source code in $TARGET_REPO contains a reachable call path to this vulnerability.*
*Last Updated: $(date)*
EOF
)
# Output single JSON object to stdout
jq -n --arg id "$ID" --arg title "$TITLE" --arg body "$BODY" \
'{id: $id, title: $title, body: $body}'
done > findings.jsonl
# 4. Finalize Report
# Slurp the JSONL stream into a single JSON array
jq -s --arg repo "$TARGET_REPO" --arg branch "$TARGET_BRANCH" '{repo: $repo, branch: $branch, findings: .}' findings.jsonl > "report-${REPO_SAFE_NAME}.json"
- name: Upload Findings Artifact
uses: actions/upload-artifact@v7
with:
name: findings-${{ env.REPO_SAFE_NAME }}
path: report-${{ env.REPO_SAFE_NAME }}.json
retention-days: 1
# Job 3: Report Findings (Sequential Fan-In)
report-findings:
needs: scan-repository
runs-on: ubuntu-latest
permissions:
issues: write
contents: read
steps:
- name: Download All Artifacts
uses: actions/download-artifact@v7
with:
pattern: findings-*
path: all-findings
merge-multiple: true
- name: Sync Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPORT_REPO: ${{ github.repository }}
run: |
# Ensure base labels exist
gh label create "security" --repo "$REPORT_REPO" --color "d73a4a" --description "Security related issue" --force 2>/dev/null || true
gh label create "govulncheck" --repo "$REPORT_REPO" --color "0052cc" --description "Found by govulncheck" --force 2>/dev/null || true
gh label create "vulnerability" --repo "$REPORT_REPO" --color "d93f0b" --description "A security vulnerability" --force 2>/dev/null || true
# Initialize tracking file for active issues once
> active_issue_numbers.txt
# Process all reports
for REPORT_FILE in all-findings/*.json; do
# Skip if no files match
[ -e "$REPORT_FILE" ] || continue
TARGET_REPO=$(jq -r '.repo' "$REPORT_FILE")
TARGET_BRANCH=$(jq -r '.branch' "$REPORT_FILE")
echo "======================================================"
echo "Processing findings for: $TARGET_REPO (Branch: $TARGET_BRANCH)"
echo "======================================================"
# Ensure dynamic location label exists and respects limits (name: 51 chars, description: 100 chars)
LOCATION_STR="$TARGET_REPO/$TARGET_BRANCH"
LOCATION_LABEL="${LOCATION_STR:0:51}"
DESC_STR="Vulnerabilities in $TARGET_REPO ($TARGET_BRANCH)"
DESC_TRUNC="${DESC_STR:0:100}"
gh label create "$LOCATION_LABEL" --repo "$REPORT_REPO" --color "fbca04" --description "$DESC_TRUNC" --force 2>/dev/null || true
# 1. Fetch existing issues for this Target Repo
gh issue list \
--repo "$REPORT_REPO" \
--label "govulncheck" \
--search "\"in $TARGET_REPO\" in:title" \
--state open \
--json number,title > open_issues.json
# 2. Process Findings
jq -c '.findings[]' "$REPORT_FILE" | while read -r FINDING; do
ID=$(echo "$FINDING" | jq -r '.id')
TITLE=$(echo "$FINDING" | jq -r '.title')
BODY=$(echo "$FINDING" | jq -r '.body')
EXISTING_NUMBER=$(jq -r --arg title "$TITLE" '.[] | select(.title == $title) | .number' open_issues.json | head -n 1)
if [ -n "$EXISTING_NUMBER" ] && [ "$EXISTING_NUMBER" != "null" ]; then
echo " [UPDATE] Issue #$EXISTING_NUMBER ($ID)"
gh issue edit "$EXISTING_NUMBER" --repo "$REPORT_REPO" --title "$TITLE" --body "$BODY" --add-label "security" --add-label "govulncheck" --add-label "vulnerability" --add-label "$LOCATION_LABEL"
echo "$EXISTING_NUMBER" >> active_issue_numbers.txt
else
echo " [CREATE] New Issue ($ID)"
NEW_ISSUE_URL=$(gh issue create --repo "$REPORT_REPO" --title "$TITLE" --body "$BODY" --label "security" --label "govulncheck" --label "vulnerability" --label "$LOCATION_LABEL")
NEW_ISSUE_NUM=$(basename "$NEW_ISSUE_URL")
echo "$NEW_ISSUE_NUM" >> active_issue_numbers.txt
sleep 1
fi
done
# 3. Close Stale Issues
jq -c '.[]' open_issues.json | while read -r OPEN_ISSUE; do
ISSUE_NUM=$(echo "$OPEN_ISSUE" | jq -r '.number')
ISSUE_TITLE=$(echo "$OPEN_ISSUE" | jq -r '.title')
# Ensure we only touch issues for the current repo and branch context
if [[ "$ISSUE_TITLE" != *"$TARGET_REPO/$TARGET_BRANCH"* ]]; then continue; fi
if ! grep -q "^$ISSUE_NUM$" active_issue_numbers.txt; then
echo " [CLOSE] Issue #$ISSUE_NUM (Fixed)"
gh issue close "$ISSUE_NUM" --repo "$REPORT_REPO" --comment "Auto-closing: Vulnerability no longer detected in the latest scan of $TARGET_REPO (Branch: $TARGET_BRANCH)."
sleep 1
fi
done
done