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
187 changes: 187 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
name: release
# Builds the release binaries, runs the test suite on each architecture,
# audits the dependency tree, writes a CycloneDX SBOM per binary target and
# publishes everything under one OpenSSH-signed MANIFEST.txt. See RELEASING.md.
on:
push:
tags: ["v*"]
workflow_dispatch:

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
audit:
name: cargo audit
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
tool: cargo-audit
- name: Audit Cargo.lock against the RustSec database
# Any advisory (vulnerability, unsound, unmaintained, yanked) fails
# the release. An advisory that cannot be fixed by an upgrade must be
# ignored here by id with a matching statement in vex.openvex.json.
run: cargo audit --deny warnings
- name: Save the audit report
if: always()
run: cargo audit --json > cargo-audit.json || true
- uses: actions/upload-artifact@v4
if: always()
with:
name: cargo-audit
path: cargo-audit.json

build:
name: build (${{ matrix.target }})
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-24.04
- target: aarch64-unknown-linux-gnu
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
env:
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: taiki-e/install-action@v2
with:
tool: cargo-cyclonedx
- name: Version
run: |
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
if [ "$GITHUB_REF_TYPE" = tag ] && [ "$GITHUB_REF_NAME" != "v$VERSION" ]; then
echo "tag $GITHUB_REF_NAME does not match Cargo.toml version $VERSION" >&2
exit 1
fi
- name: Build
run: cargo build --release --locked --target "$TARGET"
- name: Rust tests (unit + integration, ptrace on the runner)
run: cargo test --release --locked --target "$TARGET"
- name: Verifier tamper suite (stdlib Python)
run: python3 -W error -m unittest discover -s verifier -p 'test_*.py' -v
- name: Toy agent clean runs and bypass corpus
run: ci/run.sh
env:
TRACER: target/${{ matrix.target }}/release/effect-tracer
- name: CycloneDX SBOM for the binary
run: |
cargo cyclonedx --format json --spec-version 1.5 --describe binaries --target "$TARGET" -q
set -- *.cdx.json
[ "$#" -eq 1 ] || { echo "expected exactly one SBOM, found: $*" >&2; exit 1; }
mkdir -p dist
mv "$1" "dist/effect-tracer-v$VERSION-$TARGET.cdx.json"
- name: Package
run: |
stage=$(mktemp -d)
cp "target/$TARGET/release/effect-tracer" LICENSE README.md LEDGER.md "$stage/"
epoch=$(git log -1 --format=%ct)
tar --sort=name --mtime="@$epoch" --owner=0 --group=0 --numeric-owner \
-C "$stage" -cf - effect-tracer LICENSE README.md LEDGER.md \
| gzip -n -9 > "dist/effect-tracer-v$VERSION-$TARGET.tar.gz"
ls -l dist
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.target }}
path: dist/
if-no-files-found: error

sign:
name: manifest, signature, release
needs: [audit, build]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: dist-*
merge-multiple: true
path: dist
- name: Version
run: |
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Add the VEX document, the verifier and the trust anchor
run: |
cp vex.openvex.json verifier/verify_ledger.py release-signing-key.pub allowed_signers dist/
ls -l dist
- name: Write MANIFEST.txt (SHA-256 of every asset)
run: |
cd dist
sha256sum -- * > ../MANIFEST.txt
mv ../MANIFEST.txt .
cat MANIFEST.txt
- name: Sign MANIFEST.txt with the release key
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
run: |
set -euo pipefail
umask 077
[ -n "$RELEASE_SIGNING_KEY" ] || { echo "RELEASE_SIGNING_KEY secret is not set" >&2; exit 1; }
key="$RUNNER_TEMP/release-signing-key"
printf '%s\n' "$RELEASE_SIGNING_KEY" > "$key"
want=$(awk '{print $1, $2}' release-signing-key.pub)
have=$(ssh-keygen -y -f "$key" | awk '{print $1, $2}')
if [ "$want" != "$have" ]; then
echo "the secret key does not match release-signing-key.pub in the repository" >&2
rm -f "$key"; exit 1
fi
ssh-keygen -Y sign -f "$key" -n file dist/MANIFEST.txt
rm -f "$key"
- name: Verify the way a downloader would
run: |
ssh-keygen -Y verify -f allowed_signers -I effect-tracer-release -n file \
-s dist/MANIFEST.txt.sig < dist/MANIFEST.txt
cd dist && sha256sum --strict -c MANIFEST.txt
- uses: actions/upload-artifact@v4
with:
name: release-v${{ env.VERSION }}
path: dist/
if-no-files-found: error
- name: Release notes
run: |
{
awk -v v="$VERSION" '$0 ~ "^## " v " " {f=1; next} /^## / {f=0} f' CHANGELOG.md
echo
echo '## Verify'
echo
echo 'Every asset is listed with its SHA-256 in `MANIFEST.txt`, and the manifest is signed'
echo 'with the Ed25519 key in `release-signing-key.pub` (OpenSSH signature, namespace `file`).'
echo 'From a clone of this repository, with the assets downloaded into `release/`:'
echo
echo '```'
echo 'ssh-keygen -Y verify -f allowed_signers -I effect-tracer-release -n file \'
echo ' -s release/MANIFEST.txt.sig < release/MANIFEST.txt'
echo '(cd release && sha256sum --ignore-missing -c MANIFEST.txt)'
echo '```'
echo
echo 'Details, including the key fingerprint, in [RELEASING.md](RELEASING.md).'
echo
echo '## Assets'
echo
echo '```'
cat dist/MANIFEST.txt
echo '```'
} > notes.md
cat notes.md
- name: Create the GitHub release (draft; tag pushes only)
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" --draft --verify-tag \
--title "$GITHUB_REF_NAME" --notes-file notes.md dist/*
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All dates are 2026.

## 0.1.2 (Nov 1)

- Release pipeline: `.github/workflows/release.yml` builds x86_64 and
aarch64 Linux binaries on GitHub's runners, runs the full test suite on
each, and publishes them with an OpenSSH-signed `MANIFEST.txt` (Ed25519,
public key in `release-signing-key.pub` / `allowed_signers`), a CycloneDX
1.5 SBOM per binary, an OpenVEX document and the verifier script. `cargo
audit --deny warnings` gates the release. Verify steps in `RELEASING.md`.
- No tracer or verifier changes.

## 0.1.1 (Oct 31)

- A netlink `SOCK_RAW` socket (what glibc's `getaddrinfo` opens to list
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "effect-tracer"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
license = "Apache-2.0"
description = "Records every file write, socket connect, DNS query and exec of a Linux process tree into a hash-chained, Ed25519-signed ledger (ptrace)."
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,19 @@ exact transcript is in [assets/demo-transcript.txt](assets/demo-transcript.txt).
| stdlib Python verifier with its own Ed25519 | `verifier/verify_ledger.py`, tamper suite in `verifier/test_verify_ledger.py` |
| benchmarks: method, corpus, results | [BENCH.md](BENCH.md), numbers in `bench/*.json` |
| GitHub Actions example | `examples/action.yml` |
| release pipeline: signed manifest, SBOM, VEX, how to verify | [RELEASING.md](RELEASING.md), `.github/workflows/release.yml` |

## Install

Linux 5.6 or newer (x86_64 or aarch64; the published benchmark runs are aarch64, and CI builds and runs the ptrace integration test on x86_64 `ubuntu-24.04`), Rust 1.80+.

```
cargo install --git https://github.com/YusefSyed/effect-tracer --tag v0.1.1 --locked
cargo install --git https://github.com/YusefSyed/effect-tracer --tag v0.1.2 --locked
```

Or download a prebuilt binary from the [releases page](https://github.com/YusefSyed/effect-tracer/releases)
and check it against the signed manifest (next section).

No root is needed: the tracer only traces its own children. Inside
Docker, add `--cap-add SYS_PTRACE`. Without it the tracer still runs under
Docker's default seccomp profile, but that profile denies `pidfd_getfd`,
Expand All @@ -71,6 +75,27 @@ check expects `udp:` and `tcp:`. The CI workflow runs on GitHub's
`ubuntu-24.04` runners without sudo (build, Rust tests under ptrace, the
verifier tamper suite, the toy-agent clean runs and the bypass corpus).

## Verify a release

Each release from v0.1.2 on ships Linux binaries for x86_64 and aarch64, a
CycloneDX SBOM per binary, an OpenVEX document and `verify_ledger.py`, all
listed by SHA-256 in `MANIFEST.txt`, which is signed with the Ed25519
release key in `release-signing-key.pub` (OpenSSH signature, namespace
`file`). From a clone of this repository, with the downloaded assets in
`release/`:

```
ssh-keygen -Y verify -f allowed_signers -I effect-tracer-release -n file \
-s release/MANIFEST.txt.sig < release/MANIFEST.txt
(cd release && sha256sum --ignore-missing -c MANIFEST.txt)
```

The first prints `Good "file" signature for effect-tracer-release with
ED25519 key SHA256:hxiDDsLKUbG2BO4GePgzO/+xwPshP++WE3rw0QNelsc`; the second
prints `OK` for every file you downloaded. [RELEASING.md](RELEASING.md) has
the full asset list, what the workflow checks before signing, and how the
key is kept.

## Use

```
Expand Down Expand Up @@ -302,6 +327,9 @@ through [SECURITY.md](SECURITY.md) or an issue.

## Changelog

- **0.1.2** (2026-11-01): release pipeline: x86_64 and aarch64 Linux binaries
with an OpenSSH-signed `MANIFEST.txt`, a CycloneDX SBOM per binary, an
OpenVEX document and a `cargo audit` gate. No tracer or verifier changes.
- **0.1.1** (2026-10-31): a netlink `SOCK_RAW` socket (what glibc's
`getaddrinfo` opens to list interfaces) is recorded as `socket.netlink`
instead of a raw-socket `coverage.gap`. Before this, any traced program
Expand Down
111 changes: 111 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Releasing

How a release of effect-tracer is cut, what the release workflow produces,
and how anyone can check what they downloaded.

## What a release contains

`.github/workflows/release.yml` runs on every `v*` tag (and by hand through
`workflow_dispatch`, which does everything except create the GitHub
release). For the tag `vX.Y.Z` it produces:

| asset | what it is |
|---|---|
| `effect-tracer-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz` | release binary (plus `LICENSE`, `README.md`, `LEDGER.md`), built on `ubuntu-24.04` |
| `effect-tracer-vX.Y.Z-aarch64-unknown-linux-gnu.tar.gz` | the same, built on `ubuntu-24.04-arm` |
| `effect-tracer-vX.Y.Z-<target>.cdx.json` | CycloneDX 1.5 SBOM for that binary, one per target, from `cargo cyclonedx --describe binaries` |
| `vex.openvex.json` | OpenVEX statements for the dependency tree at this release |
| `verify_ledger.py` | the stdlib Python ledger verifier, as shipped in `verifier/` |
| `release-signing-key.pub`, `allowed_signers` | the public half of the release key, as committed in this repository |
| `MANIFEST.txt` | `sha256sum` lines for every asset above |
| `MANIFEST.txt.sig` | OpenSSH signature over `MANIFEST.txt`, namespace `file`, made with the release key |

Before anything is packaged the workflow builds with `--locked`, runs the
Rust tests under ptrace on each architecture, the verifier tamper suite and
`ci/run.sh` (clean runs and the bypass corpus in both modes), and runs
`cargo audit --deny warnings` against the RustSec database. Any advisory
fails the release. An advisory with no upgrade available has to be ignored
by id in the workflow *and* get a statement in `vex.openvex.json` saying
why the binary is or is not affected; the two are meant to be read
together.

At v0.1.2 the audit found no advisory for any of the 70 locked crates, so
`vex.openvex.json` is a valid OpenVEX document with an empty `statements`
list; its `tooling` field says so.

The archives are built with fixed ordering, ownership and mtime (the
commit's timestamp), so rebuilding the same commit on the same toolchain
gives the same bytes.

## Verify a release

You need `ssh-keygen` (OpenSSH 8.2 or newer) and `sha256sum` (`shasum -a
256` on macOS). The trust anchor is `allowed_signers` in this repository,
so verify from a clone of the repository, not from a copy of the file that
came with the download.

```
git clone https://github.com/YusefSyed/effect-tracer
cd effect-tracer
mkdir release
# the assets you want, plus the manifest and its signature; for example:
V=v0.1.2
for f in MANIFEST.txt MANIFEST.txt.sig effect-tracer-$V-x86_64-unknown-linux-gnu.tar.gz; do
curl -fsSLo release/$f https://github.com/YusefSyed/effect-tracer/releases/download/$V/$f
done
# (or: gh release download $V -D release)

# 1. the manifest is signed by the release key
ssh-keygen -Y verify -f allowed_signers -I effect-tracer-release -n file \
-s release/MANIFEST.txt.sig < release/MANIFEST.txt
# -> Good "file" signature for effect-tracer-release with ED25519 key SHA256:hxiDDsLKUbG2BO4GePgzO/+xwPshP++WE3rw0QNelsc

# 2. every file you downloaded matches the manifest
(cd release && sha256sum --ignore-missing -c MANIFEST.txt)
# -> one "OK" line per file present; `--ignore-missing` skips assets you did not download
```

If step 1 says anything other than `Good "file" signature`, or step 2
prints `FAILED`, do not run the binary.

Release key fingerprint: `SHA256:hxiDDsLKUbG2BO4GePgzO/+xwPshP++WE3rw0QNelsc`
(Ed25519). The same key is in `release-signing-key.pub`. If the key is
ever rotated, the new public key is added as a second line of
`allowed_signers` and the old line stays, so older releases keep
verifying.

The signature covers the manifest, and the manifest covers the SBOMs, the
VEX document and the verifier script as well as the binaries, so the same
two commands tell you those were not altered either.

## Cutting a release

1. Bump `version` in `Cargo.toml` and the `effect-tracer` entry in
`Cargo.lock`, add a dated section `## X.Y.Z (Mon D)` to `CHANGELOG.md`
(the workflow copies that section into the release notes), update the
changelog list and the install pin in `README.md`, and revisit
`vex.openvex.json`: bump its `version`, refresh `timestamp`, and add or
retire statements to match the current `cargo audit` output.
2. Push the branch and run the workflow by hand (`gh workflow run release.yml
--ref <branch>`). It must be green; download the `release-vX.Y.Z`
artifact and run the two verify commands above against it.
3. Merge to `main`, then tag and push the tag:

```
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z
```

The tag run creates a **draft** release with the assets and notes.
Read the release page, then publish it (`gh release edit vX.Y.Z
--draft=false`).

## The signing key

The private key is an Ed25519 OpenSSH key held only in the repository
secret `RELEASE_SIGNING_KEY`; it never touches the checkout. The workflow
writes it to the runner's temporary directory with mode 0600, checks that
it matches the committed `release-signing-key.pub`, signs, and deletes it.
To rotate: generate a new key, replace the secret, replace
`release-signing-key.pub`, append (do not replace) the new line in
`allowed_signers`, and say so in `CHANGELOG.md`.
1 change: 1 addition & 0 deletions allowed_signers
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
effect-tracer-release namespaces="file" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBMCJs7HR3J64yvtOUaTIP3HEU2w5g1i9mjf2rwGIYYE effect-tracer release
1 change: 1 addition & 0 deletions release-signing-key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBMCJs7HR3J64yvtOUaTIP3HEU2w5g1i9mjf2rwGIYYE effect-tracer release
Loading
Loading