diff --git a/.github/workflows/scanner-publish.yml b/.github/workflows/scanner-publish.yml new file mode 100644 index 0000000..666e92b --- /dev/null +++ b/.github/workflows/scanner-publish.yml @@ -0,0 +1,93 @@ +name: Publish Scanner + +on: + push: + tags: + - 'scanner-v*' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: nimblebraininc/mpak-scanner + +jobs: + verify: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/scanner + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: astral-sh/setup-uv@v4 + - run: uv sync --extra dev + - run: uv run ruff check src/ tests/ + - run: uv run ruff format --check src/ tests/ + - run: uv run ty check src/ + - run: uv run pytest -m "not e2e" + + publish-pypi: + needs: verify + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + defaults: + run: + working-directory: apps/scanner + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: astral-sh/setup-uv@v4 + + - name: Verify tag matches package version + run: | + TAG_VERSION="${GITHUB_REF#refs/tags/scanner-v}" + PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) != pyproject.toml version ($PKG_VERSION)" + exit 1 + fi + + - name: Build + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: apps/scanner/dist/ + + publish-docker: + needs: [verify, publish-pypi] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF#refs/tags/scanner-v}" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: apps/scanner + file: apps/scanner/Dockerfile + push: true + build-args: | + SCANNER_VERSION=${{ steps.version.outputs.version }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/apps/scanner/CLAUDE.md b/apps/scanner/CLAUDE.md index fa7cc56..e4cfd4c 100644 --- a/apps/scanner/CLAUDE.md +++ b/apps/scanner/CLAUDE.md @@ -159,35 +159,38 @@ uv run pytest # all tests (unit + e2e) ## Releasing a New Version -The scanner is distributed via PyPI. The Docker image installs from PyPI, not local source. +Releases are automated via GitHub Actions and PyPI trusted publishing. Pushing a tag triggers: verify → publish to PyPI → build + push Docker image to GHCR. + +**Version is in one place:** `pyproject.toml`. Runtime version (`__version__`, `SCANNER_VERSION`) is derived via `importlib.metadata`. ### Steps -1. **Bump version** in four files (must all match): - - `pyproject.toml` (`version = "X.Y.Z"`) - - `src/mpak_scanner/__init__.py` (`__version__ = "X.Y.Z"`) - - `src/mpak_scanner/scanner.py` (`SCANNER_VERSION = "X.Y.Z"`) - - `Dockerfile` (`mpak-scanner[job]==X.Y.Z`) +1. **Bump version** in `pyproject.toml` (the only place) 2. **Run verification**: ```bash uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run ty check src/ && uv run pytest ``` -3. **Commit and push** in `apps/mpak` - -4. **Publish to PyPI** (from `apps/mpak/apps/scanner/`): +3. **Commit, tag, and push:** ```bash - uv build && uv publish + git commit -am "scanner: bump to X.Y.Z" + git tag scanner-vX.Y.Z + git push origin main --tags ``` -5. **Build + push Docker image** (from `hq/deployments/mpak/`): - ```bash - make deploy-scanner ENV=production - make apply-scanner-infra ENV=production # only if RBAC/secrets changed - ``` +CI handles PyPI publish and Docker build/push to `ghcr.io/nimblebraininc/mpak-scanner`. See `.github/workflows/scanner-publish.yml`. + +### Production Deployment (ECR/K8s) + +After the PyPI release, deploy the scanner to production K8s (from `hq/deployments/mpak/`): + +```bash +make deploy-scanner ENV=production +make apply-scanner-infra ENV=production # only if RBAC/secrets changed +``` -The Makefile pushes both the git commit tag and `latest`. The mpak-api references `latest`, so deploying automatically updates what production uses. +The Makefile builds from the Dockerfile which pulls the version from PyPI. ### Schemas and Rules diff --git a/apps/scanner/Dockerfile b/apps/scanner/Dockerfile index 5199eec..047ec40 100644 --- a/apps/scanner/Dockerfile +++ b/apps/scanner/Dockerfile @@ -1,4 +1,5 @@ FROM python:3.13-slim +ARG SCANNER_VERSION=0.2.6 # System deps for external security tools RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -20,7 +21,7 @@ RUN curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main RUN npm install -g eslint eslint-plugin-security --no-fund --no-audit # mpak-scanner + Python security tools (bandit, guarddog) -RUN pip install --no-cache-dir "mpak-scanner[job]==0.2.4" bandit guarddog +RUN pip install --no-cache-dir "mpak-scanner[job]==${SCANNER_VERSION}" bandit guarddog ENTRYPOINT ["mpak-scanner"] CMD ["job"] diff --git a/apps/scanner/README.md b/apps/scanner/README.md index 7940264..31e3563 100644 --- a/apps/scanner/README.md +++ b/apps/scanner/README.md @@ -4,6 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/mpak-scanner)](https://pypi.org/project/mpak-scanner/) [![Python](https://img.shields.io/pypi/pyversions/mpak-scanner)](https://pypi.org/project/mpak-scanner/) [![License](https://img.shields.io/pypi/l/mpak-scanner)](https://github.com/NimbleBrainInc/mpak/blob/main/apps/scanner/LICENSE) +[![mpak.dev](https://mpak.dev/badge.svg)](https://mpak.dev) Security scanner for [MCP](https://modelcontextprotocol.io/) bundles (.mcpb). Reference implementation of the [mpak Trust Framework (MTF)](https://mpaktrust.org), an open security standard for MCP server packaging. @@ -121,6 +122,60 @@ The scanner ships with test fixtures for validation: See [tests/fixtures/README.md](tests/fixtures/README.md) for details. +## Releasing + +Releases are automated via GitHub Actions. Pushing a tag triggers the full pipeline: verify, publish to PyPI (via [trusted publishing](https://docs.pypi.org/trusted-publishers/)), and build + push Docker image to GHCR. + +**Version is defined in one place:** `pyproject.toml`. The runtime version (`mpak_scanner.__version__`, `SCANNER_VERSION`) is derived automatically via `importlib.metadata`. + +### Steps + +1. **Bump version** in `pyproject.toml`: + ```bash + # Edit pyproject.toml version field, or use hatch: + hatch version patch # 0.2.4 → 0.2.5 + hatch version minor # 0.2.4 → 0.3.0 + ``` + +2. **Run verification:** + ```bash + uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run ty check src/ && uv run pytest + ``` + +3. **Commit and push:** + ```bash + git commit -am "scanner: bump to X.Y.Z" + git push + ``` + +4. **Tag and push** (this triggers the publish): + ```bash + git tag scanner-vX.Y.Z + git push origin scanner-vX.Y.Z + ``` + +CI will: +- Run lint, format, type check, and unit tests +- Verify the tag matches `pyproject.toml` +- Build and publish to [PyPI](https://pypi.org/project/mpak-scanner/) +- Build and push Docker image to `ghcr.io/nimblebraininc/mpak-scanner:{version}` and `:latest` + +See [`scanner-publish.yml`](../../.github/workflows/scanner-publish.yml). + +### Docker Image + +The Docker image includes all external security tools (Syft, Grype, TruffleHog, ESLint, Bandit, GuardDog) and installs `mpak-scanner` from PyPI. + +```bash +# Pull from GHCR +docker pull ghcr.io/nimblebraininc/mpak-scanner:latest + +# Run a scan +docker run --rm -v /path/to/bundle.mcpb:/bundle.mcpb ghcr.io/nimblebraininc/mpak-scanner scan /bundle.mcpb +``` + +For production deployment to ECR/K8s, see `deployments/mpak/`. + ## Related Projects - [mpak registry](https://mpak.dev) - Search, download, and publish MCP bundles diff --git a/apps/scanner/pyproject.toml b/apps/scanner/pyproject.toml index 071040c..5a8edd1 100644 --- a/apps/scanner/pyproject.toml +++ b/apps/scanner/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mpak-scanner" -version = "0.2.4" +version = "0.2.6" description = "Security scanner for MCP bundles. Powers mpak Certified verification." readme = "README.md" license = "Apache-2.0" diff --git a/apps/scanner/src/mpak_scanner/__init__.py b/apps/scanner/src/mpak_scanner/__init__.py index e745f3a..ab433ab 100644 --- a/apps/scanner/src/mpak_scanner/__init__.py +++ b/apps/scanner/src/mpak_scanner/__init__.py @@ -3,5 +3,11 @@ from mpak_scanner.models import ComplianceLevel, ControlResult, SecurityReport from mpak_scanner.scanner import scan_bundle -__version__ = "0.2.4" +try: + from importlib.metadata import version as _get_version + + __version__ = _get_version("mpak-scanner") +except Exception: + __version__ = "0.0.0" + __all__ = ["scan_bundle", "SecurityReport", "ControlResult", "ComplianceLevel"] diff --git a/apps/scanner/src/mpak_scanner/scanner.py b/apps/scanner/src/mpak_scanner/scanner.py index 9da8c5b..8fd6488 100644 --- a/apps/scanner/src/mpak_scanner/scanner.py +++ b/apps/scanner/src/mpak_scanner/scanner.py @@ -69,8 +69,13 @@ logger = logging.getLogger(__name__) -# Version of the scanner -SCANNER_VERSION = "0.2.4" +# Version of the scanner (derived from pyproject.toml via importlib.metadata) +try: + from importlib.metadata import version as _get_version + + SCANNER_VERSION = _get_version("mpak-scanner") +except Exception: + SCANNER_VERSION = "0.0.0" # Domain groupings for controls (matches MTF v0.1 spec) DOMAINS = {