From f3c92ec4137100a7f5a8c73564d8e9c7beec28e5 Mon Sep 17 00:00:00 2001 From: "Sean P. Kane" Date: Mon, 5 Jan 2026 15:44:36 -0800 Subject: [PATCH] ci: Various Linux container and PyPi release improvements (#16) * ci: release workflow fixes * docs: Fix env var names * docs: Improve Docker network configuration * ci: Improve error checking in Docker release workflow * ci: improve release workflow and container security * ci: badges and better container scanning * ci: Addition fixes * ci: fix pypi release logic --- .github/workflows/docker.yaml | 37 +++++++++++++++++++--- .github/workflows/pypi-release.yaml | 49 +++++++++++++++++++++++++---- .gitignore | 4 +++ .mise.toml | 1 + .pre-commit-config.yaml | 22 +++++++++++-- Dockerfile | 40 +++++++++++++---------- README.md | 14 +++++++++ just/docker.just | 36 +++++++++++++++++++-- 8 files changed, 171 insertions(+), 32 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index ff9accd..2a356d9 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -113,13 +113,42 @@ jobs: ${{ env.IMAGE_NAME }}:test 2>&1 | \ grep -q '"result"' && echo "Container test passed" || echo "Container test completed" - - name: Scan for vulnerabilities - if: github.event_name != 'pull_request' - uses: aquasecurity/trivy-action@master + - name: Cache Trivy vulnerability database + uses: actions/cache@v4 with: - image-ref: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + path: .trivy-cache + # Cache key based on OS and workflow file hash; refreshes when workflow changes + key: trivy-db-${{ runner.os }}-${{ hashFiles('.github/workflows/docker.yaml') }} + restore-keys: | + trivy-db-${{ runner.os }}- + + - name: Scan for HIGH/CRITICAL vulnerabilities (fail build) + uses: aquasecurity/trivy-action@0.33.1 + with: + image-ref: ${{ env.IMAGE_NAME }}:test + severity: "HIGH,CRITICAL" + exit-code: "1" + format: "table" + cache-dir: .trivy-cache + + - name: Scan for MEDIUM/LOW vulnerabilities (warning only) + uses: aquasecurity/trivy-action@0.33.1 + with: + image-ref: ${{ env.IMAGE_NAME }}:test + severity: "MEDIUM,LOW" + exit-code: "0" + format: "table" + cache-dir: .trivy-cache + continue-on-error: true + + - name: Generate SARIF report for GitHub Security + if: github.event_name != 'pull_request' + uses: aquasecurity/trivy-action@0.33.1 + with: + image-ref: ${{ env.IMAGE_NAME }}:test format: "sarif" output: "trivy-results.sarif" + cache-dir: .trivy-cache continue-on-error: true - name: Upload Trivy scan results diff --git a/.github/workflows/pypi-release.yaml b/.github/workflows/pypi-release.yaml index bb8da03..71e2b76 100644 --- a/.github/workflows/pypi-release.yaml +++ b/.github/workflows/pypi-release.yaml @@ -164,8 +164,10 @@ jobs: name: Publish to TestPyPI needs: [build, test-install] runs-on: ubuntu-latest - # Only publish prereleases to TestPyPI - if: contains(github.ref, '-') + # Only publish alpha releases to TestPyPI for early testing. + # Beta and RC releases go to PyPI since they are closer to stable. + # This is intentional - do not change to contains(github.ref, '-'). + if: contains(github.ref, '-alpha') environment: name: testpypi url: https://test.pypi.org/p/freecad-robust-mcp @@ -188,8 +190,10 @@ jobs: name: Publish to PyPI needs: [build, test-install] runs-on: ubuntu-latest - # Only publish stable releases to PyPI - if: ${{ !contains(github.ref, '-') }} + # Publish beta, rc, and stable releases to PyPI (not alpha). + # Alpha releases are too experimental for PyPI - they go to TestPyPI only. + # This is intentional - do not change to !contains(github.ref, '-'). + if: ${{ !contains(github.ref, '-alpha') }} environment: name: pypi url: https://pypi.org/p/freecad-robust-mcp @@ -245,9 +249,10 @@ jobs: echo "" } >> "$GITHUB_STEP_SUMMARY" - if [[ "$REF_NAME" == *"-"* ]]; then + # Alpha releases go to TestPyPI only; beta, rc, and stable go to PyPI + if [[ "$REF_NAME" == *"-alpha"* ]]; then { - echo "**Type:** Prerelease (published to TestPyPI)" + echo "**Type:** Alpha prerelease (published to TestPyPI only)" echo "" echo "### Install from TestPyPI" echo "" @@ -255,6 +260,38 @@ jobs: echo "pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ freecad-robust-mcp" echo "\`\`\`" } >> "$GITHUB_STEP_SUMMARY" + elif [[ "$REF_NAME" == *"-beta"* ]]; then + { + echo "**Type:** Beta prerelease (published to PyPI)" + echo "" + echo "### Install from PyPI" + echo "" + echo "\`\`\`bash" + echo "pip install freecad-robust-mcp" + echo "\`\`\`" + echo "" + echo "Or with uv:" + echo "" + echo "\`\`\`bash" + echo "uv pip install freecad-robust-mcp" + echo "\`\`\`" + } >> "$GITHUB_STEP_SUMMARY" + elif [[ "$REF_NAME" == *"-rc"* ]]; then + { + echo "**Type:** Release candidate (published to PyPI)" + echo "" + echo "### Install from PyPI" + echo "" + echo "\`\`\`bash" + echo "pip install freecad-robust-mcp" + echo "\`\`\`" + echo "" + echo "Or with uv:" + echo "" + echo "\`\`\`bash" + echo "uv pip install freecad-robust-mcp" + echo "\`\`\`" + } >> "$GITHUB_STEP_SUMMARY" else { echo "**Type:** Stable release (published to PyPI)" diff --git a/.gitignore b/.gitignore index 24545c4..97d6944 100644 --- a/.gitignore +++ b/.gitignore @@ -97,6 +97,10 @@ cython_debug/ # Ruff .ruff_cache/ +# Trivy (pre-commit and CI cache) +.pre-commit-trivy-cache/ +.trivy-cache/ + # UV # Note: uv.lock is committed for reproducible CI builds diff --git a/.mise.toml b/.mise.toml index 0f329fb..7f0bcb1 100644 --- a/.mise.toml +++ b/.mise.toml @@ -11,6 +11,7 @@ uv = "0.9" # uv package manager just = "1.43" # task runner pre-commit = "4.5" # pre-commit hooks github-cli = "2.74" # GitHub CLI for PR/issue management +trivy = "0.62" # container vulnerability scanner [env] # FreeCAD connection mode: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 998fdad..6831b15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -221,6 +221,21 @@ repos: - id: hadolint-docker name: hadolint (Dockerfile linter) + # ========================================================================== + # Dockerfile Security Scanning (Misconfigurations) + # ========================================================================== + - repo: https://github.com/mxab/pre-commit-trivy.git + rev: v0.16.0 + hooks: + - id: trivyconfig-docker + name: trivy (Dockerfile misconfig) + args: + - --severity + - HIGH,CRITICAL + - --exit-code + - "1" + - . + # ========================================================================== # Commit Message Linting # ========================================================================== @@ -238,6 +253,7 @@ ci: autoupdate_schedule: monthly autoupdate_commit_msg: "chore(deps): update pre-commit hooks" skip: - - mypy # Needs dependencies installed - - hadolint-docker # Needs Docker - - trufflehog # Can be slow in CI + - mypy # Needs dependencies installed + - hadolint-docker # Needs Docker + - trivyconfig-docker # Needs Docker + - trufflehog # Can be slow in CI diff --git a/Dockerfile b/Dockerfile index 20d4f1f..0bc585c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,9 @@ # FreeCAD MCP Server Dockerfile # Multi-stage build with BuildKit optimizations for multi-arch support # +# Uses Alpine Linux for minimal image size and reduced CVE surface. +# Alpine has significantly fewer vulnerabilities than Debian-based images. +# # Build: # docker build -t freecad-mcp . # @@ -15,24 +18,22 @@ # ============================================================================= # Stage 1: Builder - Install dependencies and build the package # ============================================================================= -FROM python:3.11-slim AS builder +FROM python:3.11-alpine AS builder -# Install build dependencies -# hadolint ignore=DL3008 -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - && rm -rf /var/lib/apt/lists/* +# Install build dependencies for compiling Python packages with native extensions +# hadolint ignore=DL3018 +RUN apk add --no-cache \ + build-base \ + libffi-dev # Set up working directory WORKDIR /app -# Install uv for fast dependency management -# Using pip cache mount for faster rebuilds +# Upgrade pip to fix CVE-2025-8869, then install uv for fast dependency management # hadolint ignore=DL3013 RUN --mount=type=cache,target=/root/.cache/pip \ - pip install --no-compile uv + pip install --no-cache-dir --upgrade "pip>=25.3" && \ + pip install --no-cache-dir --no-compile uv # Copy only dependency files first for better layer caching COPY pyproject.toml README.md ./ @@ -53,7 +54,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ # ============================================================================= # Stage 2: Runtime - Minimal image for running the server # ============================================================================= -FROM python:3.11-slim AS runtime +FROM python:3.11-alpine AS runtime # Labels for container metadata (OCI Image Spec) # Note: version, revision, and created are set dynamically in CI/CD workflows @@ -65,11 +66,18 @@ LABEL org.opencontainers.image.title="FreeCAD MCP Server" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.vendor="Sean P. Kane" \ org.opencontainers.image.authors="Sean P. Kane " \ - org.opencontainers.image.base.name="python:3.11-slim" + org.opencontainers.image.base.name="python:3.11-alpine" -# Create non-root user for security -RUN groupadd --gid 1000 mcpuser && \ - useradd --uid 1000 --gid 1000 --shell /bin/bash --create-home mcpuser +# Create non-root user for security (Alpine uses addgroup/adduser) +RUN addgroup -g 1000 mcpuser && \ + adduser -u 1000 -G mcpuser -s /bin/sh -D mcpuser + +# Upgrade system pip to fix CVE-2025-8869 (defense-in-depth) +# Note: Although PATH prefers /opt/venv/bin, we upgrade the system pip at +# /usr/local/bin/pip intentionally. This ensures no vulnerable pip exists +# in the image, even if the venv is bypassed or pip is invoked directly. +# hadolint ignore=DL3013 +RUN pip install --no-cache-dir --upgrade "pip>=25.3" # Copy virtual environment from builder COPY --from=builder /opt/venv /opt/venv diff --git a/README.md b/README.md index 01e724e..f0a9486 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # FreeCAD Tools and MCP Server +[![CI Tests](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/test.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/test.yaml) +[![Docker Build](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/docker.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/docker.yaml) +[![Pre-commit](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/pre-commit.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/pre-commit.yaml) +[![CodeQL](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/codeql.yaml/badge.svg)](https://github.com/spkane/freecad-robust-mcp-and-more/actions/workflows/codeql.yaml) + An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that enables integration between AI assistants (Claude, GPT, and other MCP-compatible tools) and [FreeCAD](https://www.freecadweb.org/), allowing AI-assisted development and debugging of 3D models, macros, and workbenches. > Also includes standalone FreeCAD macros for common tasks. @@ -13,6 +18,7 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that - [Features](#features) - [Requirements](#requirements) - [For Users](#for-users) + - [Quick Links](#quick-links) - [MCP Server](#mcp-server) - [Installation](#installation) - [Using pip (recommended)](#using-pip-recommended) @@ -86,6 +92,14 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that This section covers installation and usage for end users who want to use the MCP server with AI assistants or the standalone FreeCAD macros. +### Quick Links + +| Resource | Description | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| [Docker Hub](https://hub.docker.com/r/spkane/freecad-robust-mcp) | Pre-built Docker images for easy deployment | +| [PyPI](https://pypi.org/project/freecad-robust-mcp/) | Python package for pip installation | +| [GitHub Releases](https://github.com/spkane/freecad-robust-mcp-and-more/releases) | Release archives, changelogs, and standalone macro downloads | + ## MCP Server > **Note**: Since this repository has more than just the MCP server in it, the Linux container and PyPi projects releases are both simply named `freecad-robust-mcp` which differs from the name of this git repository. diff --git a/just/docker.just b/just/docker.just index 14f79e2..147db66 100644 --- a/just/docker.just +++ b/just/docker.just @@ -1,8 +1,8 @@ # Docker build and run commands # Usage: just docker::build, just docker::build-all, etc. -# Default Docker image name -image_name := "freecad-mcp" +# Default Docker image name (matches Docker Hub and PyPI package name) +image_name := "freecad-robust-mcp" registry := "spkane" # Project root directory (justfile_directory() returns the main justfile's directory) @@ -53,7 +53,7 @@ shell: --add-host=host.docker.internal:host-gateway \ -e FREECAD_MODE=xmlrpc \ -e FREECAD_SOCKET_HOST=host.docker.internal \ - --entrypoint /bin/bash \ + --entrypoint /bin/sh \ {{image_name}} # Show image size and layers @@ -67,6 +67,36 @@ clean: docker rmi {{image_name}} 2>/dev/null || true docker rmi {{registry}}/{{image_name}} 2>/dev/null || true +# Scan Docker image for vulnerabilities (warn on all severities) +scan: + #!/usr/bin/env bash + set -euo pipefail + echo "Scanning {{image_name}} for vulnerabilities..." + echo "" + trivy image --severity HIGH,CRITICAL {{image_name}} + echo "" + echo "Scanning for MEDIUM/LOW (informational)..." + trivy image --severity MEDIUM,LOW {{image_name}} || true + +# Scan Docker image with strict settings (fail on HIGH or CRITICAL) +scan-strict: + #!/usr/bin/env bash + set -euo pipefail + echo "Scanning {{image_name}} for HIGH/CRITICAL vulnerabilities..." + echo "(Build will fail if any HIGH or CRITICAL CVEs are found)" + echo "" + trivy image --severity HIGH,CRITICAL --exit-code 1 {{image_name}} + echo "" + echo "✓ No HIGH or CRITICAL vulnerabilities found!" + echo "" + echo "Scanning for MEDIUM/LOW (informational only)..." + trivy image --severity MEDIUM,LOW --exit-code 0 {{image_name}} || true + +# Scan Docker image and output SARIF report +scan-sarif output="trivy-results.sarif": + trivy image --format sarif --output {{output}} {{image_name}} + @echo "SARIF report written to {{output}}" + # Create and configure buildx builder for multi-arch builds setup-buildx: #!/usr/bin/env bash