-
-
Notifications
You must be signed in to change notification settings - Fork 0
237 lines (207 loc) · 8.75 KB
/
Copy pathcodeql.yml
File metadata and controls
237 lines (207 loc) · 8.75 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
name: CodeQL - Analyze
on:
pull_request:
branches: [main, nightly, development]
push:
branches: [main, nightly, development]
workflow_dispatch:
schedule:
- cron: '0 3 * * 1' # Mondays 03:00 UTC
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
env:
GOTOOLCHAIN: local
GO_VERSION: '1.26.4'
permissions:
contents: read
security-events: write
actions: read
pull-requests: read
jobs:
analyze:
name: CodeQL analysis (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
pull-requests: read
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript-typescript' ]
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
# Use github.ref (full ref path) instead of github.ref_name:
# - push/schedule: resolves to refs/heads/<branch>, checking out latest HEAD
# - pull_request: resolves to refs/pull/<n>/merge, the correct PR merge ref
# github.ref_name fails for PRs because it yields "<n>/merge" which checkout
# interprets as a branch name (refs/heads/<n>/merge) that does not exist.
ref: ${{ github.ref }}
- name: Verify CodeQL parity guard
if: matrix.language == 'go'
run: bash scripts/ci/check-codeql-parity.sh
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
# Use CodeQL config to exclude documented false positives
# Go: Excludes go/request-forgery for url_testing.go (has 4-layer SSRF defense)
# See: .github/codeql/codeql-config.yml for full justification
config-file: ./.github/codeql/codeql-config.yml
- name: Setup Go
if: matrix.language == 'go'
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- name: Verify Go toolchain and build
if: matrix.language == 'go'
run: |
set -euo pipefail
cd backend
go version
MOD_GO_VERSION="$(awk '/^go / {print $2; exit}' go.mod)"
ACTIVE_GO_VERSION="$(go env GOVERSION | sed 's/^go//')"
case "$ACTIVE_GO_VERSION" in
"$MOD_GO_VERSION"|"$MOD_GO_VERSION".*)
;;
*)
echo "::error::Go toolchain mismatch: go.mod requires ${MOD_GO_VERSION}, active is ${ACTIVE_GO_VERSION}"
exit 1
;;
esac
go build ./...
- name: Prepare SARIF output directory
run: mkdir -p sarif-results
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
- name: Perform CodeQL Analysis
id: codeql_analyze
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
category: "/language:${{ matrix.language }}"
output: sarif-results/${{ matrix.language }}
- name: Check CodeQL Results
if: always() && steps.codeql_analyze.conclusion != 'skipped'
run: |
set -euo pipefail
SARIF_DIR="sarif-results/${{ matrix.language }}"
if [ ! -d "$SARIF_DIR" ]; then
echo "::error::Expected SARIF output directory is missing: $SARIF_DIR"
echo "❌ **ERROR:** SARIF output directory is missing: $SARIF_DIR" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
SARIF_FILE="$(find "$SARIF_DIR" -maxdepth 1 -type f -name '*.sarif' | head -n 1 || true)"
{
echo "## 🔒 CodeQL Security Analysis Results"
echo ""
echo "**Language:** ${{ matrix.language }}"
echo "**Query Suite:** security-and-quality"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
if [ -z "$SARIF_FILE" ] || [ ! -r "$SARIF_FILE" ]; then
echo "::error::Expected SARIF file is missing or unreadable: $SARIF_FILE"
echo "❌ **ERROR:** SARIF file is missing or unreadable: $SARIF_FILE" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
# shellcheck disable=SC2016
EFFECTIVE_LEVELS_JQ='[
.runs[] as $run
| $run.results[]
| . as $result
| ($run.tool.driver.rules // []) as $rules
| ((
$result.level
// (if (($result.ruleIndex | type) == "number") then ($rules[$result.ruleIndex].defaultConfiguration.level // empty) else empty end)
// ([
$rules[]?
| select((.id // "") == ($result.ruleId // ""))
| (.defaultConfiguration.level // empty)
][0] // empty)
// ""
) | ascii_downcase)
]'
echo "Found SARIF file: $SARIF_FILE"
ERROR_COUNT=$(jq -r "${EFFECTIVE_LEVELS_JQ} | map(select(. == \"error\")) | length" "$SARIF_FILE")
WARNING_COUNT=$(jq -r "${EFFECTIVE_LEVELS_JQ} | map(select(. == \"warning\")) | length" "$SARIF_FILE")
NOTE_COUNT=$(jq -r "${EFFECTIVE_LEVELS_JQ} | map(select(. == \"note\")) | length" "$SARIF_FILE")
{
echo "**Findings:**"
echo "- 🔴 Errors: $ERROR_COUNT"
echo "- 🟡 Warnings: $WARNING_COUNT"
echo "- 🔵 Notes: $NOTE_COUNT"
echo ""
if [ "$ERROR_COUNT" -gt 0 ]; then
echo "❌ **BLOCKING:** CodeQL error-level security issues found"
echo ""
echo "### Top Issues:"
echo '```'
# shellcheck disable=SC2016
jq -r '
.runs[] as $run
| $run.results[]
| . as $result
| ($run.tool.driver.rules // []) as $rules
| ((
$result.level
// (if (($result.ruleIndex | type) == "number") then ($rules[$result.ruleIndex].defaultConfiguration.level // empty) else empty end)
// ([
$rules[]?
| select((.id // "") == ($result.ruleId // ""))
| (.defaultConfiguration.level // empty)
][0] // empty)
// ""
) | ascii_downcase) as $effectiveLevel
| select($effectiveLevel == "error")
| "\($effectiveLevel): \($result.ruleId // \"<unknown-rule>\"): \($result.message.text)"
' "$SARIF_FILE" | head -5
echo '```'
else
echo "✅ No blocking CodeQL issues found"
fi
} >> "$GITHUB_STEP_SUMMARY"
{
echo ""
echo "View full results in the [Security tab](https://github.com/${{ github.repository }}/security/code-scanning)"
} >> "$GITHUB_STEP_SUMMARY"
- name: Fail on High-Severity Findings
if: always() && steps.codeql_analyze.conclusion != 'skipped'
run: |
set -euo pipefail
SARIF_DIR="sarif-results/${{ matrix.language }}"
if [ ! -d "$SARIF_DIR" ]; then
echo "::error::Expected SARIF output directory is missing: $SARIF_DIR"
exit 1
fi
SARIF_FILE="$(find "$SARIF_DIR" -maxdepth 1 -type f -name '*.sarif' | head -n 1 || true)"
if [ -z "$SARIF_FILE" ] || [ ! -r "$SARIF_FILE" ]; then
echo "::error::Expected SARIF file is missing or unreadable: $SARIF_FILE"
exit 1
fi
# shellcheck disable=SC2016
ERROR_COUNT=$(jq -r '[
.runs[] as $run
| $run.results[]
| . as $result
| ($run.tool.driver.rules // []) as $rules
| ((
$result.level
// (if (($result.ruleIndex | type) == "number") then ($rules[$result.ruleIndex].defaultConfiguration.level // empty) else empty end)
// ([
$rules[]?
| select((.id // "") == ($result.ruleId // ""))
| (.defaultConfiguration.level // empty)
][0] // empty)
// ""
) | ascii_downcase) as $effectiveLevel
| select($effectiveLevel == "error")
] | length' "$SARIF_FILE")
if [ "$ERROR_COUNT" -gt 0 ]; then
echo "::error::CodeQL found $ERROR_COUNT blocking findings (effective-level=error). Fix before merging. Policy: .github/security-severity-policy.yml"
exit 1
fi