2525 if : ${{ !inputs.skip_lint }}
2626 runs-on : ubuntu-latest
2727 steps :
28- - uses : actions/checkout@v4
28+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2929
3030 - name : Install build deps
3131 run : sudo apt-get update && sudo apt-get install -y zlib1g-dev cmake
3737 sudo apt-get update
3838 sudo apt-get install -y clang-format-20
3939
40- - uses : actions/cache@v4
40+ - uses : actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
4141 id : cppcheck-cache
4242 with :
4343 path : /opt/cppcheck
6464 if : ${{ !inputs.skip_lint }}
6565 runs-on : ubuntu-latest
6666 steps :
67- - uses : actions/checkout@v4
67+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
6868
6969 - name : " Layer 1: Static allow-list audit"
7070 run : scripts/security-audit.sh
7575 - name : " Layer 8: Vendored dependency integrity"
7676 run : scripts/security-vendored.sh
7777
78+ # ── Step 1c: CodeQL SAST gate ────────────────────────────────
79+ codeql-gate :
80+ if : ${{ !inputs.skip_lint }}
81+ runs-on : ubuntu-latest
82+ steps :
83+ - name : Wait for CodeQL on current commit (max 45 min)
84+ env :
85+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
86+ run : |
87+ CURRENT_SHA="${{ github.sha }}"
88+ echo "Current commit: $CURRENT_SHA"
89+ echo "Waiting for CodeQL to complete on this commit..."
90+
91+ for attempt in $(seq 1 90); do
92+ LATEST=$(gh api repos/${{ github.repository }}/actions/workflows/codeql.yml/runs?per_page=5 \
93+ --jq '.workflow_runs[] | select(.head_sha == "'"$CURRENT_SHA"'") | "\(.conclusion) \(.status)"' 2>/dev/null | head -1 || echo "")
94+
95+ if [ -z "$LATEST" ]; then
96+ echo " Attempt $attempt/90: No CodeQL run found for $CURRENT_SHA yet..."
97+ sleep 30
98+ continue
99+ fi
100+
101+ CONCLUSION=$(echo "$LATEST" | cut -d' ' -f1)
102+ STATUS=$(echo "$LATEST" | cut -d' ' -f2)
103+
104+ if [ "$STATUS" = "completed" ] && [ "$CONCLUSION" = "success" ]; then
105+ echo "=== CodeQL completed successfully on current commit ==="
106+ exit 0
107+ elif [ "$STATUS" = "completed" ]; then
108+ echo "BLOCKED: CodeQL completed with conclusion: $CONCLUSION"
109+ exit 1
110+ fi
111+
112+ echo " Attempt $attempt/90: CodeQL status=$STATUS (waiting 30s)..."
113+ sleep 30
114+ done
115+
116+ echo "BLOCKED: CodeQL did not complete within 45 minutes"
117+ exit 1
118+
119+ - name : Check for open code scanning alerts
120+ env :
121+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
122+ run : |
123+ # Wait for GitHub to finish processing alert state changes.
124+ # There is a race between CodeQL marking the workflow as "completed"
125+ # and the alerts API reflecting new/closed alerts from that scan.
126+ echo "Waiting 60s for alert API to settle after CodeQL completion..."
127+ sleep 60
128+
129+ # Poll alerts twice with a gap to confirm the count is stable
130+ ALERTS1=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
131+ echo "Open alerts (check 1): $ALERTS1"
132+ sleep 15
133+ ALERTS2=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
134+ echo "Open alerts (check 2): $ALERTS2"
135+
136+ # Use the higher count (conservative — if either check sees alerts, block)
137+ ALERTS=$ALERTS2
138+ if [ "$ALERTS1" -gt "$ALERTS2" ]; then
139+ ALERTS=$ALERTS1
140+ fi
141+
142+ if [ "$ALERTS" -gt 0 ]; then
143+ echo "BLOCKED: $ALERTS open code scanning alert(s) found."
144+ gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
145+ --jq '.[] | " #\(.number) [\(.rule.security_severity_level // .rule.severity)] \(.rule.id) — \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line)"' 2>/dev/null || true
146+ echo "Fix them: https://github.com/${{ github.repository }}/security/code-scanning"
147+ exit 1
148+ fi
149+ echo "=== CodeQL gate passed (0 open alerts) ==="
150+
78151 # ── Step 2: Unit tests (ASan + UBSan) ───────────────────────
79152 # macOS: use cc (Apple Clang) — GCC on macOS doesn't ship ASan runtime
80153 # Linux: use system gcc — full ASan/UBSan support
@@ -104,7 +177,7 @@ jobs:
104177 cxx : c++
105178 runs-on : ${{ matrix.os }}
106179 steps :
107- - uses : actions/checkout@v4
180+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
108181
109182 - name : Install deps (Ubuntu)
110183 if : startsWith(matrix.os, 'ubuntu')
@@ -118,9 +191,9 @@ jobs:
118191 needs : [lint]
119192 runs-on : windows-latest
120193 steps :
121- - uses : actions/checkout@v4
194+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
122195
123- - uses : msys2/setup-msys2@v2
196+ - uses : msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
124197 with :
125198 msystem : CLANG64
126199 path-type : inherit
@@ -163,13 +236,13 @@ jobs:
163236 cxx : c++
164237 runs-on : ${{ matrix.os }}
165238 steps :
166- - uses : actions/checkout@v4
239+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
167240
168241 - name : Install deps (Ubuntu)
169242 if : startsWith(matrix.os, 'ubuntu')
170243 run : sudo apt-get update && sudo apt-get install -y zlib1g-dev
171244
172- - uses : actions/setup-node@v4
245+ - uses : actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
173246 with :
174247 node-version : " 22"
175248
@@ -178,8 +251,9 @@ jobs:
178251
179252 - name : Archive standard binary
180253 run : |
254+ cp LICENSE build/c/
181255 tar -czf codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
182- -C build/c codebase-memory-mcp
256+ -C build/c codebase-memory-mcp LICENSE
183257
184258 - name : Build UI binary
185259 run : scripts/build.sh --with-ui CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
@@ -190,10 +264,11 @@ jobs:
190264
191265 - name : Archive UI binary
192266 run : |
267+ cp LICENSE build/c/
193268 tar -czf codebase-memory-mcp-ui-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
194- -C build/c codebase-memory-mcp
269+ -C build/c codebase-memory-mcp LICENSE
195270
196- - uses : actions/upload-artifact@v4
271+ - uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
197272 with :
198273 name : binaries-${{ matrix.goos }}-${{ matrix.goarch }}
199274 path : " *.tar.gz"
@@ -203,9 +278,9 @@ jobs:
203278 needs : [test-unix, test-windows]
204279 runs-on : windows-latest
205280 steps :
206- - uses : actions/checkout@v4
281+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
207282
208- - uses : msys2/setup-msys2@v2
283+ - uses : msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
209284 with :
210285 msystem : CLANG64
211286 path-type : inherit
@@ -215,7 +290,7 @@ jobs:
215290 make
216291 zip
217292
218- - uses : actions/setup-node@v4
293+ - uses : actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
219294 with :
220295 node-version : " 22"
221296
@@ -229,7 +304,7 @@ jobs:
229304 BIN=build/c/codebase-memory-mcp
230305 [ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
231306 cp "$BIN" codebase-memory-mcp.exe
232- zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe
307+ zip codebase-memory-mcp-windows-amd64.zip codebase-memory-mcp.exe LICENSE
233308
234309 - name : Build UI binary
235310 shell : msys2 {0}
@@ -241,9 +316,9 @@ jobs:
241316 BIN=build/c/codebase-memory-mcp
242317 [ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
243318 cp "$BIN" codebase-memory-mcp-ui.exe
244- zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp-ui.exe
319+ zip codebase-memory-mcp-ui-windows-amd64.zip codebase-memory-mcp-ui.exe LICENSE
245320
246- - uses : actions/upload-artifact@v4
321+ - uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
247322 with :
248323 name : binaries-windows-amd64
249324 path : " *.zip"
@@ -271,9 +346,9 @@ jobs:
271346 variant : [standard, ui]
272347 runs-on : ${{ matrix.os }}
273348 steps :
274- - uses : actions/checkout@v4
349+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
275350
276- - uses : actions/download-artifact@v4
351+ - uses : actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
277352 with :
278353 name : binaries-${{ matrix.goos }}-${{ matrix.goarch }}
279354
@@ -302,6 +377,37 @@ jobs:
302377 if : matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64'
303378 run : scripts/security-fuzz.sh ./codebase-memory-mcp
304379
380+ - name : Fuzz testing (60s random input)
381+ if : matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64'
382+ run : scripts/security-fuzz-random.sh ./codebase-memory-mcp 60
383+
384+ - name : ClamAV scan (Linux)
385+ if : matrix.variant == 'standard' && startsWith(matrix.os, 'ubuntu')
386+ run : |
387+ sudo apt-get update -qq && sudo apt-get install -y -qq clamav > /dev/null 2>&1
388+ sudo sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf 2>/dev/null || true
389+ grep -q "DatabaseMirror" /etc/clamav/freshclam.conf 2>/dev/null || \
390+ echo "DatabaseMirror database.clamav.net" | sudo tee -a /etc/clamav/freshclam.conf > /dev/null
391+ sudo freshclam --quiet
392+ echo "=== ClamAV scan ==="
393+ clamscan --no-summary ./codebase-memory-mcp
394+ echo "=== ClamAV: clean ==="
395+
396+ - name : ClamAV scan (macOS)
397+ if : matrix.variant == 'standard' && startsWith(matrix.os, 'macos')
398+ run : |
399+ brew install clamav > /dev/null 2>&1
400+ CLAMAV_ETC=$(brew --prefix)/etc/clamav
401+ if [ ! -f "$CLAMAV_ETC/freshclam.conf" ]; then
402+ cp "$CLAMAV_ETC/freshclam.conf.sample" "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
403+ sed -i '' 's/^Example/#Example/' "$CLAMAV_ETC/freshclam.conf" 2>/dev/null || true
404+ echo "DatabaseMirror database.clamav.net" >> "$CLAMAV_ETC/freshclam.conf"
405+ fi
406+ freshclam --quiet --no-warnings 2>/dev/null || freshclam --quiet 2>/dev/null || echo "WARNING: freshclam update failed, using bundled signatures"
407+ echo "=== ClamAV scan (macOS) ==="
408+ clamscan --no-summary ./codebase-memory-mcp
409+ echo "=== ClamAV: clean ==="
410+
305411 smoke-windows :
306412 if : ${{ !inputs.skip_builds }}
307413 needs : [build-windows]
@@ -311,17 +417,17 @@ jobs:
311417 variant : [standard, ui]
312418 runs-on : windows-latest
313419 steps :
314- - uses : actions/checkout@v4
420+ - uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
315421
316- - uses : msys2/setup-msys2@v2
422+ - uses : msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
317423 with :
318424 msystem : CLANG64
319425 path-type : inherit
320426 install : >-
321427 mingw-w64-clang-x86_64-python3
322428 unzip
323429
324- - uses : actions/download-artifact@v4
430+ - uses : actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
325431 with :
326432 name : binaries-windows-amd64
327433
@@ -345,3 +451,17 @@ jobs:
345451 if : matrix.variant == 'standard'
346452 shell : msys2 {0}
347453 run : scripts/security-install.sh ./codebase-memory-mcp.exe
454+
455+ - name : Windows Defender scan
456+ if : matrix.variant == 'standard'
457+ shell : pwsh
458+ run : |
459+ Write-Host "=== Windows Defender scan (with ML heuristics) ==="
460+ & "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null
461+ $result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File "$PWD\codebase-memory-mcp.exe" -DisableRemediation
462+ Write-Host $result
463+ if ($LASTEXITCODE -ne 0) {
464+ Write-Host "BLOCKED: Windows Defender flagged the binary!"
465+ exit 1
466+ }
467+ Write-Host "=== Windows Defender: clean ==="
0 commit comments