diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..26e5714 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,37 @@ +version: 2 + +# Dependency-freshness & supply-chain policy: docs/dependency-policy.md +# +# Dependabot owns only the routine minor/patch lane. A Go major is a module-path +# (/vN) change driven by `gomajor` (see `make deps-majors`), which Dependabot +# cannot perform — majors it raises are SIGNALS, not merge-ready PRs. Freshness +# is tracked by `make deps-outdated` (go-mod-outdated), not by this file. +# +# Every ecosystem carries a 7-day cooldown so a freshly-published (possibly +# compromised) release is not ingested immediately. Security updates are exempt +# from the cooldown by Dependabot's design. Majors are no longer ignored. + +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + minor-and-patch: + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + minor-and-patch: + update-types: + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ad8614..0085b14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,12 +10,69 @@ permissions: jobs: build-test: + name: Build & test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod + cache: true - run: go build ./... - run: go vet ./... - run: go test ./... + + # WS49 — vulnerability gate (osv-scanner). + # + # Landed REPORT-ONLY: the initial scan is not clean — stdlib 1.26.3 carries + # GO-2026-5037 / GO-2026-5039 (fixed in 1.26.4), handed to the catch-up upgrade + # (WS51). Once that lands, the go directive moves to 1.26.4, the scan goes + # clean, and this job flips to blocking: drop `continue-on-error` and add + # `osv-scan` to the `all-checks` needs: list. + osv-scan: + name: Vulnerability scan (osv-scanner) + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Run osv-scanner (pinned; local parity with `make vuln-scan`) + run: make vuln-scan + + # WS50 — non-blocking dependency-freshness report. Surfaces drift on every PR + # without flaking the build; enforcement stays with review + catch-up upgrades. + deps-report: + name: Dependency freshness (report-only) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Report outdated direct deps + run: | + { + echo '### Outdated direct dependencies' + echo '```' + make deps-outdated || true + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # Aggregate required check. osv-scan is intentionally NOT here yet (report-only + # per WS49); the WS51 upgrade adds it once the tree is clean. + all-checks: + name: All checks passed + runs-on: ubuntu-latest + needs: [build-test] + if: always() + steps: + - name: Verify required jobs succeeded + run: | + if [ "${{ needs.build-test.result }}" != "success" ]; then + echo "build-test did not succeed" + exit 1 + fi diff --git a/.gitignore b/.gitignore index 1760365..db8d839 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ bin/ *.test coverage.out + +# Built adapter binary (publish via CI; never commit) +/criteria-adapter-shell diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3489a1e --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +# Makefile — criteria-adapter-shell +# +# Build/test plus the security & dependency-freshness tooling required by the +# Criteria supply-chain policy (mirrors the monorepo's WS49/WS50). See +# docs/dependency-policy.md for the rules these targets enforce. +# +# Tool versions are pinned here (no floating @latest) so CI and local runs +# resolve the SAME version — reproducibility and supply-chain safety. This +# single-module repo pins tools in the Makefile rather than a separate tools/ +# go.mod (the monorepo's mechanism); bump these deliberately. + +GO ?= go + +OSV_SCANNER_VERSION := v2.3.8 +GO_MOD_OUTDATED_VERSION := v0.9.0 +GOMAJOR_VERSION := v0.15.0 + +.PHONY: help build test vet tidy lint vuln-scan deps-outdated deps-majors + +help: ## List targets + @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN{FS=":.*?## "}{printf " %-16s %s\n", $$1, $$2}' + +build: ## Build the adapter + $(GO) build ./... + +test: ## Run tests + $(GO) test ./... + +vet: ## go vet + $(GO) vet ./... + +tidy: ## go mod tidy + $(GO) mod tidy + +# --- Security gate (WS49) ----------------------------------------------------- + +vuln-scan: ## Scan for known vulnerabilities (osv-scanner; local parity with CI osv-scan) + $(GO) run github.com/google/osv-scanner/v2/cmd/osv-scanner@$(OSV_SCANNER_VERSION) scan source -r . + +# --- Dependency freshness (WS50) --------------------------------------------- +# The source of truth for "are we on latest major.minor", not Dependabot. + +deps-outdated: ## Report direct deps behind their latest minor/patch (go-mod-outdated) + $(GO) list -u -m -json all | $(GO) run github.com/psampaz/go-mod-outdated@$(GO_MOD_OUTDATED_VERSION) -update -direct + +deps-majors: ## List available major-version (/vN) upgrades (gomajor); apply per dependency-policy + $(GO) run github.com/icholy/gomajor@$(GOMAJOR_VERSION) list diff --git a/README.md b/README.md index 8546042..e9b8cca 100644 --- a/README.md +++ b/README.md @@ -9,38 +9,105 @@ It hardens the spawned process: an env allowlist, `PATH` sanitization (`command_path`), per-step timeouts, bounded stdout/stderr capture, and working-directory confinement (under `$HOME` or `CRITERIA_SHELL_ALLOWED_PATHS`). -## Usage +## Install + +The adapter is published as a signed, multi-platform OCI artifact. Pin it in your +workflow and lock it: + +```bash +criteria adapter lock # pins digest + signer in .criteria.lock.hcl +``` + +Supported platforms: `linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`. + +## Setup (adapter configuration) + +Declare the adapter and bind it to an environment. The shell adapter takes **no +adapter-level `config {}` keys** — all behavior is controlled per step via the +inputs below. The runtime working-directory allowlist is controlled by the host: ```hcl -adapter "shell" "ci" {} +adapter "shell" "ci" { + source = "ghcr.io/brokenbots/criteria-adapter-shell" + version = "0.5.x" +} +``` + +| Host control | How | Effect | +| --- | --- | --- | +| `CRITERIA_SHELL_ALLOWED_PATHS` | env var on the adapter process | Extra roots (besides `$HOME`) that `working_directory` may resolve under. | + +Secrets referenced by a command are delivered over the Criteria secret channel +(per-session `secrets {}` or per-step secret inputs); their values are redacted +from streamed output. + +## Step inputs +| Input | Required | Description | +| --- | --- | --- | +| `command` | **yes** | Command string passed to `sh -c` (Unix) / `cmd /C` (Windows). | +| `env` | no | JSON map of extra env vars. Values starting with `$` inherit from the parent env (e.g. `$GOFLAGS`). `PATH` is reserved — use `command_path`. Build it with `jsonencode({KEY = "$KEY"})`. | +| `command_path` | no | OS-path-separator-delimited directory list that **replaces** `PATH` for the child. | +| `timeout` | no | Hard step timeout, e.g. `10m`. Min `1s`, max `1h`. Default `5m`. | +| `output_limit_bytes` | no | Per-stream capture limit. Range `1024`–`67108864`. Default `4194304` (4 MiB). | +| `working_directory` | no | CWD for the process. Must resolve under `$HOME` or `CRITERIA_SHELL_ALLOWED_PATHS`. | + +```hcl step "build" { adapter = adapter.shell.ci input { command = "go build ./..." timeout = "10m" + env = jsonencode({ GOFLAGS = "$GOFLAGS" }) } } ``` -Inputs: `command` (required), `env` (JSON map; `$VAR` values inherit from the -parent env), `command_path`, `timeout`, `output_limit_bytes`, -`working_directory`. Outputs: `stdout`, `stderr`, `exit_code`. +## Config overrides + +The shell adapter is **fully step-driven** — every knob (`timeout`, +`output_limit_bytes`, `command_path`, `env`, `working_directory`) is a step +input, so each step overrides the defaults independently. There is no adapter +`config {}` block to override. + +## Outputs + +| Output | Description | +| --- | --- | +| `stdout` | Captured stdout (bounded by `output_limit_bytes`). | +| `stderr` | Captured stderr (bounded by `output_limit_bytes`). | +| `exit_code` | Process exit code, as a string. | + +Outcome mapping: exit `0` → `success`, non-zero (or timeout) → `failure`. ## Build & test ```bash -go build -o bin/criteria-adapter-shell . -go test ./... +make build # go build ./... +make test # go test ./... ``` The host-driven conformance suite lives on the [`deferred/conformance`](../../tree/deferred/conformance) branch (it depends on the Criteria host's internal test harness and cannot build standalone yet). +## Security & dependencies + +Supply-chain controls and the dependency-freshness policy are documented in +[SECURITY.md](SECURITY.md) and [docs/dependency-policy.md](docs/dependency-policy.md). +Reproduce the CI security checks locally: + +```bash +make vuln-scan # osv-scanner — known-vulnerability gate (WS49) +make deps-outdated # go-mod-outdated — freshness report (WS50) +make deps-majors # gomajor — available major (/vN) upgrades +``` + ## Publish -Tagging `vX.Y.Z` builds the binary and publishes it as an OCI artifact to +Tagging `vX.Y.Z` runs [`.github/workflows/publish.yml`](.github/workflows/publish.yml), +which cross-builds all four platforms and publishes them as a single +multi-platform, signed OCI artifact to `ghcr.io/brokenbots/criteria-adapter-shell:X.Y.Z` via the reusable [`brokenbots/publish-adapter`](https://github.com/brokenbots/publish-adapter) action. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8fec0c8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security + +## Reporting a vulnerability + +Please report security issues privately via GitHub's **"Report a vulnerability"** +flow (Security → Advisories) on this repository, or email security@brokenbots.net. +Do not open a public issue for an undisclosed vulnerability. + +## Supply-chain controls + +This adapter ships as a **signed, multi-platform OCI artifact** +(`linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`), keyless-signed +via Sigstore/Fulcio with a Rekor transparency-log entry, published by +[`brokenbots/publish-adapter`](https://github.com/brokenbots/publish-adapter). +Consumers can pin the signer in `.criteria.lock.hcl` (`criteria adapter lock`) +so `apply`/`pull` enforce the signature. + +Dependency hygiene is enforced in CI and documented in +[docs/dependency-policy.md](docs/dependency-policy.md): + +- **`osv-scan`** — osv-scanner runs on every PR/push; no shipping known + vulnerabilities. Exceptions are documented + dated in + [`osv-scanner.toml`](osv-scanner.toml). +- **`deps-report`** — non-blocking freshness report (latest major.minor target). +- **Dependabot** — routine minor/patch updates with a 7-day supply-chain cooldown + (security fixes exempt). + +Reproduce the CI security checks locally with `make vuln-scan` and +`make deps-outdated`. diff --git a/criteria-adapter-shell b/criteria-adapter-shell deleted file mode 100755 index 861677f..0000000 Binary files a/criteria-adapter-shell and /dev/null differ diff --git a/docs/dependency-policy.md b/docs/dependency-policy.md new file mode 100644 index 0000000..a3a3b02 --- /dev/null +++ b/docs/dependency-policy.md @@ -0,0 +1,85 @@ +# Dependency-freshness & supply-chain policy + +This adapter follows the same two locked mandates as the +[Criteria monorepo](https://github.com/brokenbots/criteria/blob/main/docs/dependency-policy.md). +Each adapter repo owns its own copy of the policy; this file is the local +authority. It applies to every ecosystem we vendor: this Go module and the +GitHub Actions used in CI. + +## 1. Stay current — latest major.minor + +Be on the **latest major and minor** of every dependency. Patch versions roll up +freely *within* the cooldown rule below. + +The only reason to pin **below** latest is a concrete one: + +- a newer version has a **known security vulnerability** that affects us, or +- a newer version carries a **bug we are actually hit by**. + +Any such pin is a documented, dated exception — see +[Holding a dependency below latest](#holding-a-dependency-below-latest). + +## 2. Defend against supply-chain attacks — 7-day cooldown + +Do **not** adopt any release **newer than 7 days** unless it fixes a known +security issue or a specific bug we're hit by. A freshly-published (and possibly +compromised) release gets a cooldown window before we ingest it. + +**Security updates bypass the cooldown.** Availability of a fix outranks the +supply-chain wait, so security-update PRs (Dependabot's security lane) are not +delayed. + +## How "latest" is determined — Go tooling, not Dependabot + +Dependabot is **not** the source of truth for freshness. It is slow, and it +cannot drive Go **major** upgrades: in Go a major bump is a *module-path change* +(`.../foo` → `.../foo/v2`) plus call-site edits, which neither Dependabot nor a +plain `go get -u` performs. Dependabot is demoted to the routine minor/patch lane +(see below); the freshness picture and major upgrades are driven by Go tooling, +version-pinned in the [`Makefile`](../Makefile) (no floating `@latest`): + +| Command | Tool | Answers | +| --- | --- | --- | +| `make deps-outdated` | [`go-mod-outdated`](https://github.com/psampaz/go-mod-outdated) | Which **direct** deps are behind their latest minor/patch. | +| `make deps-majors` | [`gomajor`](https://github.com/icholy/gomajor) | Which **major** (`/vN`) upgrades are available. | +| `make vuln-scan` | [`osv-scanner`](https://github.com/google/osv-scanner) | Which deps carry a known advisory (WS49). | + +> The monorepo pins these tools in a dedicated `tools/go.mod`. A single-module +> adapter pins them by version in the `Makefile` instead — same guarantee (no +> `@latest`), less ceremony. + +A non-blocking `deps-report` CI job runs `make deps-outdated` on every PR and +posts the result to the job summary, so drift is visible without flaking the +build. Enforcement of "latest" stays with review, not a hard gate — upstream +release cadence would make a hard gate flap. + +Applying the upgrades: + +- **Patch/minor:** `go get @` (honor the 7-day cooldown). +- **Major:** `gomajor get @latest`, which rewrites the `/vN` module path + and import sites; absorb any remaining breaking API changes in source. + +## The update bot — Dependabot (routine minor/patch lane) + +[`.github/dependabot.yml`](../.github/dependabot.yml) is configured to: + +- cover this Go module plus the `github-actions` ecosystem; +- **not** ignore `semver-major` updates (majors it raises are *signals* — drive + them with `gomajor`); +- apply a **7-day cooldown** (`cooldown: default-days: 7`); security updates are + exempt by Dependabot's design; +- group minor + patch updates to keep PR volume sane. + +## Holding a dependency below latest + +To pin a dependency below its latest version, record it as a dated exception so +the decision is auditable and re-reviewed — mirroring the `osv-scanner.toml` +"documented + dated" convention. Add an entry to the table below **and** the +matching `ignore` constraint in `.github/dependabot.yml`, citing the advisory or +bug id and a review date. + +| Dependency | Held at | Reason (advisory / bug) | Review by | +| --- | --- | --- | --- | +| _none_ | | | | + +On the review date the exception must be cleared or re-justified. diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 0000000..fcfab5b --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,22 @@ +# osv-scanner configuration (WS49 — vulnerability gate) +# +# Mandate: no shipping code with known security vulnerabilities. The CI `osv-scan` +# job (.github/workflows/ci.yml) and `make vuln-scan` run osv-scanner over this +# module using this config. +# +# Default posture: NO ignores. A clean tree carries an empty file. +# +# An ignore is an explicit, auditable decision — never a silent skip. Each entry +# MUST carry: +# - id the OSV / GHSA / GO advisory id (e.g. "GO-2026-1234") +# - reason why it is safe to defer (unreachable code path, false positive, +# upstream fix unavailable, etc.) +# - ignoreUntil a future review-expiry date (RFC3339). On expiry the finding +# re-surfaces and must be re-justified or cleared. +# +# Example (keep commented unless an exception is actually in force): +# +# [[IgnoredVulns]] +# id = "GO-2026-1234" +# reason = "Vulnerable function not reachable from our code paths." +# ignoreUntil = "2026-09-01T00:00:00Z"