Skip to content

Commit a99fdc3

Browse files
authored
Add CodeQL analysis for all 5 SDK languages (#132)
* Add CodeQL workflow for all 5 SDK languages Interprocedural taint-tracking analysis across Go, TypeScript, Ruby, Swift, and Kotlin. Matrix strategy with conditional setup/build steps; Swift on macos-15, others on ubuntu-latest. Compiled languages use manual builds; interpreted languages use source extraction. Generated code is excluded via two layers: paths-ignore for source extraction (JS/Ruby) and a SARIF post-filter for compiled languages where build tracing pulls generated code into the database regardless. Analysis and upload are separated so extractor/query failures break the build while GHAS-unavailable upload failures are tolerated. * Add SARIF filter regression test Fixture-based test for the jq expression that strips generated-code alerts from SARIF output. Covers relative URIs, absolute URIs, null/missing .results, and empty/missing .locations to guard against silent regressions when the filter regex is edited. * Fix Kotlin CodeQL: disable Gradle build cache so extractor sees compilation Gradle's build cache returns FROM-CACHE artifacts, so the CodeQL build tracer never observes compilation and the extractor captures nothing. Adding --no-build-cache and clean forces a fresh compile within the traced session. * Skip Swift CodeQL on PRs that don't touch swift/ Swift CodeQL takes ~12 minutes due to full compilation under macOS build tracing. Split it out of the matrix into a separate job gated by dorny/paths-filter: PRs only run Swift analysis when swift/** files changed; push-to-main, schedule, and dispatch always run it. * Path-filter all SDK languages, not just Swift Extend dorny/paths-filter to gate all 5 languages: Go, TypeScript, Ruby, Kotlin, Swift. PRs only run CodeQL for languages whose SDK directory changed. Changes to .github/codeql/ or the workflow file itself trigger all languages. Push, schedule, and dispatch always run everything. The changes job builds a dynamic JSON matrix for the ubuntu languages and a boolean for the separate Swift job.
1 parent 224075a commit a99fdc3

File tree

4 files changed

+349
-0
lines changed

4 files changed

+349
-0
lines changed

.github/codeql/codeql-config.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
queries:
2+
- uses: security-and-quality
3+
4+
paths:
5+
- go/
6+
- typescript/
7+
- ruby/
8+
- swift/
9+
- kotlin/
10+
11+
paths-ignore:
12+
# Generated SDK code (fix in the generator or spec, not here).
13+
# Note: paths-ignore only prevents source-level extraction (JS, Ruby).
14+
# For compiled languages (Go, Swift, Kotlin), generated code enters the
15+
# database via build tracing — the workflow's SARIF filter step strips
16+
# those alerts before upload.
17+
- go/pkg/generated/
18+
- typescript/src/generated/
19+
- typescript/dist/
20+
- ruby/lib/basecamp/generated/
21+
- swift/Sources/Basecamp/Generated/
22+
- kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/
23+
# Test infrastructure
24+
- conformance/
25+
- kotlin/conformance/
26+
27+
query-filters:
28+
- exclude:
29+
kind: [diagnostic, metric]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
# Regression test for the SARIF generated-code filter used in codeql.yml.
3+
# Runs the same jq expression against a fixture and asserts the output.
4+
set -euo pipefail
5+
6+
FILTER='
7+
.runs |= map(.results |= (. // [] | map(
8+
select(
9+
(.locations // [])[0].physicalLocation.artifactLocation.uri // "" |
10+
test("(^|/)(go/pkg/generated/|typescript/(src/generated|dist)/|ruby/lib/basecamp/generated/|swift/Sources/Basecamp/Generated/|kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/)") | not
11+
)
12+
)))
13+
'
14+
15+
DIR="$(cd "$(dirname "$0")" && pwd)"
16+
FIXTURE="$DIR/testdata/sarif-filter-fixture.json"
17+
18+
actual=$(jq "$FILTER" "$FIXTURE")
19+
kept=$(echo "$actual" | jq -c '[.runs[].results[].ruleId] | sort')
20+
expected='["keep-kotlin-generator","keep-no-locations","keep-null-locations","keep-real-go","keep-swift-generator"]'
21+
22+
if [ "$kept" = "$expected" ]; then
23+
echo "PASS: SARIF filter kept correct results"
24+
else
25+
echo "FAIL: expected $expected"
26+
echo " got $kept"
27+
exit 1
28+
fi
29+
30+
# Verify null/missing .results don't blow up
31+
null_run_count=$(echo "$actual" | jq '[.runs[] | select(.results == [])] | length')
32+
if [ "$null_run_count" -eq 2 ]; then
33+
echo "PASS: null/missing .results handled"
34+
else
35+
echo "FAIL: expected 2 empty-result runs, got $null_run_count"
36+
exit 1
37+
fi
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"version": "2.1.0",
3+
"runs": [
4+
{
5+
"results": [
6+
{
7+
"ruleId": "keep-real-go",
8+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "go/pkg/client/http.go"}}}]
9+
},
10+
{
11+
"ruleId": "drop-relative-go-generated",
12+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "go/pkg/generated/api.go"}}}]
13+
},
14+
{
15+
"ruleId": "drop-absolute-go-generated",
16+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "/Users/runner/work/basecamp-sdk/basecamp-sdk/go/pkg/generated/api.go"}}}]
17+
},
18+
{
19+
"ruleId": "drop-swift-generated",
20+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "swift/Sources/Basecamp/Generated/Models.swift"}}}]
21+
},
22+
{
23+
"ruleId": "keep-swift-generator",
24+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "swift/Sources/BasecampGenerator/main.swift"}}}]
25+
},
26+
{
27+
"ruleId": "drop-kotlin-generated",
28+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "/home/runner/work/repo/kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/Api.kt"}}}]
29+
},
30+
{
31+
"ruleId": "keep-kotlin-generator",
32+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "kotlin/generator/src/main/kotlin/Main.kt"}}}]
33+
},
34+
{
35+
"ruleId": "drop-ts-dist",
36+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "typescript/dist/index.js"}}}]
37+
},
38+
{
39+
"ruleId": "drop-ts-generated",
40+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "typescript/src/generated/schema.ts"}}}]
41+
},
42+
{
43+
"ruleId": "drop-ruby-generated",
44+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": "ruby/lib/basecamp/generated/types.rb"}}}]
45+
},
46+
{
47+
"ruleId": "keep-no-locations",
48+
"locations": []
49+
},
50+
{
51+
"ruleId": "keep-null-locations"
52+
}
53+
]
54+
},
55+
{},
56+
{
57+
"results": null
58+
}
59+
]
60+
}

.github/workflows/codeql.yml

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
schedule:
8+
- cron: '0 7 * * 1' # Monday 7am UTC (security.yml at 6am)
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: read
13+
pull-requests: read
14+
security-events: write
15+
16+
jobs:
17+
changes:
18+
name: Detect changes
19+
runs-on: ubuntu-latest
20+
outputs:
21+
matrix: ${{ steps.set-matrix.outputs.matrix }}
22+
swift: ${{ steps.set-matrix.outputs.swift }}
23+
steps:
24+
- uses: dorny/paths-filter@v3
25+
if: github.event_name == 'pull_request'
26+
id: filter
27+
with:
28+
filters: |
29+
go:
30+
- 'go/**'
31+
typescript:
32+
- 'typescript/**'
33+
ruby:
34+
- 'ruby/**'
35+
kotlin:
36+
- 'kotlin/**'
37+
swift:
38+
- 'swift/**'
39+
config:
40+
- '.github/codeql/**'
41+
- '.github/workflows/codeql.yml'
42+
43+
- name: Build language matrix
44+
id: set-matrix
45+
env:
46+
EVENT: ${{ github.event_name }}
47+
GO: ${{ steps.filter.outputs.go }}
48+
TS: ${{ steps.filter.outputs.typescript }}
49+
RUBY: ${{ steps.filter.outputs.ruby }}
50+
KOTLIN: ${{ steps.filter.outputs.kotlin }}
51+
SWIFT: ${{ steps.filter.outputs.swift }}
52+
CONFIG: ${{ steps.filter.outputs.config }}
53+
run: |
54+
matrix='[]'
55+
add() { matrix=$(echo "$matrix" | jq -c --arg l "$1" --arg m "$2" '. + [{"language": $l, "build-mode": $m}]'); }
56+
57+
if [ "$EVENT" != "pull_request" ] || [ "$CONFIG" = "true" ]; then
58+
add go manual
59+
add javascript none
60+
add ruby none
61+
add java-kotlin manual
62+
swift=true
63+
else
64+
if [ "$GO" = "true" ]; then add go manual; fi
65+
if [ "$TS" = "true" ]; then add javascript none; fi
66+
if [ "$RUBY" = "true" ]; then add ruby none; fi
67+
if [ "$KOTLIN" = "true" ]; then add java-kotlin manual; fi
68+
swift=$SWIFT
69+
fi
70+
71+
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
72+
echo "swift=$swift" >> "$GITHUB_OUTPUT"
73+
74+
analyze:
75+
name: CodeQL (${{ matrix.language }})
76+
needs: changes
77+
if: ${{ needs.changes.outputs.matrix != '[]' }}
78+
runs-on: ubuntu-latest
79+
strategy:
80+
fail-fast: false
81+
matrix:
82+
include: ${{ fromJSON(needs.changes.outputs.matrix) }}
83+
84+
steps:
85+
- name: Checkout code
86+
uses: actions/checkout@v6
87+
88+
# --- Language setup (conditional) ---
89+
90+
- name: Set up Go
91+
if: matrix.language == 'go'
92+
uses: actions/setup-go@v6
93+
with:
94+
go-version-file: 'go/go.mod'
95+
96+
- name: Set up Java
97+
if: matrix.language == 'java-kotlin'
98+
uses: actions/setup-java@v5
99+
with:
100+
distribution: 'temurin'
101+
java-version: '17'
102+
103+
- name: Setup Gradle
104+
if: matrix.language == 'java-kotlin'
105+
uses: gradle/actions/setup-gradle@v5
106+
107+
# --- CodeQL init ---
108+
109+
- name: Initialize CodeQL
110+
uses: github/codeql-action/init@v4
111+
with:
112+
languages: ${{ matrix.language }}
113+
build-mode: ${{ matrix.build-mode }}
114+
config-file: ./.github/codeql/codeql-config.yml
115+
116+
# --- Build steps (manual mode only) ---
117+
118+
- name: Build (Go)
119+
if: matrix.language == 'go'
120+
working-directory: go
121+
env:
122+
CODEQL_EXTRACTOR_GO_BUILD_TRACING: 'on'
123+
run: go build ./...
124+
125+
- name: Build (Kotlin)
126+
if: matrix.language == 'java-kotlin'
127+
working-directory: kotlin
128+
run: ./gradlew --no-build-cache clean :basecamp-sdk:build :generator:build
129+
130+
# --- Analysis (fails build on real errors) ---
131+
132+
- name: Perform CodeQL Analysis
133+
uses: github/codeql-action/analyze@v4
134+
with:
135+
category: codeql-${{ matrix.language }}
136+
upload: never
137+
output: sarif-results
138+
139+
# --- Filter generated-code alerts ---
140+
# paths-ignore in codeql-config.yml only governs source extraction
141+
# (effective for JS/Ruby). For compiled languages the extractor traces
142+
# the build, so generated code enters the database. Strip those results
143+
# from SARIF before upload.
144+
145+
- name: Filter generated-code alerts from SARIF
146+
run: |
147+
for sarif in sarif-results/*.sarif; do
148+
[ -f "$sarif" ] || continue
149+
before=$(jq '[.runs[].results // [] | length] | add // 0' "$sarif")
150+
jq '
151+
.runs |= map(.results |= (. // [] | map(
152+
select(
153+
(.locations // [])[0].physicalLocation.artifactLocation.uri // "" |
154+
test("(^|/)(go/pkg/generated/|typescript/(src/generated|dist)/|ruby/lib/basecamp/generated/|swift/Sources/Basecamp/Generated/|kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/)") | not
155+
)
156+
)))
157+
' "$sarif" > "${sarif}.tmp" && mv "${sarif}.tmp" "$sarif"
158+
after=$(jq '[.runs[].results | length] | add // 0' "$sarif")
159+
filtered=$((before - after))
160+
echo "$(basename "$sarif"): $before findings, filtered $filtered generated-code alerts, $after remaining"
161+
done
162+
163+
# --- Upload (tolerates GHAS unavailability) ---
164+
165+
- name: Upload SARIF to GitHub Security tab
166+
uses: github/codeql-action/upload-sarif@v4
167+
continue-on-error: true # Requires GitHub Advanced Security
168+
with:
169+
sarif_file: sarif-results
170+
category: codeql-${{ matrix.language }}
171+
172+
analyze-swift:
173+
name: CodeQL (swift)
174+
needs: changes
175+
if: needs.changes.outputs.swift == 'true'
176+
runs-on: macos-15
177+
178+
steps:
179+
- name: Checkout code
180+
uses: actions/checkout@v6
181+
182+
- name: Initialize CodeQL
183+
uses: github/codeql-action/init@v4
184+
with:
185+
languages: swift
186+
build-mode: manual
187+
config-file: ./.github/codeql/codeql-config.yml
188+
189+
- name: Build (Swift)
190+
working-directory: swift
191+
run: swift build
192+
193+
- name: Perform CodeQL Analysis
194+
uses: github/codeql-action/analyze@v4
195+
with:
196+
category: codeql-swift
197+
upload: never
198+
output: sarif-results
199+
200+
- name: Filter generated-code alerts from SARIF
201+
run: |
202+
for sarif in sarif-results/*.sarif; do
203+
[ -f "$sarif" ] || continue
204+
before=$(jq '[.runs[].results // [] | length] | add // 0' "$sarif")
205+
jq '
206+
.runs |= map(.results |= (. // [] | map(
207+
select(
208+
(.locations // [])[0].physicalLocation.artifactLocation.uri // "" |
209+
test("(^|/)(go/pkg/generated/|typescript/(src/generated|dist)/|ruby/lib/basecamp/generated/|swift/Sources/Basecamp/Generated/|kotlin/sdk/src/commonMain/kotlin/com/basecamp/sdk/generated/)") | not
210+
)
211+
)))
212+
' "$sarif" > "${sarif}.tmp" && mv "${sarif}.tmp" "$sarif"
213+
after=$(jq '[.runs[].results | length] | add // 0' "$sarif")
214+
filtered=$((before - after))
215+
echo "$(basename "$sarif"): $before findings, filtered $filtered generated-code alerts, $after remaining"
216+
done
217+
218+
- name: Upload SARIF to GitHub Security tab
219+
uses: github/codeql-action/upload-sarif@v4
220+
continue-on-error: true # Requires GitHub Advanced Security
221+
with:
222+
sarif_file: sarif-results
223+
category: codeql-swift

0 commit comments

Comments
 (0)