diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83d513b8..6e8b02cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,7 @@ jobs: ! command -v node ! command -v npm ! command -v git + ! command -v curl ! command -v pipenv test "$(id -u)" -eq 10001 test -x /app/scripts/run.sh @@ -63,3 +64,14 @@ jobs: python -c "import asyncio, os; from algosdk import account, mnemonic; private_key, _ = account.generate_account(); os.environ[\"ALGO_MNEMONIC\"] = mnemonic.from_private_key(private_key); import app; assert asyncio.run(app.status())[\"version\"] == app.VERSION" python -m pip check ' + - name: Scan production image + run: | + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f \ + image \ + --no-progress \ + --scanners vuln,secret \ + --severity HIGH,CRITICAL \ + --exit-code 1 \ + "cometa-backend:${COMETA_IMAGE_TAG:-local}" diff --git a/BOARD.md b/BOARD.md index 1aad67b2..359197ba 100644 --- a/BOARD.md +++ b/BOARD.md @@ -27,7 +27,7 @@ | Precision-safe pricing | done | Decimal observations, provenance, freshness policy, guarded legacy boundary | | Provider resilience | done | Typed fallback errors, bounded stale data, retry classification, circuit breaker | | Replay identity | done | Deterministic nested event IDs and collection-level uniqueness constraints | -| Container baseline | done | Digest-pinned base, unprivileged user, healthcheck, bounded logs, Docker exclusions | +| Container baseline | done | Digest-pinned Alpine base, multi-stage non-root runtime, healthcheck, image exclusions, Trivy CI gate | | API hardening | done | Fail-closed header authentication, trusted hosts, explicit CORS policy, bounded LP/asset/wallet requests | | Native Reach decoding | done | Versioned global/local codecs, exact-width integers, deterministic layout tests, no private npm runtime | | Legacy runtime removal | done | CB-077: Node/Reach sidecar and production source bind mount removed | diff --git a/CLAUDE.md b/CLAUDE.md index 91e13a7a..145386d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,7 +11,7 @@ Backend for Cometa — an Algorand DeFi platform handling liquidity pools, token - **Blockchain**: Algorand (py-algorand-sdk, algosdk) - **Contract state**: versioned native Reach decoder over Algorand state - **Deployment**: Docker Compose on VPS -- **Image**: digest-pinned Python 3.12 slim +- **Image**: digest-pinned Python 3.12 Alpine ## Project Structure diff --git a/Dockerfile b/Dockerfile index 6c8c548b..8ef3da3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS builder +FROM python:3.12-alpine@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df AS builder WORKDIR /app @@ -10,9 +10,7 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \ ARG PIPENV_RELEASE=2024.4.0 -RUN apt-get update \ - && apt-get install --yes --no-install-recommends git \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache build-base git libffi-dev COPY Pipfile Pipfile.lock ./ RUN python -m venv /opt/pipenv \ @@ -20,7 +18,7 @@ RUN python -m venv /opt/pipenv \ && /opt/pipenv/bin/pipenv verify \ && /opt/pipenv/bin/pipenv sync -FROM python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS runtime +FROM python:3.12-alpine@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df AS runtime WORKDIR /app @@ -30,8 +28,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ COPY --from=builder /app/.venv /app/.venv -RUN groupadd --system --gid 10001 cometa \ - && useradd --system --uid 10001 --gid cometa --home-dir /home/cometa --create-home cometa +RUN addgroup -S -g 10001 cometa \ + && adduser -S -D -u 10001 -G cometa -h /home/cometa cometa COPY --chown=cometa:cometa app.py env.py telegram_bot.py ./ COPY --chown=cometa:cometa api ./api diff --git a/README.md b/README.md index edc26a32..7013dfad 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@
@@ -42,6 +42,7 @@ MongoDB—into stable, query-oriented API models for the product frontend. | **Resilient price routing** | Vestige and Tinyman payloads are validated with provenance and bounded staleness; retry classification and a guarded Vestige refresh prevent failure storms. | | **Operational boundaries** | Selected latency-sensitive SDK calls leave the event loop through executors; wallet fan-out is cached and bounded; background workers reconcile chain state without coupling reads to refresh latency. | | **Versioned chain decoding** | Reach 0.1.11 state is decoded natively from Algorand with explicit per-version layouts, exact-width integers, and fail-closed schema validation. | +| **Supply-chain hardening** | The digest-pinned Alpine image is multi-stage, non-root, and Python-only; CI smoke-tests it and rejects high/critical vulnerabilities or embedded secrets. | The codebase combines a production system's real constraints with incremental modernization: pure domain modules and strict typing sit beside legacy adapters, @@ -134,9 +135,10 @@ This single command runs: state-codec and security-boundary tests. CI repeats those checks on every pull request and every push to `main`, verifies -the lockfile, and validates the Compose configuration. The focused coverage -ratchet is currently 75%; it measures maintained domain and infrastructure -modules rather than presenting a misleading whole-repository number. +the lockfile and Compose configuration, builds and smoke-tests the production +image, and scans it with Trivy. The focused coverage ratchet is currently 75%; +it measures maintained domain and infrastructure modules rather than presenting +a misleading whole-repository number. Useful individual targets are `make lint`, `make format-check`, `make typecheck`, and `make test`. diff --git a/scripts/run.sh b/scripts/run.sh index 1d6d37bb..4231a5cf 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,13 +1,13 @@ -#!/usr/bin/env bash -set -Eeuo pipefail +#!/bin/sh +set -eu -readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" -readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)" +script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" +project_root="$(dirname -- "${script_dir}")" -cd -- "${PROJECT_ROOT}" +cd -- "${project_root}" -if [[ ! -f "app.py" ]]; then - printf 'ERROR: application entrypoint not found: %s/app.py\n' "${PROJECT_ROOT}" >&2 +if [ ! -f "app.py" ]; then + printf 'ERROR: application entrypoint not found: %s/app.py\n' "${project_root}" >&2 exit 1 fi