diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..1d472e3 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,80 @@ +name: Build and test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Download dependencies + run: go mod download + + - name: Build + run: make build-ci + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Download dependencies + run: go mod download + + - name: Lint + uses: golangci/golangci-lint-action@v6 + with: + install-mode: goinstall + version: v1.64.8 + + test: + name: Test + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Download dependencies + run: go mod download + + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y bubblewrap socat + + - name: Test + run: make test-ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6c714d6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +on: + push: + tags: + - "v*" + +run-name: "Release ${{ github.ref_name }}" + +permissions: + contents: read + +jobs: + goreleaser: + permissions: + contents: write + id-token: write # Required for SLSA + runs-on: ubuntu-latest + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate hashes for provenance + id: hash + run: | + cd dist + echo "hashes=$(sha256sum * | grep -v checksums.txt | base64 -w0)" >> "$GITHUB_OUTPUT" + + provenance: + needs: [goreleaser] + permissions: + actions: read + id-token: write + contents: write + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 + with: + base64-subjects: "${{ needs.goreleaser.outputs.hashes }}" + upload-assets: true diff --git a/.gitignore b/.gitignore index 2afc9ec..185e30f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Binary (only at root, not cmd/fence or pkg/fence) /fence +/fence_unix +/fence_darwin # OS files .DS_Store @@ -15,3 +17,6 @@ Thumbs.db *.test coverage.out +# GoReleaser +/dist/ + diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..b3e0d76 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,41 @@ +run: + timeout: 5m + modules-download-mode: readonly + +linters-settings: + gci: + sections: + - standard + - default + - prefix(github.com/Use-Tusk/fence) + gofmt: + simplify: true + goimports: + local-prefixes: github.com/Use-Tusk/fence + gocritic: + disabled-checks: + - singleCaseSwitch + revive: + rules: + - name: exported + disabled: true + +linters: + enable-all: false + disable-all: true + enable: + - staticcheck + - errcheck + - gosimple + - govet + - unused + - ineffassign + - gosec + - gocritic + - revive + - gofumpt + - misspell + +issues: + exclude-use-default: false + diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..44f1500 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,83 @@ +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -s -w + - -X main.version={{.Version}} + - -X main.buildTime={{.Date}} + - -X main.gitCommit={{.Commit}} + binary: fence + main: ./cmd/fence + +archives: + - formats: [tar.gz] + name_template: >- + {{ .ProjectName }}_ + {{- .Version }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + files: + - README.md + - LICENSE + - ARCHITECTURE.md + - CONTRIBUTING.md + +checksum: + name_template: "checksums.txt" + +changelog: + sort: asc + use: github + format: "{{ .SHA }}: {{ .Message }}{{ with .AuthorUsername }} (@{{ . }}){{ end }}" + filters: + exclude: + - "^test:" + - "^test\\(" + - "^chore: update$" + - "^chore: docs$" + - "^docs: update$" + - "^chore: typo$" + - "^chore\\(deps\\): " + - "^(build|ci): " + - "merge conflict" + - Merge pull request + - Merge remote-tracking branch + - Merge branch + - go mod tidy + groups: + - title: "New Features" + regexp: '^.*?feat(\(.+\))??!?:.+$' + order: 100 + - title: "Security updates" + regexp: '^.*?sec(\(.+\))??!?:.+$' + order: 150 + - title: "Bug fixes" + regexp: '^.*?(fix|refactor)(\(.+\))??!?:.+$' + order: 200 + - title: "Documentation updates" + regexp: ^.*?docs?(\(.+\))??!?:.+$ + order: 400 + - title: Other work + order: 9999 + +release: + github: + owner: Use-Tusk + name: fence + draft: false + prerelease: auto diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..67cc208 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,165 @@ +# Contributing + +Thanks for helping improve `fence`! + +If you have any questions, feel free to open an issue. + +## Quick start + +- Requirements: + - Go 1.25+ + - macOS or Linux +- Clone and prepare: + + ```bash + git clone https://github.com/Use-Tusk/fence + cd fence + make setup # Install deps and lint tools + make build # Build the binary + ./fence --help + ``` + +## Dev workflow + +Common targets: + +| Command | Description | +|---------|-------------| +| `make build` | Build the binary (`./fence`) | +| `make run` | Build and run | +| `make test` | Run tests | +| `make test-ci` | Run tests with coverage | +| `make deps` | Download/tidy modules | +| `make fmt` | Format code with gofumpt | +| `make lint` | Run golangci-lint | +| `make build-ci` | Build with version info (used in CI) | +| `make help` | Show all available targets | + +## Code structure + +See [ARCHITECTURE.md](ARCHITECTURE.md) for the full project structure and component details. + +## Style and conventions + +- Keep edits focused and covered by tests where possible. +- Update [ARCHITECTURE.md](ARCHITECTURE.md) when adding features or changing behavior. +- Prefer small, reviewable PRs with a clear rationale. +- Run `make fmt` and `make lint` before committing. + +## Testing + +```bash +# Run all tests +make test + +# Run with verbose output +go test -v ./... + +# Run with coverage +make test-ci +``` + +### Testing on macOS + +```bash +# Test blocked network request +./fence curl https://example.com + +# Test with allowed domain +echo '{"network":{"allowedDomains":["example.com"]}}' > /tmp/test.json +./fence -s /tmp/test.json curl https://example.com + +# Test monitor mode +./fence -m -c "touch /etc/test" +``` + +### Testing on Linux + +Requires `bubblewrap` and `socat`: + +```bash +# Ubuntu/Debian +sudo apt install bubblewrap socat + +# Test in Colima or VM +./fence curl https://example.com +``` + +## Troubleshooting + +**"command not found" after go install:** + +- Add `$GOPATH/bin` to your PATH +- Or use `go env GOPATH` to find the path + +**Module issues:** + +```bash +go mod tidy # Clean up dependencies +``` + +**Build cache issues:** + +```bash +go clean -cache +go clean -modcache +``` + +**macOS sandbox issues:** + +- Check `log stream --predicate 'eventMessage ENDSWITH "_SBX"'` for violations +- Ensure you're not running as root + +**Linux bwrap issues:** + +- May need `sudo` or `kernel.unprivileged_userns_clone=1` +- Check that socat and bwrap are installed + +## For Maintainers + +### Releasing + +Releases are automated using [GoReleaser](https://goreleaser.com/) via GitHub Actions. + +#### Creating a release + +1. Tag the commit with a semantic version: + + ```bash + git tag v1.0.0 + git push origin v1.0.0 + ``` + +2. GitHub Actions will automatically: + - Build binaries for all supported platforms + - Create archives with README, LICENSE, and ARCHITECTURE.md + - Generate checksums + - Create a GitHub release with changelog + - Upload all artifacts + +#### Supported platforms + +The release workflow builds for: + +- **Linux**: amd64, arm64 +- **macOS (darwin)**: amd64, arm64 + +#### Building locally for distribution + +```bash +# Build for current platform +make build + +# Cross-compile +make build-linux +make build-darwin + +# With version info (mimics CI builds) +make build-ci +``` + +To test the GoReleaser configuration locally: + +```bash +goreleaser release --snapshot --clean +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..36a5071 --- /dev/null +++ b/Makefile @@ -0,0 +1,103 @@ +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOMOD=$(GOCMD) mod +BINARY_NAME=fence +BINARY_UNIX=$(BINARY_NAME)_unix + +.PHONY: all build build-ci build-linux test test-ci clean deps install-lint-tools setup setup-ci run fmt lint release release-minor help + +all: build + +build: + @echo "๐Ÿ”จ Building $(BINARY_NAME)..." + $(GOBUILD) -o $(BINARY_NAME) -v ./cmd/fence + +build-ci: + @echo "๐Ÿ—๏ธ CI: Building $(BINARY_NAME) with version info..." + $(eval VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")) + $(eval BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')) + $(eval GIT_COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")) + $(GOBUILD) -ldflags "-s -w -X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.gitCommit=$(GIT_COMMIT)" -o $(BINARY_NAME) -v ./cmd/fence + +test: + @echo "๐Ÿงช Running tests..." + $(GOTEST) -v ./... + +test-ci: + @echo "๐Ÿงช CI: Running tests with coverage..." + $(GOTEST) -v -race -coverprofile=coverage.out ./... + +clean: + @echo "๐Ÿงน Cleaning..." + $(GOCLEAN) + rm -f $(BINARY_NAME) + rm -f $(BINARY_UNIX) + rm -f coverage.out + +deps: + @echo "๐Ÿ“ฆ Downloading dependencies..." + $(GOMOD) download + $(GOMOD) tidy + +build-linux: + @echo "๐Ÿง Building for Linux..." + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) -v ./cmd/fence + +build-darwin: + @echo "๐ŸŽ Building for macOS..." + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINARY_NAME)_darwin -v ./cmd/fence + +install-lint-tools: + @echo "๐Ÿ“ฆ Installing linting tools..." + go install mvdan.cc/gofumpt@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + @echo "โœ… Linting tools installed" + +setup: deps install-lint-tools + @echo "โœ… Development environment ready" + +setup-ci: deps install-lint-tools + @echo "โœ… CI environment ready" + +run: build + ./$(BINARY_NAME) + +fmt: + @echo "๐Ÿ“ Formatting code..." + gofumpt -w . + +lint: + @echo "๐Ÿ” Linting code..." + golangci-lint run --allow-parallel-runners + +release: + @echo "๐Ÿš€ Creating patch release..." + ./scripts/release.sh patch + +release-minor: + @echo "๐Ÿš€ Creating minor release..." + ./scripts/release.sh minor + +help: + @echo "Available targets:" + @echo " all - build (default)" + @echo " build - Build the binary" + @echo " build-ci - Build for CI with version info" + @echo " build-linux - Build for Linux" + @echo " build-darwin - Build for macOS" + @echo " test - Run tests" + @echo " test-ci - Run tests for CI with coverage" + @echo " clean - Clean build artifacts" + @echo " deps - Download dependencies" + @echo " install-lint-tools - Install linting tools" + @echo " setup - Setup development environment" + @echo " setup-ci - Setup CI environment" + @echo " run - Build and run" + @echo " fmt - Format code" + @echo " lint - Lint code" + @echo " release - Create patch release (v0.0.X)" + @echo " release-minor - Create minor release (v0.X.0)" + @echo " help - Show this help" + diff --git a/internal/sandbox/monitor.go b/internal/sandbox/monitor.go index a175560..b481fa5 100644 --- a/internal/sandbox/monitor.go +++ b/internal/sandbox/monitor.go @@ -139,7 +139,7 @@ func parseViolation(line string) string { } // Filter out noisy violations - if isNoisyViolation(operation, details) { + if isNoisyViolation(details) { return "" } @@ -170,7 +170,7 @@ func shouldShowViolation(operation string) bool { } // isNoisyViolation returns true if this violation is system noise that should be filtered. -func isNoisyViolation(operation, details string) bool { +func isNoisyViolation(details string) bool { // Filter out TTY/terminal writes (very noisy from any process that prints output) if strings.HasPrefix(details, "/dev/tty") || strings.HasPrefix(details, "/dev/pts") { @@ -195,4 +195,3 @@ func isNoisyViolation(operation, details string) bool { func GetSessionSuffix() string { return sessionSuffix // defined in macos.go } - diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..56bddac --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: ./scripts/release.sh [patch|minor] +# Default: patch + +BUMP_TYPE="${1:-patch}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +# Validate bump type +if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" ]]; then + error "Invalid bump type: $BUMP_TYPE. Use 'patch' or 'minor' (or no argument for minor)." +fi + +info "Bump type: $BUMP_TYPE" + +# ============================================================================= +# Preflight checks +# ============================================================================= + +info "Running preflight checks..." + +# Check we're in a git repository +if ! git rev-parse --is-inside-work-tree &>/dev/null; then + error "Not in a git repository" +fi + +# Check we're on the default branch (main) +DEFAULT_BRANCH="main" +CURRENT_BRANCH=$(git branch --show-current) +if [[ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]]; then + error "Not on default branch. Current: $CURRENT_BRANCH, Expected: $DEFAULT_BRANCH" +fi + +# Check for uncommitted changes +if ! git diff --quiet || ! git diff --staged --quiet; then + error "Working directory has uncommitted changes. Commit or stash them first." +fi + +# Check for untracked files (warning only) +UNTRACKED=$(git ls-files --others --exclude-standard) +if [[ -n "$UNTRACKED" ]]; then + warn "Untracked files present (continuing anyway):" + echo "$UNTRACKED" | head -5 +fi + +# Fetch latest from remote +info "Fetching latest from origin..." +git fetch origin "$DEFAULT_BRANCH" --tags + +# Check if local branch is up to date with remote +LOCAL_COMMIT=$(git rev-parse HEAD) +REMOTE_COMMIT=$(git rev-parse "origin/$DEFAULT_BRANCH") +if [[ "$LOCAL_COMMIT" != "$REMOTE_COMMIT" ]]; then + error "Local branch is not up to date with origin/$DEFAULT_BRANCH. Run 'git pull' first." +fi + +# Check if there are commits since last tag +LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") +if [[ -n "$LAST_TAG" ]]; then + COMMITS_SINCE_TAG=$(git rev-list "$LAST_TAG"..HEAD --count) + if [[ "$COMMITS_SINCE_TAG" -eq 0 ]]; then + error "No commits since last tag ($LAST_TAG). Nothing to release." + fi + info "Commits since $LAST_TAG: $COMMITS_SINCE_TAG" +fi + +# Check that tests pass +info "Running tests..." +if ! make test-ci; then + error "Tests failed. Fix them before releasing." +fi + +# Check that lint passes +info "Running linter..." +if ! make lint; then + error "Lint failed. Fix issues before releasing." +fi + +info "โœ“ All preflight checks passed" + +# ============================================================================= +# Calculate new version +# ============================================================================= + +if [[ -z "$LAST_TAG" ]]; then + # No existing tags, start at v0.1.0 + NEW_VERSION="v0.1.0" + info "No existing tags found. Starting at $NEW_VERSION" +else + # Parse current version (strip 'v' prefix) + VERSION="${LAST_TAG#v}" + IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" + + # Validate parsed version + if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" ]]; then + error "Failed to parse version from tag: $LAST_TAG" + fi + + # Increment based on bump type + case "$BUMP_TYPE" in + patch) + PATCH=$((PATCH + 1)) + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + esac + + NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}" + info "Version bump: $LAST_TAG โ†’ $NEW_VERSION" +fi + +# ============================================================================= +# Confirm and create tag +# ============================================================================= + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo " Ready to release: $NEW_VERSION" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +read -p "Create and push tag $NEW_VERSION? [y/N] " -n 1 -r +echo "" + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + info "Aborted." + exit 0 +fi + +# Create annotated tag +info "Creating tag $NEW_VERSION..." +git tag -a "$NEW_VERSION" -m "Release $NEW_VERSION" + +# Push tag to origin +info "Pushing tag to origin..." +git push origin "$NEW_VERSION" + +echo "" +info "โœ“ Released $NEW_VERSION" +info "GitHub Actions will now build and publish the release." +info "Watch progress at: https://github.com/Use-Tusk/fence/actions"