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
93 changes: 93 additions & 0 deletions .github/workflows/scanner-publish.yml
Original file line number Diff line number Diff line change
@@ -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
35 changes: 19 additions & 16 deletions apps/scanner/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion apps/scanner/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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"]
55 changes: 55 additions & 0 deletions apps/scanner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion apps/scanner/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 7 additions & 1 deletion apps/scanner/src/mpak_scanner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
9 changes: 7 additions & 2 deletions apps/scanner/src/mpak_scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading