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
37 changes: 37 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
bin/
*.test
coverage.out

# Built adapter binary (publish via CI; never commit)
/criteria-adapter-shell
48 changes: 48 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
83 changes: 75 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <workflow-dir> # 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.
Expand Down
29 changes: 29 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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`.
Binary file removed criteria-adapter-shell
Binary file not shown.
85 changes: 85 additions & 0 deletions docs/dependency-policy.md
Original file line number Diff line number Diff line change
@@ -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 <module>@<version>` (honor the 7-day cooldown).
- **Major:** `gomajor get <module>@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.
Loading
Loading