Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
name: check

on:
# dist.yml calls this workflow for tags and waits for it before releasing
push:
branches: ['**']
pull_request:
workflow_call:

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
# make check uses GOVULNCHECK_VERSION from the Makefile
- run: make check
# dist needs cgo; this catches a change that stops the release build from linking
# Build with cgo and musl to catch release linking failures
- run: sudo apt-get update && sudo apt-get install -y musl-tools
- run: make dist
73 changes: 54 additions & 19 deletions .github/workflows/dist.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
name: dist

# Builds release binaries for every platform. The tree-sitter parsers need cgo, so each build
# needs a C compiler for its target: Linux and Windows cross-compile on one Ubuntu runner, and
# macOS builds on a macOS runner. A tag push also publishes the binaries as a GitHub Release; a
# manual run only uploads them as a workflow artifact.
# Build release binaries with the C compilers required by the tree-sitter parsers.
# Linux builds link musl on runners matching each target architecture. Windows
# cross-compiles on the amd64 Linux runner; macOS builds on a macOS runner.
# Tag pushes publish a GitHub Release after check passes and govulncheck passes
# for each binary. Manual runs upload workflow artifacts.

on:
push:
Expand All @@ -14,66 +15,100 @@ permissions:
contents: read

jobs:
check:
uses: ./.github/workflows/check.yml

linux-windows:
runs-on: ubuntu-latest
# Pin the image supplying the statically linked C runtimes documented in THIRD_PARTY_NOTICES
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # the version stamp comes from git describe
- uses: actions/setup-go@v5
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu gcc-mingw-w64-x86-64
- run: sudo apt-get update && sudo apt-get install -y musl-tools gcc-mingw-w64-x86-64
- run: make dist
- run: make dist GOARCH=arm64 CC=aarch64-linux-gnu-gcc
- run: make dist GOOS=windows CC=x86_64-w64-mingw32-gcc
- uses: actions/upload-artifact@v4
# Copy notices from the installed MinGW-w64 package used to link the Windows binary
- run: cp /usr/share/doc/mingw-w64-x86-64-dev/copyright dist/THIRD_PARTY_NOTICES.mingw-w64
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist-linux-windows
path: dist/

linux-arm64:
# Keep the C runtime source consistent with linux-windows
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- run: sudo apt-get update && sudo apt-get install -y musl-tools
- run: make dist
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist-linux-arm64
path: dist/

darwin:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- run: make dist
- run: make dist GOARCH=amd64 CC="clang -arch x86_64"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist-darwin
path: dist/

checksums:
needs: [linux-windows, darwin]
needs: [linux-windows, linux-arm64, darwin]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
path: dist
pattern: dist-*
merge-multiple: true
# Scan the Go and module versions embedded in each binary, covering every release target
# and build tag; a source scan covers only this runner's configuration
- run: make vuln-dist
- run: cd dist && sha256sum mcp-scanner_* > SHA256SUMS
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: dist
path: dist/

release:
needs: checksums
needs: [check, checksums]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
contents: write # create the release
id-token: write # request the signing certificate for the provenance attestation
attestations: write # store the provenance attestation
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: dist
path: dist
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: dist/mcp-scanner_*
- run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes
env:
GH_TOKEN: ${{ github.token }}
40 changes: 27 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ API_BIN := mcp-scanner-api
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -s -w -X main.scanVersion=$(VERSION)

.PHONY: all build build-yara check-version test test-yara race vet fmt fmt-check lint vuln fuzz cover check clean dist sync-rules
.PHONY: all build build-yara check-version test test-yara race vet fmt fmt-check lint vuln vuln-dist fuzz cover check clean dist sync-rules

all: check build

Expand Down Expand Up @@ -58,9 +58,18 @@ lint:
@command -v staticcheck >/dev/null || { echo "staticcheck not installed: go install honnef.co/go/tools/cmd/staticcheck@latest"; exit 1; }
staticcheck ./...

# Use the same govulncheck release locally and in CI; go run caches the build
GOVULNCHECK_VERSION := v1.9.0
GOVULNCHECK := go run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)

vuln:
@command -v govulncheck >/dev/null || { echo "govulncheck not installed: go install golang.org/x/vuln/cmd/govulncheck@latest"; exit 1; }
govulncheck ./...
$(GOVULNCHECK) ./...

# Scan the Go and module versions embedded in each dist/ binary, covering the release's
# GOOS/GOARCH and build tags. A source scan covers only the host configuration.
# With no matching binaries, the unexpanded glob causes the scan to fail.
vuln-dist:
@set -e; for f in dist/$(BIN)_*; do echo "govulncheck -mode=binary $$f"; $(GOVULNCHECK) -mode=binary "$$f"; done

# A short CI run. Crashes fail the target; longer campaigns are manual
fuzz:
Expand Down Expand Up @@ -88,23 +97,28 @@ check: fmt-check vet vuln test race check-version
# C, so the build needs cgo and a C compiler for the target. To build for another platform, name
# a cross compiler:
#
# make dist GOARCH=arm64 CC=aarch64-linux-gnu-gcc
# make dist GOOS=windows CC=x86_64-w64-mingw32-gcc
# make dist GOARCH=amd64 CC="clang -arch x86_64" # on an arm64 Mac
#
# A macOS binary needs a macOS host. Linux binaries are linked statically so they run on any
# distribution; netgo and osusergo keep DNS and user lookups in Go, which static glibc cannot
# serve. Windows binaries link the MinGW runtime statically, so they need no extra DLLs.
DIST_OS := $(or $(GOOS),$(shell go env GOOS))
DIST_ARCH := $(or $(GOARCH),$(shell go env GOARCH))
DIST_EXT := $(if $(filter windows,$(DIST_OS)),.exe)
DIST_TAGS := $(if $(filter linux,$(DIST_OS)),netgo osusergo)
DIST_LDFLAGS := $(if $(filter linux,$(DIST_OS)),-linkmode external) $(if $(filter linux windows,$(DIST_OS)),-extldflags -static)
# A macOS binary needs a macOS host. Linux builds use musl-gcc (musl-tools) on a host matching
# the target architecture. Static musl linking avoids a dependency on the distribution's libc
# and requires its license notice; static glibc linking would require relinkable objects.
# musl defaults to a 128 KiB thread stack, below the 8 MiB used by glibc on the release runners.
# -z stack-size requests 8 MiB for tree-sitter's C code when parsing hostile input.
# netgo and osusergo keep DNS and user lookups in Go. Windows builds link the MinGW runtime
# statically, so they need no extra runtime DLLs.
DIST_OS := $(or $(GOOS),$(shell go env GOOS))
DIST_ARCH := $(or $(GOARCH),$(shell go env GOARCH))
DIST_EXT := $(if $(filter windows,$(DIST_OS)),.exe)
DIST_TAGS := $(if $(filter linux,$(DIST_OS)),netgo osusergo)
DIST_CC := $(if $(filter linux,$(DIST_OS)),CC=musl-gcc)
LINUX_LDFLAGS := -linkmode external -extldflags "-static -Wl,-z,stack-size=8388608"
DIST_LDFLAGS := $(if $(filter linux,$(DIST_OS)),$(LINUX_LDFLAGS))$(if $(filter windows,$(DIST_OS)),-extldflags -static)
SHA256 := $(if $(shell command -v sha256sum 2>/dev/null),sha256sum,shasum -a 256)

dist:
@mkdir -p dist
GOOS=$(DIST_OS) GOARCH=$(DIST_ARCH) CGO_ENABLED=1 go build -tags '$(DIST_TAGS)' \
$(DIST_CC) GOOS=$(DIST_OS) GOARCH=$(DIST_ARCH) CGO_ENABLED=1 go build -tags '$(DIST_TAGS)' \
-ldflags '$(LDFLAGS) $(DIST_LDFLAGS)' -o dist/$(BIN)_$(DIST_OS)_$(DIST_ARCH)$(DIST_EXT) ./cmd/mcp-scanner
cp -f LICENSE THIRD_PARTY_NOTICES dist/
cd dist && $(SHA256) $(BIN)_* > SHA256SUMS
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,33 @@ SKIPPED + UNKNOWN no analyzer produced usable analysis

Timeouts, crashes, unexplained files, and missing statuses downgrade the scan.
An analyzer error is never self-excusing; only an explicit best-effort policy
can exempt it. A known limitation is that any recorded file-skip reason accounts
for all skipped files, so some incomplete file scans can still be `SAFE`.
can exempt it. Skips that could hide source, such as a symlink to a source
directory outside the scan root, also record a limit. Every limit downgrades
the scan.

## Install

```sh
go install github.com/devlab-group/mcp-scanner/cmd/mcp-scanner@latest
```

GitHub releases include prebuilt binaries for Linux, macOS and Windows,
`SHA256SUMS`, and a build provenance attestation for each binary. Verify the
checksum and attestation before running a downloaded binary:

```sh
sha256sum --check --ignore-missing SHA256SUMS
gh attestation verify mcp-scanner_linux_amd64 \
--repo devlab-group/mcp-scanner \
--signer-workflow devlab-group/mcp-scanner/.github/workflows/dist.yml \
--source-ref refs/tags/vX.Y.Z
```

The attestation check verifies that the binary came from this repository's
release workflow and the specified tag. The checksum detects download damage;
because it comes from the same release as the binary, it cannot establish the
binary's origin. The attestation provides that check.

Signature matching requires libyara and a tagged build:

```sh
Expand Down
Loading
Loading