Skip to content

Commit 2e21a01

Browse files
committed
Release security hardening: VirusTotal + attestations + cosign + SBOM + OpenSSF + SECURITY.md
Release pipeline (step 5 + 6) now includes: - SLSA build provenance attestations for all binaries - Sigstore cosign keyless signing (.bundle files) - SBOM (CycloneDX) with all vendored dependencies - VirusTotal scan of all binaries (70+ engines) - OpenSSF Scorecard (repo security health score) - All results appended to release notes automatically Also: - SECURITY.md: vulnerability reporting + security measures - Removed DLL API strings from test binary (issue #89)
1 parent f43774f commit 2e21a01

File tree

5 files changed

+205
-73
lines changed

5 files changed

+205
-73
lines changed

.github/workflows/release.yml

Lines changed: 140 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ on:
1919

2020
permissions:
2121
contents: write
22+
id-token: write
23+
attestations: write
2224

2325
jobs:
2426
# ── Step 1: Lint (clang-format + cppcheck) ───────────────────
@@ -343,6 +345,8 @@ jobs:
343345
runs-on: ubuntu-latest
344346
permissions:
345347
contents: write
348+
id-token: write
349+
attestations: write
346350
steps:
347351
- uses: actions/checkout@v4
348352

@@ -356,6 +360,72 @@ jobs:
356360
- name: Generate checksums
357361
run: sha256sum *.tar.gz *.zip > checksums.txt
358362

363+
# ── Artifact attestations (SLSA provenance) ──────────────
364+
# Cryptographically proves each binary was built by this
365+
# GitHub Actions workflow from this repo. Verifiable with:
366+
# gh attestation verify <file> --repo DeusData/codebase-memory-mcp
367+
- name: Attest build provenance (tar.gz)
368+
uses: actions/attest-build-provenance@v2
369+
with:
370+
subject-path: '*.tar.gz'
371+
372+
- name: Attest build provenance (zip)
373+
uses: actions/attest-build-provenance@v2
374+
with:
375+
subject-path: '*.zip'
376+
377+
- name: Attest build provenance (checksums)
378+
uses: actions/attest-build-provenance@v2
379+
with:
380+
subject-path: 'checksums.txt'
381+
382+
# ── SBOM generation ──────────────────────────────────────
383+
# Software Bill of Materials listing all vendored dependencies.
384+
- name: Generate SBOM
385+
run: |
386+
cat > sbom.json << 'SBOMEOF'
387+
{
388+
"bomFormat": "CycloneDX",
389+
"specVersion": "1.4",
390+
"version": 1,
391+
"metadata": {
392+
"component": {
393+
"type": "application",
394+
"name": "codebase-memory-mcp",
395+
"version": "${{ inputs.version }}"
396+
}
397+
},
398+
"components": [
399+
{"type": "library", "name": "sqlite3", "version": "3.49.1", "description": "Vendored SQLite amalgamation"},
400+
{"type": "library", "name": "yyjson", "version": "0.10.0", "description": "Fast JSON parser"},
401+
{"type": "library", "name": "mongoose", "version": "7.16", "description": "Embedded HTTP server"},
402+
{"type": "library", "name": "mimalloc", "version": "2.1.7", "description": "Memory allocator"},
403+
{"type": "library", "name": "xxhash", "version": "0.8.2", "description": "Fast hash function"},
404+
{"type": "library", "name": "tre", "version": "0.8.0", "description": "POSIX regex (Windows)"},
405+
{"type": "library", "name": "tree-sitter", "version": "0.24.4", "description": "AST parser runtime (64 grammars)"}
406+
]
407+
}
408+
SBOMEOF
409+
410+
- name: Attest SBOM
411+
uses: actions/attest-sbom@v2
412+
with:
413+
subject-path: '*.tar.gz'
414+
sbom-path: 'sbom.json'
415+
416+
# ── Sigstore cosign signing ──────────────────────────────
417+
# Keyless signing via GitHub OIDC identity. Verifiable with:
418+
# cosign verify-blob --bundle <file>.bundle <file>
419+
- name: Install cosign
420+
uses: sigstore/cosign-installer@v3
421+
422+
- name: Sign release artifacts with cosign
423+
run: |
424+
for f in *.tar.gz *.zip checksums.txt; do
425+
cosign sign-blob --yes --bundle "${f}.bundle" "$f"
426+
done
427+
428+
# ── Create release ───────────────────────────────────────
359429
- name: Delete existing release
360430
if: ${{ inputs.replace }}
361431
env:
@@ -377,23 +447,34 @@ jobs:
377447
*.tar.gz
378448
*.zip
379449
checksums.txt
450+
sbom.json
451+
*.bundle
380452
body: ${{ inputs.release_notes || '' }}
381453
generate_release_notes: ${{ inputs.release_notes == '' }}
382454

383-
# ── Step 6: VirusTotal scan all release binaries ─────────────
384-
virustotal:
455+
# ── Step 6: Post-release security verification ───────────────
456+
# Scans binaries with VirusTotal, runs OpenSSF Scorecard,
457+
# and appends all results to the release notes.
458+
verify:
385459
needs: [release]
386460
runs-on: ubuntu-latest
387461
permissions:
388462
contents: write
463+
security-events: write
464+
id-token: write
389465
steps:
390-
- name: Download release assets
466+
- uses: actions/checkout@v4
467+
with:
468+
persist-credentials: false
469+
470+
# ── VirusTotal scan ──────────────────────────────────────
471+
- name: Download release binaries
391472
env:
392473
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
393474
VERSION: ${{ inputs.version }}
394475
run: |
395476
mkdir -p assets
396-
gh release download "$VERSION" --dir assets --repo "$GITHUB_REPOSITORY"
477+
gh release download "$VERSION" --dir assets --repo "$GITHUB_REPOSITORY" --pattern '*.tar.gz' --pattern '*.zip'
397478
ls -la assets/
398479
399480
- name: Scan all binaries with VirusTotal
@@ -405,34 +486,75 @@ jobs:
405486
assets/*.tar.gz
406487
assets/*.zip
407488
408-
- name: Parse scan results and check for detections
489+
# ── OpenSSF Scorecard ────────────────────────────────────
490+
- name: Run OpenSSF Scorecard
491+
uses: ossf/scorecard-action@v2
492+
id: scorecard
493+
with:
494+
results_file: scorecard.sarif
495+
results_format: sarif
496+
publish_results: true
497+
498+
- name: Upload Scorecard SARIF
499+
uses: github/codeql-action/upload-sarif@v3
500+
with:
501+
sarif_file: scorecard.sarif
502+
503+
- name: Extract Scorecard score
504+
id: score
505+
run: |
506+
# Extract overall score from SARIF (properties.score)
507+
SCORE=$(python3 -c "
508+
import json, sys
509+
with open('scorecard.sarif') as f:
510+
d = json.load(f)
511+
props = d.get('runs', [{}])[0].get('tool', {}).get('driver', {}).get('properties', {})
512+
print(props.get('score', 'N/A'))
513+
" 2>/dev/null || echo "N/A")
514+
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
515+
echo "OpenSSF Scorecard: $SCORE/10"
516+
517+
# ── Append all results to release notes ──────────────────
518+
- name: Append security verification to release notes
409519
env:
410520
VT_ANALYSIS: ${{ steps.virustotal.outputs.analysis }}
521+
SCORECARD_SCORE: ${{ steps.score.outputs.score }}
411522
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
412523
VERSION: ${{ inputs.version }}
413524
run: |
414-
echo "=== VirusTotal Scan Results ==="
415-
echo "$VT_ANALYSIS"
525+
echo "=== Building security verification report ==="
416526
417-
# Build markdown table for release notes
418-
VT_REPORT="---\n\n### VirusTotal Scan Results\n\n"
419-
VT_REPORT+="All release binaries were scanned by [VirusTotal](https://www.virustotal.com/) (70+ antivirus engines).\n\n"
420-
VT_REPORT+="| Binary | Scan |\n|--------|------|\n"
527+
REPORT=$'---\n\n### Security Verification\n\n'
528+
REPORT+=$'All release binaries have been independently verified:\n\n'
421529
422-
FAILED=false
530+
# VirusTotal results
531+
REPORT+=$'**VirusTotal** — scanned by 70+ antivirus engines:\n\n'
532+
REPORT+=$'| Binary | Scan |\n|--------|------|\n'
423533
while IFS= read -r line; do
424534
[ -z "$line" ] && continue
425-
# Format: filename=analysisURL
426535
FILE=$(echo "$line" | cut -d'=' -f1)
427536
URL=$(echo "$line" | cut -d'=' -f2-)
428537
BASENAME=$(basename "$FILE")
429-
VT_REPORT+="| $BASENAME | [View Report]($URL) |\n"
538+
REPORT+="| $BASENAME | [View Report]($URL) |"$'\n'
430539
done <<< "$VT_ANALYSIS"
431540
541+
# OpenSSF Scorecard
542+
REPORT+=$'\n**OpenSSF Scorecard** — repository security health: **'"$SCORECARD_SCORE"$'/10**\n'
543+
REPORT+=$'[View detailed scorecard](https://scorecard.dev/viewer/?uri=github.com/DeusData/codebase-memory-mcp)\n\n'
544+
545+
# Build provenance
546+
REPORT+=$'**Build Provenance (SLSA)** — cryptographic proof each binary was built by GitHub Actions from this repo:\n'
547+
REPORT+=$'```\ngh attestation verify <downloaded-file> --repo DeusData/codebase-memory-mcp\n```\n\n'
548+
549+
# Cosign
550+
REPORT+=$'**Sigstore cosign** — keyless signature verification:\n'
551+
REPORT+=$'```\ncosign verify-blob --bundle <file>.bundle <file>\n```\n\n'
552+
553+
# SBOM
554+
REPORT+=$'**SBOM** — Software Bill of Materials (`sbom.json`) lists all vendored dependencies.\n'
555+
432556
# Append to release notes
433557
EXISTING=$(gh release view "$VERSION" --json body --jq '.body' --repo "$GITHUB_REPOSITORY")
434-
UPDATED="${EXISTING}\n\n${VT_REPORT}"
435-
echo -e "$UPDATED" | gh release edit "$VERSION" --notes-file - --repo "$GITHUB_REPOSITORY"
558+
printf '%s\n\n%s\n' "$EXISTING" "$REPORT" | gh release edit "$VERSION" --notes-file - --repo "$GITHUB_REPOSITORY"
436559
437-
echo ""
438-
echo "=== Scan links appended to release notes ==="
560+
echo "=== Security verification appended to release notes ==="

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ graph-ui/dist/
4949
# Generated reports
5050
BENCHMARK_REPORT.md
5151
TEST_PLAN.md
52+
CHANGELOG.md

SECURITY.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Security Policy
2+
3+
## Reporting a Vulnerability
4+
5+
If you discover a security vulnerability, please report it responsibly:
6+
7+
1. **Do NOT open a public issue** for security vulnerabilities
8+
2. Email: martin.vogel.tech@gmail.com
9+
3. Include: description, reproduction steps, affected version, potential impact
10+
11+
We will acknowledge your report within 48 hours and provide a fix timeline within 7 days.
12+
13+
## Security Measures
14+
15+
This project implements multiple layers of security verification:
16+
17+
### Build-Time (CI)
18+
19+
- **8-layer security audit suite** runs on every build (static analysis, binary string audit, network egress monitoring, install path validation, MCP robustness testing, UI security audit, vendored dependency integrity, smoke test hardening)
20+
- **All dangerous function calls** (`system()`, `popen()`, `fork()`, `connect()`) require a reviewed entry in `scripts/security-allowlist.txt`
21+
- **Vendored dependency checksums** verified on every build (72 files, SHA-256)
22+
23+
### Release-Time
24+
25+
- **VirusTotal scanning** — all release binaries scanned by 70+ antivirus engines, reports linked in release notes
26+
- **SLSA build provenance** — cryptographic attestation proving each binary was built by GitHub Actions from this repository
27+
- **Sigstore cosign signing** — keyless signatures verifiable by anyone
28+
- **SBOM** — Software Bill of Materials listing all vendored dependencies
29+
- **SHA-256 checksums** — published with every release
30+
31+
### Code-Level Defenses
32+
33+
- **Shell injection prevention**`cbm_validate_shell_arg()` rejects metacharacters before all `popen()`/`system()` calls
34+
- **SQLite authorizer** — blocks `ATTACH`/`DETACH` at engine level
35+
- **CORS locked to localhost** — graph UI only accessible from localhost origins
36+
- **Path containment**`realpath()` check prevents reading files outside project root
37+
- **Process-kill restriction** — only server-spawned PIDs can be terminated
38+
39+
### Verification
40+
41+
Users can independently verify any release binary:
42+
43+
```bash
44+
# SLSA provenance (proves binary came from this repo's CI)
45+
gh attestation verify <downloaded-file> --repo DeusData/codebase-memory-mcp
46+
47+
# Sigstore cosign (keyless signature)
48+
cosign verify-blob --bundle <file>.bundle <file>
49+
50+
# SHA-256 checksum
51+
sha256sum -c checksums.txt
52+
```
53+
54+
## Supported Versions
55+
56+
| Version | Supported |
57+
|---------|-----------|
58+
| 0.5.x | Yes |
59+
| < 0.5 | No (Go codebase, superseded by C rewrite) |

src/pipeline/pass_calls.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,5 @@ void cbm_pipeline_pass_fastapi_depends(cbm_pipeline_ctx_t *ctx, const cbm_file_i
438438
}
439439
}
440440

441-
/* DLL resolve tracking removed — the string literals (GetProcAddress, dlsym,
442-
* LoadLibrary) triggered Windows Defender Wacatac.B!ml false positive.
443-
* See: https://github.com/DeusData/codebase-memory-mcp/issues/89 */
441+
/* DLL resolve tracking removed — triggered Windows Defender false positive.
442+
* See issue #89. */

tests/test_c_lsp.c

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* - Lambda captures, struct fields, trailing returns
1717
* - STL containers, C idioms (func ptrs, opaque handles)
1818
* - C11 _Generic, bitfields, unions, varargs
19-
* - DLL/dlsym patterns, SFINAE, placement new
19+
* - DLL patterns, SFINAE, placement new
2020
*/
2121
#include "test_framework.h"
2222
#include "cbm.h"
@@ -14928,55 +14928,8 @@ TEST(clsp_easy_win_sfinaevoid_t) {
1492814928
PASS();
1492914929
}
1493014930

14931-
TEST(clsp_dll_get_proc_address) {
14932-
CBMFileResult *r =
14933-
extract_c("\n"
14934-
"typedef void* HMODULE;\n"
14935-
"typedef void (*HandleFunc)(int);\n"
14936-
"\n"
14937-
"void* LoadLibrary(const char* name);\n"
14938-
"void* GetProcAddress(void* module, const char* name);\n"
14939-
"\n"
14940-
"void test() {\n"
14941-
" HMODULE dll = LoadLibrary(\"mylib.dll\");\n"
14942-
" HandleFunc handle = (HandleFunc)GetProcAddress(dll, \"HandleMyGarbage\");\n"
14943-
" handle(42);\n"
14944-
"}\n"
14945-
"");
14946-
ASSERT_NOT_NULL(r);
14947-
ASSERT_GTE(find_resolved(r, "test", "external.HandleMyGarbage"), 0);
14948-
{
14949-
int idx = find_resolved(r, "test", "external.HandleMyGarbage");
14950-
if (idx >= 0)
14951-
ASSERT_STR_EQ(r->resolved_calls.items[idx].strategy, "lsp_dll_resolve");
14952-
}
14953-
cbm_free_result(r);
14954-
PASS();
14955-
}
14956-
14957-
TEST(clsp_dll_dlsym) {
14958-
CBMFileResult *r = extract_c("\n"
14959-
"typedef void (*init_fn)(void);\n"
14960-
"\n"
14961-
"void* dlopen(const char* filename, int flags);\n"
14962-
"void* dlsym(void* handle, const char* symbol);\n"
14963-
"\n"
14964-
"void test() {\n"
14965-
" void* h = dlopen(\"libfoo.so\", 1);\n"
14966-
" init_fn init = (init_fn)dlsym(h, \"initialize\");\n"
14967-
" init();\n"
14968-
"}\n"
14969-
"");
14970-
ASSERT_NOT_NULL(r);
14971-
ASSERT_GTE(find_resolved(r, "test", "external.initialize"), 0);
14972-
{
14973-
int idx = find_resolved(r, "test", "external.initialize");
14974-
if (idx >= 0)
14975-
ASSERT_STR_EQ(r->resolved_calls.items[idx].strategy, "lsp_dll_resolve");
14976-
}
14977-
cbm_free_result(r);
14978-
PASS();
14979-
}
14931+
/* DLL resolve LSP tests removed — string literals triggered
14932+
* Windows Defender false positive. See issue #89. */
1498014933

1498114934
TEST(clsp_dll_custom_resolver) {
1498214935
CBMFileResult *r = extract_c("\n"
@@ -15848,8 +15801,6 @@ SUITE(c_lsp) {
1584815801
RUN_TEST(clsp_easy_win_overload_rvalue_ref);
1584915802
RUN_TEST(clsp_easy_win_sfinaeenable_if);
1585015803
RUN_TEST(clsp_easy_win_sfinaevoid_t);
15851-
RUN_TEST(clsp_dll_get_proc_address);
15852-
RUN_TEST(clsp_dll_dlsym);
1585315804
RUN_TEST(clsp_dll_custom_resolver);
1585415805
RUN_TEST(clsp_dll_cpp_static_cast);
1585515806
RUN_TEST(clsp_dll_reinterpret_cast);

0 commit comments

Comments
 (0)