diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b3e6df3..56ba647 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 +FROM mcr.microsoft.com/devcontainers/base:ubuntu26.04 # hadolint ignore=DL3008 RUN set -eux; \ @@ -13,4 +13,9 @@ RUN set -eux; \ apt-get -y purge linux-libc-dev; \ apt-get -y autoremove --purge; \ apt-get clean; \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/*; \ + # Remove Pebble, an unused ACME/TLS test server baked into the base + # image. It is not owned by apt, has no systemd unit, and is never run, + # yet its outdated Go runtime (golang.org/x/net, x/sys, stdlib) is the + # sole source of all fixable HIGH CVEs in the image. + rm -rf /usr/bin/pebble /var/lib/pebble diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..9695bfa --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,29 @@ +{ + "features": { + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": { + "version": "1.0.5", + "resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a", + "integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a" + }, + "ghcr.io/devcontainers/features/common-utils:2": { + "version": "2.5.9", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a", + "integrity": "sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671", + "integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "1.7.1", + "resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6", + "integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6" + }, + "ghcr.io/devcontainers/features/sshd:1": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/sshd@sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee", + "integrity": "sha256:f5251b8e4325f68f7280973c6cd65daff414449c66f240621502d4e8e74eb7ee" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f12e8fd..2b07f1e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,11 +24,16 @@ }, "ghcr.io/devcontainers/features/github-cli:1": { "version": "latest" - } + }, + "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {} }, + "mounts": [ + "source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume" + ], "customizations": { "vscode": { "extensions": [ + "anthropic.claude-code", "github.copilot", "github.copilot-chat", "ms-python.python", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9e9aa81..21fe026 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,12 @@ # Keep devcontainer dependencies up to date version: 2 updates: - # Monitor GitHub Actions + # Monitor GitHub Actions — daily, security-critical supply-chain component - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - open-pull-requests-limit: 5 + interval: "daily" + open-pull-requests-limit: 10 labels: - "dependencies" - "github-actions" @@ -19,12 +19,12 @@ updates: - "minor" - "patch" - # Monitor Docker base images + # Monitor Docker base images — daily, CVEs ship in base images - package-ecosystem: "docker" directory: "/.devcontainer" schedule: - interval: "weekly" - open-pull-requests-limit: 5 + interval: "daily" + open-pull-requests-limit: 10 labels: - "dependencies" - "docker" @@ -36,3 +36,16 @@ updates: update-types: - "minor" - "patch" + + # Monitor devcontainer features — daily, includes claude-code, node, github-cli, common-utils, sshd + - package-ecosystem: "devcontainers" + directory: "/.devcontainer" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "devcontainers" + commit-message: + prefix: "feat" + include: "scope" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03b28cb..164525b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,11 +5,12 @@ on: branches: [ main ] pull_request: branches: [ main ] + schedule: + - cron: '0 2 * * *' + workflow_dispatch: permissions: contents: read - actions: read - security-events: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,6 +21,9 @@ jobs: runs-on: ubuntu-latest name: Build and Validate timeout-minutes: 15 + permissions: + contents: read + security-events: write steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -58,20 +62,34 @@ jobs: --report-path gitleaks.sarif - name: Upload gitleaks SARIF - if: always() + if: > + always() && + (github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository) uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 with: sarif_file: gitleaks.sarif category: security-analysis/gitleaks - name: Build Docker image - run: docker build --pull -t dev-template:latest .devcontainer/ + run: | + docker build --pull -t "dev-template:${{ github.sha }}" .devcontainer/ + docker save "dev-template:${{ github.sha }}" -o /tmp/dev-template.tar + + - name: Upload image artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: docker-image-${{ github.sha }} + path: /tmp/dev-template.tar + retention-days: 1 test: runs-on: ubuntu-latest name: Test Devcontainer needs: build timeout-minutes: 20 + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -84,7 +102,7 @@ jobs: push: never runCmd: | set -eu - for cmd in python3 node npm gh opencode curl jq; do + for cmd in python3 node npm gh opencode curl jq claude; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "::error::$cmd is missing" exit 1 @@ -97,3 +115,98 @@ jobs: opencode --version curl --version | head -1 jq --version + claude --version + + scan: + runs-on: ubuntu-latest + name: Security and SBOM Analysis + needs: build + timeout-minutes: 20 + permissions: + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Download image artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: docker-image-${{ github.sha }} + path: /tmp + + - name: Load Docker image + run: docker load -i /tmp/dev-template.tar + + - name: Run Trivy vulnerability scanner (image) + id: trivy-image + continue-on-error: true + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: 'dev-template:${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL' + ignore-unfixed: true + limit-severities-for-sarif: true + exit-code: '1' + cache: false + + - name: Upload Trivy image scan results + if: > + always() && + (github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository) + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + with: + sarif_file: 'trivy-results.sarif' + category: 'security-analysis/trivy-image' + + - name: Generate SBOM + if: always() + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: 'dev-template:${{ github.sha }}' + format: 'spdx-json' + output-file: 'sbom.spdx.json' + + - name: Upload SBOM as artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: sbom-${{ github.sha }} + path: sbom.spdx.json + retention-days: 30 + + - name: Run Trivy vulnerability scanner (filesystem) + id: trivy-fs + continue-on-error: true + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-fs-results.sarif' + severity: 'CRITICAL' + ignore-unfixed: true + limit-severities-for-sarif: true + exit-code: '1' + cache: false + + - name: Upload Trivy filesystem scan results + if: > + always() && + (github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository) + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 + with: + sarif_file: 'trivy-fs-results.sarif' + category: 'security-analysis/trivy-filesystem' + + - name: Fail job if any Trivy scan found CRITICAL vulnerabilities + if: steps.trivy-image.outcome == 'failure' || steps.trivy-fs.outcome == 'failure' + run: | + echo "::error::Trivy found CRITICAL vulnerabilities. Review the Security tab for details." + exit 1 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 5843e0a..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Security Analysis - -on: - schedule: - - cron: '0 2 * * *' - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -permissions: - contents: read - security-events: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - security-scan: - runs-on: ubuntu-latest - name: Security and SBOM Analysis - timeout-minutes: 20 - steps: - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Build Docker image for scanning - id: build - continue-on-error: true - run: | - IMAGE_NAME="dev-template:${{ github.sha }}" - docker build --pull -t "$IMAGE_NAME" .devcontainer/ - echo "IMAGE_NAME=$IMAGE_NAME" >> "$GITHUB_ENV" - - - name: Run Trivy vulnerability scanner (image) - if: steps.build.outcome == 'success' - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - with: - image-ref: '${{ env.IMAGE_NAME }}' - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH' - exit-code: '0' - - - name: Upload Trivy image scan results - if: steps.build.outcome == 'success' - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 - with: - sarif_file: 'trivy-results.sarif' - category: 'security-analysis/trivy-image' - - - name: Generate SBOM - if: steps.build.outcome == 'success' - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 - with: - image: '${{ env.IMAGE_NAME }}' - format: 'spdx-json' - output-file: 'sbom.spdx.json' - - - name: Upload SBOM as artifact - if: steps.build.outcome == 'success' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: sbom-${{ github.sha }} - path: sbom.spdx.json - retention-days: 30 - - - name: Run Trivy vulnerability scanner (filesystem) - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - with: - scan-type: 'fs' - scan-ref: '.' - format: 'sarif' - output: 'trivy-fs-results.sarif' - severity: 'CRITICAL,HIGH' - exit-code: '0' - - - name: Upload Trivy filesystem scan results - if: always() - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4 - with: - sarif_file: 'trivy-fs-results.sarif' - category: 'security-analysis/trivy-filesystem' diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8f0798f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# AGENTS.md + +Guidance for AI agents and automated contributors working in this repository. + +## Project overview + +This repository is a lean development container template for GitHub Codespaces +and local VS Code Dev Containers. It defines a reproducible Ubuntu 26.04 +environment with Python 3, Node.js LTS, GitHub CLI, Claude Code CLI, and +OpenCode TUI. The repository contains no application source code — all +meaningful files are configuration. + +## Repository layout + +``` +.devcontainer/ + Dockerfile Base image pin + apt package installs (python3, venv, pip, sudo) + devcontainer.json Feature declarations, VS Code extensions, volume mounts, lifecycle commands +.github/ + dependabot.yml Daily Dependabot updates for GitHub Actions, Docker images, devcontainer features + workflows/ + ci.yml Single CI pipeline: build → test → scan (see CI section below) +.gitignore Excludes OS files, editor dirs, Claude session state (.claude/, CLAUDE.md) +AGENTS.md This file +LICENSE MIT +README.md Human-facing documentation +``` + +## Conventions + +### Commit messages +Follow Conventional Commits as used throughout the repo history: +- `feat(devcontainer):` — devcontainer features, base image, extensions +- `ci:` / `ci(deps):` — workflow changes, dependency bumps +- `docker:` — Dockerfile or base image changes +- `docs:` — README, AGENTS.md, comments + +### JSON +`devcontainer.json` is **strict JSON** (no comments, no trailing commas). +Validate with `python3 -m json.tool .devcontainer/devcontainer.json` before +committing. + +### GitHub Actions +All `uses:` entries **must** be pinned to a full 40-character commit SHA with a +`# vX.Y.Z` or `# vX` trailing comment, e.g.: +```yaml +uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +``` +Never use a floating tag (`@main`, `@v4`, `@latest`). Dependabot keeps SHAs +current via daily PRs. + +### Dockerfile +- One `FROM` line; base image tag must be a versioned MCR tag so Dependabot + can track it (e.g. `ubuntu26.04`, `ubuntu-24.04`; note MCR dropped the + hyphen for 26.04+). +- `hadolint` runs in CI; respect its rules or add a targeted + `# hadolint ignore=DLXXXX` with a justification comment. +- Keep the package list minimal — features (not the Dockerfile) install + language runtimes and CLI tools. + +## CI pipeline (`ci.yml`) + +Three sequential jobs share a single Docker build via artifact: + +| Job | `needs` | What it does | +|-----|---------|-------------| +| `build` | — | hadolint → gitleaks (SARIF) → `docker build` → `docker save` → upload artifact | +| `test` | `build` | Full devcontainer build via `devcontainers/ci`; smoke-tests all required binaries | +| `scan` | `build` | `docker load` → Trivy image scan (gates on CRITICAL) → SBOM → Trivy fs scan (gates on CRITICAL) | + +Triggers: `push`/`pull_request` on `main`, daily `schedule` (02:00 UTC), +`workflow_dispatch`. + +**SARIF uploads** are skipped for fork PRs (read-only token) via: +```yaml +if: > + always() && + (github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository) +``` + +## Local validation commands + +Run these before committing, in order: + +```bash +# 1. JSON validity +python3 -m json.tool .devcontainer/devcontainer.json > /dev/null + +# 2. Dockerfile lint +docker run --rm -i hadolint/hadolint < .devcontainer/Dockerfile + +# 3. Base image build (mirrors ci.yml build job) +docker build --pull -t dev-template:local .devcontainer/ + +# 4. Full devcontainer build + smoke test (mirrors ci.yml test job) +# Requires network egress to ghcr.io and registry.npmjs.org +npx -y @devcontainers/cli up --workspace-folder . --remove-existing-container +npx @devcontainers/cli exec --workspace-folder . -- bash -c ' + set -eu + for cmd in python3 node npm gh opencode curl jq claude; do + command -v "$cmd" || { echo "MISSING: $cmd"; exit 1; } + done + claude --version + echo "All checks passed" +' +``` + +## Guardrails + +- **Keep the CI smoke test in sync.** If you add a new tool via a feature or + `updateContentCommand`, add it to the `for cmd in ...` loop in `ci.yml` + (`test` job, `runCmd` block). +- **No secrets in tracked files.** `.claude/` and `CLAUDE.md` are gitignored; + do not commit authentication tokens, API keys, or credential files. +- **Do not remove SHA pins.** Floating action refs will fail code review. +- **Do not push directly to `main`.** All changes go through a PR from a + feature branch. +- **Volume mount target.** The Claude Code volume is mounted at + `/home/vscode/.claude` (matching `remoteUser: vscode`). If the remote user + ever changes, the mount target must be updated to match. diff --git a/README.md b/README.md index c4a9912..16d64fa 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,18 @@ Codespaces and local VS Code Dev Containers. ## Features -- Ubuntu 24.04 base image +- Ubuntu 26.04 (Resolute) base image - Single-image devcontainer build based on `.devcontainer/Dockerfile` - Core tooling for general development work: - Python 3 with `venv` and `pip` - Node.js LTS with `npm` - GitHub CLI + - Claude Code CLI (authenticated session persisted across rebuilds) - OpenCode TUI installed via `updateContentCommand` (cached by prebuilds) - Bash shell with common utilities - Build essentials (`gcc`, `make`, and related packages) via base image - VS Code extensions: + - Claude Code - Python and Pylance - GitHub Copilot and Copilot Chat - YAML @@ -27,13 +29,18 @@ Codespaces and local VS Code Dev Containers. The devcontainer is tuned for fast Codespaces startup: -- Minimal feature set: only `common-utils`, `sshd`, `node`, and `github-cli` - features are installed — Docker-in-Docker and git features are omitted +- Minimal feature set: only `common-utils`, `sshd`, `node`, `github-cli`, and + `claude-code` features are installed — Docker-in-Docker and git features are + omitted - `common-utils` configured with zsh and Oh My Zsh disabled -- `curl`, `wget`, `jq`, and `git` sourced from the base image and - `common-utils` feature — not re-installed in the Dockerfile -- OpenCode TUI installed in `updateContentCommand` instead of `postCreateCommand`, - so it is cached during Codespaces prebuilds and not re-run on every start +- `curl`, `wget`, `jq`, and `git` sourced from the base image — not + re-installed in the Dockerfile +- OpenCode TUI installed in `updateContentCommand` instead of + `postCreateCommand`, so it is cached during Codespaces prebuilds and not + re-run on every start +- Claude Code authentication stored in a named volume + (`claude-code-config-${devcontainerId}`) so sign-in survives container + rebuilds - Small, targeted extension set Approximate startup time: **~45–75 seconds** without prebuilds and @@ -70,23 +77,28 @@ created from `main` will start in approximately **10–25 seconds**. 1. Click "Code" button on the GitHub repository 2. Select "Create codespace on main" 3. Wait for the environment to build +4. Run `claude` in the integrated terminal and follow the authentication prompt ### VS Code Local Dev Containers 1. Clone this repository 2. Open in VS Code 3. Click "Reopen in Container" when prompted +4. Run `claude` in the integrated terminal and follow the authentication prompt ## Repository Structure - `.devcontainer/devcontainer.json`: main devcontainer definition, features, VS Code extensions, and lifecycle commands - `.devcontainer/Dockerfile`: minimal image customization for Python packages -- `.github/workflows/ci.yml`: CI checks for Dockerfile linting, secret - scanning, image build, and devcontainer smoke testing -- `.github/workflows/security.yml`: scheduled and on-push Trivy image and - filesystem scans, plus SBOM generation -- `.github/dependabot.yml`: weekly GitHub Actions and Docker base-image updates +- `.github/workflows/ci.yml`: CI pipeline — Dockerfile linting, secret + scanning, image build (artifact-shared across jobs), devcontainer smoke + testing, Trivy vulnerability scanning (gates on CRITICAL), SBOM generation, + and scheduled daily security scans +- `.github/dependabot.yml`: daily updates for GitHub Actions, Docker base + images, and devcontainer features +- `AGENTS.md`: guidance for AI agents and automated contributors working in + this repository ## Using as a Template @@ -124,8 +136,8 @@ To add Docker support when you need it: } ``` -If you add new features or packages, keep the CI workflow in sync with any new -tooling expectations you want validated during the container smoke test. +If you add new features or packages, keep the CI smoke test in sync with any +new tooling expectations you want validated during the container smoke test. ## License