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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,40 @@ jobs:
- name: Verify prebuilt ARM64 image
run: bash ./scripts/test-prebuilt-image.sh edgewatch:prebuilt-arm64 linux/arm64 v0.0.0-ci

# The release also publishes an ARM64 image. Its seccomp filter numbers the
# system calls differently, and its Landlock restriction runs on another
# kernel. The container job builds that image only under emulation, where
# neither runs, so this job runs the sandbox tests and the real scans on an
# ARM64 kernel.
arm64-sandbox:
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache: true
- name: Test the sandbox, scanner, and notification packages
run: go test -race -timeout=15m ./internal/sandbox/ ./internal/scanner/ ./internal/notify/
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Build the ARM64 image natively
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
load: true
platforms: linux/arm64
tags: edgewatch:arm64
build-args: |
VERSION=v0.0.0-ci
cache-from: type=gha,scope=container-native-arm64
cache-to: ${{ github.event_name == 'push' && 'type=gha,mode=max,scope=container-native-arm64' || '' }}
- name: Verify scanner sandbox with real scans
run: ./scripts/verify-scanner-sandbox.sh edgewatch:arm64

# GitHub Actions caches are ref-isolated, so release tags cannot restore a
# cache written by main. Seed separate main and release GHCR caches here;
# tags refresh only the release cache. Grant package writes only to trusted
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Documentation-only changes need a diff review and checks of referenced paths and
| Database schema | Store migration tests and `./scripts/check-schema-docs.sh` |
| Compose configuration | Run `docker compose config --quiet` and `docker compose -f compose.yaml -f compose.syn.yaml config --quiet`, then verify the rendered capability, hardening, image, and storage policies described in `docs/src/content/docs/deployment/container-hardening.md` and the CI `Validate Compose deployment` step |
| Scanner dependency pin | `./scripts/verify-naabu-pin.sh` |
| Scanner execution or sandbox | Build the image and run `./scripts/verify-scanner-sandbox.sh IMAGE`, which needs Docker and a kernel with Landlock; it compares real sandboxed, Landlock-only, and unconfined scans of local listeners, tries Landlock escapes, and checks the seccomp filter, core limits, and a sandboxed notification delivery |
| Scanner execution or sandbox | Build the image and run `./scripts/verify-scanner-sandbox.sh IMAGE`, which needs Docker and a kernel with Landlock; it compares real sandboxed, Landlock-only, and unconfined scans of local listeners, tries Landlock escapes, and checks the seccomp filter, core limits, and a sandboxed notification delivery; CI runs it on AMD64 and ARM64 runners |
| Release helper scripts | `./scripts/test-release-artifacts.sh`; this uses fixture binaries and does not build a release candidate |
| Release workflow or GoReleaser configuration | Follow the exact GoReleaser check and immutable-candidate gates in `.github/workflows/release.yml`; the candidate, publication, image, and runtime smoke gates run only for tags |

Expand Down
5 changes: 4 additions & 1 deletion docs/src/content/docs/deployment/container-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ directory, and that a process restricted with Landlock cannot read the
database, list the data directory, read `config.yaml`, write to the data
directory, or execute a file it wrote, even as UID 0. It requires that a
running Nmap and the notification process each have one seccomp filter more
than the daemon, and that neither they nor the daemon can dump core.
than the daemon, and that neither they nor the daemon can dump core. The
seccomp filter and the Landlock restriction depend on the architecture and
the kernel, so CI also runs the sandbox tests and these real scans on an
ARM64 runner, against an ARM64 image built there.

## Data ownership and upgrades

Expand Down
25 changes: 18 additions & 7 deletions internal/scanner/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,42 @@ import (
"path/filepath"
"reflect"
"strings"
"syscall"
"testing"

"github.com/crypt0rr/edgewatch/internal/sandbox"
"golang.org/x/sys/unix"
)

// writeScript writes a script that the test then runs. A process that a
// parallel test forks while the script is open for writing inherits the
// descriptor until it executes its own program, and running the script
// meanwhile fails with "text file busy". Go forks only while it holds
// syscall.ForkLock exclusively, so holding it shared while the script is
// written keeps every fork out of that window.
func writeScript(t *testing.T, path, script string) {
t.Helper()
syscall.ForkLock.RLock()
defer syscall.ForkLock.RUnlock()
if err := os.WriteFile(path, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
}

// writeFakeHelper writes a stand-in for the EdgeWatch executable whose
// sandbox-exec command records its arguments and inherited descriptor in dir
// and then runs body.
func writeFakeHelper(t *testing.T, dir, body string) string {
t.Helper()
path := filepath.Join(dir, "edgewatch")
script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > " + filepath.Join(dir, "args") + "\n" + body
if err := os.WriteFile(path, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
writeScript(t, path, "#!/bin/sh\nprintf '%s\\n' \"$@\" > "+filepath.Join(dir, "args")+"\n"+body)
return path
}

func writeFakeScanner(t *testing.T, name string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 99\n"), 0o700); err != nil {
t.Fatal(err)
}
writeScript(t, path, "#!/bin/sh\nexit 99\n")
return path
}

Expand Down
Loading