Compare commits
3 Commits
5aeb9c86c0
...
feat-isola
| Author | SHA1 | Date | |
|---|---|---|---|
| a04f5feee2 | |||
| c95fca830b | |||
| 5affaf77a5 |
@@ -1,101 +0,0 @@
|
|||||||
name: Benchmarks
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
min_runs:
|
|
||||||
description: "Minimum benchmark runs"
|
|
||||||
required: false
|
|
||||||
default: "30"
|
|
||||||
quick:
|
|
||||||
description: "Quick mode (fewer runs)"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
benchmark-linux:
|
|
||||||
name: Benchmark (Linux)
|
|
||||||
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: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Download dependencies
|
|
||||||
run: go mod download
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
bubblewrap \
|
|
||||||
socat \
|
|
||||||
uidmap \
|
|
||||||
curl \
|
|
||||||
netcat-openbsd \
|
|
||||||
ripgrep \
|
|
||||||
hyperfine \
|
|
||||||
jq \
|
|
||||||
bc
|
|
||||||
# Configure subuid/subgid
|
|
||||||
echo "$(whoami):100000:65536" | sudo tee -a /etc/subuid
|
|
||||||
echo "$(whoami):100000:65536" | sudo tee -a /etc/subgid
|
|
||||||
sudo chmod u+s $(which bwrap)
|
|
||||||
|
|
||||||
- name: Install benchstat
|
|
||||||
run: go install golang.org/x/perf/cmd/benchstat@latest
|
|
||||||
|
|
||||||
- name: Build greywall
|
|
||||||
run: make build-ci
|
|
||||||
|
|
||||||
- name: Run Go microbenchmarks
|
|
||||||
run: |
|
|
||||||
mkdir -p benchmarks
|
|
||||||
go test -run=^$ -bench=. -benchmem -count=10 ./internal/sandbox/... | tee benchmarks/go-bench-linux.txt
|
|
||||||
|
|
||||||
- name: Run CLI benchmarks
|
|
||||||
run: |
|
|
||||||
MIN_RUNS="${{ github.event.inputs.min_runs || '30' }}"
|
|
||||||
QUICK="${{ github.event.inputs.quick || 'false' }}"
|
|
||||||
|
|
||||||
if [[ "$QUICK" == "true" ]]; then
|
|
||||||
./scripts/benchmark.sh -q -o benchmarks
|
|
||||||
else
|
|
||||||
./scripts/benchmark.sh -n "$MIN_RUNS" -o benchmarks
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload benchmark results
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: benchmark-results-linux
|
|
||||||
path: benchmarks/
|
|
||||||
retention-days: 30
|
|
||||||
|
|
||||||
- name: Display results
|
|
||||||
run: |
|
|
||||||
echo "=== Linux Benchmark Results ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for f in benchmarks/*.md; do
|
|
||||||
[[ -f "$f" ]] && cat "$f"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Go Microbenchmarks ==="
|
|
||||||
grep -E '^Benchmark|^ok|^PASS' benchmarks/go-bench-linux.txt | head -50 || true
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
name: Build and test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
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-linux:
|
|
||||||
name: Test (Linux)
|
|
||||||
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: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
|
|
||||||
- name: Set up Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
- name: Download dependencies
|
|
||||||
run: go mod download
|
|
||||||
|
|
||||||
- name: Install Linux sandbox dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
bubblewrap \
|
|
||||||
socat \
|
|
||||||
uidmap \
|
|
||||||
curl \
|
|
||||||
netcat-openbsd \
|
|
||||||
ripgrep
|
|
||||||
# Configure subuid/subgid for the runner user (required for unprivileged user namespaces)
|
|
||||||
echo "$(whoami):100000:65536" | sudo tee -a /etc/subuid
|
|
||||||
echo "$(whoami):100000:65536" | sudo tee -a /etc/subgid
|
|
||||||
# Make bwrap setuid so it can create namespaces as non-root user
|
|
||||||
sudo chmod u+s $(which bwrap)
|
|
||||||
|
|
||||||
- name: Verify sandbox dependencies
|
|
||||||
run: |
|
|
||||||
echo "=== Checking sandbox dependencies ==="
|
|
||||||
bwrap --version
|
|
||||||
socat -V | head -1
|
|
||||||
echo "User namespaces enabled: $(cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null || echo 'check not available')"
|
|
||||||
echo "Kernel version: $(uname -r)"
|
|
||||||
echo "uidmap installed: $(which newuidmap 2>/dev/null && echo yes || echo no)"
|
|
||||||
echo "subuid configured: $(grep $(whoami) /etc/subuid 2>/dev/null || echo 'not configured')"
|
|
||||||
echo "bwrap setuid: $(ls -la $(which bwrap) | grep -q '^-rws' && echo yes || echo no)"
|
|
||||||
echo "=== Testing bwrap basic functionality ==="
|
|
||||||
bwrap --ro-bind / / -- /bin/echo "bwrap works!"
|
|
||||||
echo "=== Testing bwrap with user namespace ==="
|
|
||||||
bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 -- /bin/echo "bwrap user namespace works!"
|
|
||||||
|
|
||||||
- name: Run unit and integration tests
|
|
||||||
run: make test-ci
|
|
||||||
|
|
||||||
- name: Build binary for smoke tests
|
|
||||||
run: make build-ci
|
|
||||||
|
|
||||||
- name: Run smoke tests
|
|
||||||
run: GREYWALL_TEST_NETWORK=1 ./scripts/smoke_test.sh ./greywall
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
run-name: "Release ${{ github.ref_name }}"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
goreleaser:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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:
|
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
||||||
GORELEASER_FORCE_TOKEN: gitea
|
|
||||||
|
|
||||||
publish-version:
|
|
||||||
needs: [goreleaser]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout gh-pages
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: gh-pages
|
|
||||||
|
|
||||||
- name: Update latest version
|
|
||||||
run: |
|
|
||||||
echo "${{ github.ref_name }}" > latest.txt
|
|
||||||
|
|
||||||
cat > latest.json << EOF
|
|
||||||
{
|
|
||||||
"version": "${{ github.ref_name }}",
|
|
||||||
"published_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
||||||
"url": "https://gitea.app.monadical.io/monadical/greywall/releases/tag/${{ github.ref_name }}"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Commit and push to gh-pages
|
|
||||||
run: |
|
|
||||||
git config user.name "gitea-actions[bot]"
|
|
||||||
git config user.email "gitea-actions[bot]@noreply.gitea.app.monadical.io"
|
|
||||||
git add latest.txt latest.json
|
|
||||||
git commit -m "Update latest version to ${{ github.ref_name }}" || echo "No changes to commit"
|
|
||||||
git push origin gh-pages
|
|
||||||
@@ -1,49 +1,40 @@
|
|||||||
version: "2"
|
|
||||||
run:
|
run:
|
||||||
|
timeout: 5m
|
||||||
modules-download-mode: readonly
|
modules-download-mode: readonly
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- default
|
||||||
|
- prefix(gitea.app.monadical.io/monadical/greywall)
|
||||||
|
gofmt:
|
||||||
|
simplify: true
|
||||||
|
goimports:
|
||||||
|
local-prefixes: gitea.app.monadical.io/monadical/greywall
|
||||||
|
gocritic:
|
||||||
|
disabled-checks:
|
||||||
|
- singleCaseSwitch
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: exported
|
||||||
|
disabled: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
default: none
|
enable-all: false
|
||||||
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- errcheck
|
|
||||||
- gocritic
|
|
||||||
- gosec
|
|
||||||
- govet
|
|
||||||
- ineffassign
|
|
||||||
- misspell
|
|
||||||
- revive
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
- unused
|
- unused
|
||||||
settings:
|
- ineffassign
|
||||||
gocritic:
|
- gosec
|
||||||
disabled-checks:
|
- gocritic
|
||||||
- singleCaseSwitch
|
- revive
|
||||||
revive:
|
|
||||||
rules:
|
|
||||||
- name: exported
|
|
||||||
disabled: true
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofumpt
|
- gofumpt
|
||||||
settings:
|
- misspell
|
||||||
gci:
|
|
||||||
sections:
|
issues:
|
||||||
- standard
|
exclude-use-default: false
|
||||||
- default
|
|
||||||
- prefix(gitea.app.monadical.io/monadical/greywall)
|
|
||||||
gofmt:
|
|
||||||
simplify: true
|
|
||||||
goimports:
|
|
||||||
local-prefixes:
|
|
||||||
- gitea.app.monadical.io/monadical/greywall
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
gitea_urls:
|
|
||||||
api: https://gitea.app.monadical.io/api/v1
|
|
||||||
download: https://gitea.app.monadical.io
|
|
||||||
skip_tls_verify: false
|
|
||||||
|
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
@@ -47,7 +42,7 @@ checksum:
|
|||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
use: gitea
|
use: github
|
||||||
format: "{{ .SHA }}: {{ .Message }}{{ with .AuthorUsername }} (@{{ . }}){{ end }}"
|
format: "{{ .SHA }}: {{ .Message }}{{ with .AuthorUsername }} (@{{ . }}){{ end }}"
|
||||||
filters:
|
filters:
|
||||||
exclude:
|
exclude:
|
||||||
@@ -81,7 +76,7 @@ changelog:
|
|||||||
order: 9999
|
order: 9999
|
||||||
|
|
||||||
release:
|
release:
|
||||||
gitea:
|
github:
|
||||||
owner: monadical
|
owner: monadical
|
||||||
name: greywall
|
name: greywall
|
||||||
draft: false
|
draft: false
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ scripts/ Smoke tests, benchmarks, release
|
|||||||
|
|
||||||
- **Language:** Go 1.25+
|
- **Language:** Go 1.25+
|
||||||
- **Formatter:** `gofumpt` (enforced in CI)
|
- **Formatter:** `gofumpt` (enforced in CI)
|
||||||
- **Linter:** `golangci-lint` v2 (config in `.golangci.yml`)
|
- **Linter:** `golangci-lint` v1.64.8 (config in `.golangci.yml`)
|
||||||
- **Import order:** stdlib, third-party, local (`gitea.app.monadical.io/monadical/greywall`)
|
- **Import order:** stdlib, third-party, local (`gitea.app.monadical.io/monadical/greywall`)
|
||||||
- **Platform code:** build tags (`//go:build linux`, `//go:build darwin`) with `*_stub.go` for unsupported platforms
|
- **Platform code:** build tags (`//go:build linux`, `//go:build darwin`) with `*_stub.go` for unsupported platforms
|
||||||
- **Error handling:** custom error types (e.g., `CommandBlockedError`)
|
- **Error handling:** custom error types (e.g., `CommandBlockedError`)
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -70,7 +70,7 @@ build-darwin:
|
|||||||
install-lint-tools:
|
install-lint-tools:
|
||||||
@echo "Installing linting tools..."
|
@echo "Installing linting tools..."
|
||||||
go install mvdan.cc/gofumpt@latest
|
go install mvdan.cc/gofumpt@latest
|
||||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
@echo "Linting tools installed"
|
@echo "Linting tools installed"
|
||||||
|
|
||||||
setup: deps install-lint-tools
|
setup: deps install-lint-tools
|
||||||
|
|||||||
@@ -203,20 +203,19 @@ func runCommand(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if templatePath != "" {
|
if templatePath != "" {
|
||||||
learnedCfg, loadErr := config.Load(templatePath)
|
learnedCfg, loadErr := config.Load(templatePath)
|
||||||
switch {
|
if loadErr != nil {
|
||||||
case loadErr != nil:
|
|
||||||
if debug {
|
if debug {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] Warning: failed to load learned template: %v\n", loadErr)
|
fmt.Fprintf(os.Stderr, "[greywall] Warning: failed to load learned template: %v\n", loadErr)
|
||||||
}
|
}
|
||||||
case learnedCfg != nil:
|
} else if learnedCfg != nil {
|
||||||
cfg = config.Merge(cfg, learnedCfg)
|
cfg = config.Merge(cfg, learnedCfg)
|
||||||
if debug {
|
if debug {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] Auto-loaded learned template for %q\n", templateLabel)
|
fmt.Fprintf(os.Stderr, "[greywall] Auto-loaded learned template for %q\n", templateLabel)
|
||||||
}
|
}
|
||||||
case templateName != "":
|
} else if templateName != "" {
|
||||||
// Explicit --template but file doesn't exist
|
// Explicit --template but file doesn't exist
|
||||||
return fmt.Errorf("learned template %q not found at %s\nRun: greywall templates list", templateName, templatePath)
|
return fmt.Errorf("learned template %q not found at %s\nRun: greywall templates list", templateName, templatePath)
|
||||||
case cmdName != "":
|
} else if cmdName != "" {
|
||||||
// No template found for this command - suggest creating one
|
// No template found for this command - suggest creating one
|
||||||
fmt.Fprintf(os.Stderr, "[greywall] No learned template for %q. Run with --learning to create one.\n", cmdName)
|
fmt.Fprintf(os.Stderr, "[greywall] No learned template for %q. Run with --learning to create one.\n", cmdName)
|
||||||
}
|
}
|
||||||
@@ -504,7 +503,7 @@ Examples:
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
name := args[0]
|
name := args[0]
|
||||||
templatePath := sandbox.LearnedTemplatePath(name)
|
templatePath := sandbox.LearnedTemplatePath(name)
|
||||||
data, err := os.ReadFile(templatePath) //nolint:gosec // user-specified template path - intentional
|
data, err := os.ReadFile(templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return fmt.Errorf("template %q not found\nRun: greywall templates list", name)
|
return fmt.Errorf("template %q not found\nRun: greywall templates list", name)
|
||||||
|
|||||||
@@ -166,11 +166,11 @@ func GenerateLearnedTemplate(straceLogPath, cmdName string, debug bool) (string,
|
|||||||
|
|
||||||
// Save template
|
// Save template
|
||||||
templatePath := LearnedTemplatePath(cmdName)
|
templatePath := LearnedTemplatePath(cmdName)
|
||||||
if err := os.MkdirAll(filepath.Dir(templatePath), 0o750); err != nil {
|
if err := os.MkdirAll(filepath.Dir(templatePath), 0o755); err != nil {
|
||||||
return "", fmt.Errorf("failed to create template directory: %w", err)
|
return "", fmt.Errorf("failed to create template directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(templatePath, []byte(template), 0o600); err != nil {
|
if err := os.WriteFile(templatePath, []byte(template), 0o644); err != nil {
|
||||||
return "", fmt.Errorf("failed to write template: %w", err)
|
return "", fmt.Errorf("failed to write template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +305,11 @@ func isSensitivePath(path, home string) bool {
|
|||||||
|
|
||||||
// Check GPG
|
// Check GPG
|
||||||
gnupgDir := filepath.Join(home, ".gnupg")
|
gnupgDir := filepath.Join(home, ".gnupg")
|
||||||
return strings.HasPrefix(path, gnupgDir+"/")
|
if strings.HasPrefix(path, gnupgDir+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDangerousFilePatterns returns denyWrite entries for DangerousFiles.
|
// getDangerousFilePatterns returns denyWrite entries for DangerousFiles.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func ParseStraceLog(logPath string, debug bool) (*StraceResult, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open strace log: %w", err)
|
return nil, fmt.Errorf("failed to open strace log: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _ = f.Close() }()
|
defer f.Close()
|
||||||
|
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
seenWrite := make(map[string]bool)
|
seenWrite := make(map[string]bool)
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ func TestParseStraceLog(t *testing.T) {
|
|||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
logFile := filepath.Join(t.TempDir(), "strace.log")
|
logFile := filepath.Join(t.TempDir(), "strace.log")
|
||||||
if err := os.WriteFile(logFile, []byte(logContent), 0o600); err != nil {
|
if err := os.WriteFile(logFile, []byte(logContent), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ func TestFindApplicationDirectory(t *testing.T) {
|
|||||||
|
|
||||||
func TestCollapsePaths(t *testing.T) {
|
func TestCollapsePaths(t *testing.T) {
|
||||||
// Temporarily override home for testing
|
// Temporarily override home for testing
|
||||||
t.Setenv("HOME", "/home/testuser")
|
origHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", "/home/testuser")
|
||||||
|
defer os.Setenv("HOME", origHome)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -317,7 +319,9 @@ func TestToTildePath(t *testing.T) {
|
|||||||
func TestListLearnedTemplates(t *testing.T) {
|
func TestListLearnedTemplates(t *testing.T) {
|
||||||
// Use a temp dir to isolate from real user config
|
// Use a temp dir to isolate from real user config
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
origConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
|
defer os.Setenv("XDG_CONFIG_HOME", origConfigDir)
|
||||||
|
|
||||||
// Initially empty
|
// Initially empty
|
||||||
templates, err := ListLearnedTemplates()
|
templates, err := ListLearnedTemplates()
|
||||||
@@ -330,18 +334,10 @@ func TestListLearnedTemplates(t *testing.T) {
|
|||||||
|
|
||||||
// Create some templates
|
// Create some templates
|
||||||
dir := LearnedTemplateDir()
|
dir := LearnedTemplateDir()
|
||||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
os.MkdirAll(dir, 0o755)
|
||||||
t.Fatal(err)
|
os.WriteFile(filepath.Join(dir, "opencode.json"), []byte("{}"), 0o644)
|
||||||
}
|
os.WriteFile(filepath.Join(dir, "myapp.json"), []byte("{}"), 0o644)
|
||||||
if err := os.WriteFile(filepath.Join(dir, "opencode.json"), []byte("{}"), 0o600); err != nil {
|
os.WriteFile(filepath.Join(dir, "notjson.txt"), []byte(""), 0o644) // should be ignored
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "myapp.json"), []byte("{}"), 0o600); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "notjson.txt"), []byte(""), 0o600); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
templates, err = ListLearnedTemplates()
|
templates, err = ListLearnedTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -419,7 +415,9 @@ func TestBuildTemplateNoAllowRead(t *testing.T) {
|
|||||||
func TestGenerateLearnedTemplate(t *testing.T) {
|
func TestGenerateLearnedTemplate(t *testing.T) {
|
||||||
// Create a temp dir for templates
|
// Create a temp dir for templates
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
origConfigDir := os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
|
defer os.Setenv("XDG_CONFIG_HOME", origConfigDir)
|
||||||
|
|
||||||
// Create a fake strace log
|
// Create a fake strace log
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
@@ -432,7 +430,7 @@ func TestGenerateLearnedTemplate(t *testing.T) {
|
|||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
logFile := filepath.Join(tmpDir, "strace.log")
|
logFile := filepath.Join(tmpDir, "strace.log")
|
||||||
if err := os.WriteFile(logFile, []byte(logContent), 0o600); err != nil {
|
if err := os.WriteFile(logFile, []byte(logContent), 0o644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +444,7 @@ func TestGenerateLearnedTemplate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read and verify template
|
// Read and verify template
|
||||||
data, err := os.ReadFile(templatePath) //nolint:gosec // reading test-generated template file
|
data, err := os.ReadFile(templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read template: %v", err)
|
t.Fatalf("failed to read template: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,14 +519,8 @@ func buildDenyByDefaultMounts(cfg *config.Config, cwd string, debug bool) []stri
|
|||||||
if fileExists(p) && canMountOver(p) &&
|
if fileExists(p) && canMountOver(p) &&
|
||||||
!strings.HasPrefix(p, "/dev/") && !strings.HasPrefix(p, "/proc/") && !boundPaths[p] {
|
!strings.HasPrefix(p, "/dev/") && !strings.HasPrefix(p, "/proc/") && !boundPaths[p] {
|
||||||
boundPaths[p] = true
|
boundPaths[p] = true
|
||||||
// Create intermediary dirs if needed.
|
// Create intermediary dirs if needed
|
||||||
// For files, only create dirs up to the parent to avoid
|
for _, dir := range intermediaryDirs("/", p) {
|
||||||
// creating a directory at the file's path.
|
|
||||||
dirTarget := p
|
|
||||||
if !isDirectory(p) {
|
|
||||||
dirTarget = filepath.Dir(p)
|
|
||||||
}
|
|
||||||
for _, dir := range intermediaryDirs("/", dirTarget) {
|
|
||||||
if !isSystemMountPoint(dir) {
|
if !isSystemMountPoint(dir) {
|
||||||
args = append(args, "--dir", dir)
|
args = append(args, "--dir", dir)
|
||||||
}
|
}
|
||||||
@@ -539,11 +533,7 @@ func buildDenyByDefaultMounts(cfg *config.Config, cwd string, debug bool) []stri
|
|||||||
if !ContainsGlobChars(normalized) && fileExists(normalized) && canMountOver(normalized) &&
|
if !ContainsGlobChars(normalized) && fileExists(normalized) && canMountOver(normalized) &&
|
||||||
!strings.HasPrefix(normalized, "/dev/") && !strings.HasPrefix(normalized, "/proc/") && !boundPaths[normalized] {
|
!strings.HasPrefix(normalized, "/dev/") && !strings.HasPrefix(normalized, "/proc/") && !boundPaths[normalized] {
|
||||||
boundPaths[normalized] = true
|
boundPaths[normalized] = true
|
||||||
dirTarget := normalized
|
for _, dir := range intermediaryDirs("/", normalized) {
|
||||||
if !isDirectory(normalized) {
|
|
||||||
dirTarget = filepath.Dir(normalized)
|
|
||||||
}
|
|
||||||
for _, dir := range intermediaryDirs("/", dirTarget) {
|
|
||||||
if !isSystemMountPoint(dir) {
|
if !isSystemMountPoint(dir) {
|
||||||
args = append(args, "--dir", dir)
|
args = append(args, "--dir", dir)
|
||||||
}
|
}
|
||||||
@@ -564,7 +554,7 @@ func buildDenyByDefaultMounts(cfg *config.Config, cwd string, debug bool) []stri
|
|||||||
if emptyFile == "" {
|
if emptyFile == "" {
|
||||||
emptyFile = filepath.Join(os.TempDir(), "greywall", "empty")
|
emptyFile = filepath.Join(os.TempDir(), "greywall", "empty")
|
||||||
_ = os.MkdirAll(filepath.Dir(emptyFile), 0o750)
|
_ = os.MkdirAll(filepath.Dir(emptyFile), 0o750)
|
||||||
_ = os.WriteFile(emptyFile, nil, 0o444) //nolint:gosec // intentionally world-readable empty file for bind-mount masking
|
_ = os.WriteFile(emptyFile, nil, 0o444)
|
||||||
}
|
}
|
||||||
args = append(args, "--ro-bind", emptyFile, p)
|
args = append(args, "--ro-bind", emptyFile, p)
|
||||||
if debug {
|
if debug {
|
||||||
@@ -685,16 +675,15 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
|
|||||||
|
|
||||||
defaultDenyRead := cfg != nil && cfg.Filesystem.IsDefaultDenyRead()
|
defaultDenyRead := cfg != nil && cfg.Filesystem.IsDefaultDenyRead()
|
||||||
|
|
||||||
switch {
|
if opts.Learning {
|
||||||
case opts.Learning:
|
|
||||||
// Skip defaultDenyRead logic in learning mode (already set up above)
|
// Skip defaultDenyRead logic in learning mode (already set up above)
|
||||||
case defaultDenyRead:
|
} else if defaultDenyRead {
|
||||||
// Deny-by-default mode: start with empty root, then whitelist system paths + CWD
|
// Deny-by-default mode: start with empty root, then whitelist system paths + CWD
|
||||||
if opts.Debug {
|
if opts.Debug {
|
||||||
fmt.Fprintf(os.Stderr, "[greywall:linux] DefaultDenyRead mode enabled - tmpfs root with selective mounts\n")
|
fmt.Fprintf(os.Stderr, "[greywall:linux] DefaultDenyRead mode enabled - tmpfs root with selective mounts\n")
|
||||||
}
|
}
|
||||||
bwrapArgs = append(bwrapArgs, buildDenyByDefaultMounts(cfg, cwd, opts.Debug)...)
|
bwrapArgs = append(bwrapArgs, buildDenyByDefaultMounts(cfg, cwd, opts.Debug)...)
|
||||||
default:
|
} else {
|
||||||
// Legacy mode: bind entire root filesystem read-only
|
// Legacy mode: bind entire root filesystem read-only
|
||||||
bwrapArgs = append(bwrapArgs, "--ro-bind", "/", "/")
|
bwrapArgs = append(bwrapArgs, "--ro-bind", "/", "/")
|
||||||
}
|
}
|
||||||
@@ -930,7 +919,7 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
|
|||||||
// Supported by glibc, Go 1.21+, c-ares, and most DNS resolver libraries.
|
// Supported by glibc, Go 1.21+, c-ares, and most DNS resolver libraries.
|
||||||
_, _ = tmpResolv.WriteString("nameserver 1.1.1.1\nnameserver 8.8.8.8\noptions use-vc\n")
|
_, _ = tmpResolv.WriteString("nameserver 1.1.1.1\nnameserver 8.8.8.8\noptions use-vc\n")
|
||||||
}
|
}
|
||||||
_ = tmpResolv.Close()
|
tmpResolv.Close()
|
||||||
dnsRelayResolvConf = tmpResolv.Name()
|
dnsRelayResolvConf = tmpResolv.Name()
|
||||||
bwrapArgs = append(bwrapArgs, "--ro-bind", dnsRelayResolvConf, "/etc/resolv.conf")
|
bwrapArgs = append(bwrapArgs, "--ro-bind", dnsRelayResolvConf, "/etc/resolv.conf")
|
||||||
if opts.Debug {
|
if opts.Debug {
|
||||||
@@ -1078,8 +1067,7 @@ sleep 0.3
|
|||||||
// after the main command exits; the user can Ctrl+C to stop it.
|
// after the main command exits; the user can Ctrl+C to stop it.
|
||||||
// A SIGCHLD trap kills strace once its direct child exits, handling
|
// A SIGCHLD trap kills strace once its direct child exits, handling
|
||||||
// the common case of background daemons (LSP servers, watchers).
|
// the common case of background daemons (LSP servers, watchers).
|
||||||
switch {
|
if opts.Learning && opts.StraceLogPath != "" {
|
||||||
case opts.Learning && opts.StraceLogPath != "":
|
|
||||||
innerScript.WriteString(fmt.Sprintf(`# Learning mode: trace filesystem access (foreground for terminal access)
|
innerScript.WriteString(fmt.Sprintf(`# Learning mode: trace filesystem access (foreground for terminal access)
|
||||||
strace -f -qq -I2 -e trace=openat,open,creat,mkdir,mkdirat,unlinkat,renameat,renameat2,symlinkat,linkat -o %s -- %s
|
strace -f -qq -I2 -e trace=openat,open,creat,mkdir,mkdirat,unlinkat,renameat,renameat2,symlinkat,linkat -o %s -- %s
|
||||||
GREYWALL_STRACE_EXIT=$?
|
GREYWALL_STRACE_EXIT=$?
|
||||||
@@ -1091,7 +1079,7 @@ exit $GREYWALL_STRACE_EXIT
|
|||||||
`,
|
`,
|
||||||
ShellQuoteSingle(opts.StraceLogPath), command,
|
ShellQuoteSingle(opts.StraceLogPath), command,
|
||||||
))
|
))
|
||||||
case useLandlockWrapper:
|
} else if useLandlockWrapper {
|
||||||
// Use Landlock wrapper if available
|
// Use Landlock wrapper if available
|
||||||
// Pass config via environment variable (serialized as JSON)
|
// Pass config via environment variable (serialized as JSON)
|
||||||
// This ensures allowWrite/denyWrite rules are properly applied
|
// This ensures allowWrite/denyWrite rules are properly applied
|
||||||
@@ -1112,7 +1100,7 @@ exit $GREYWALL_STRACE_EXIT
|
|||||||
|
|
||||||
// Use exec to replace bash with the wrapper (which will exec the command)
|
// Use exec to replace bash with the wrapper (which will exec the command)
|
||||||
innerScript.WriteString(fmt.Sprintf("exec %s\n", ShellQuote(wrapperArgs)))
|
innerScript.WriteString(fmt.Sprintf("exec %s\n", ShellQuote(wrapperArgs)))
|
||||||
default:
|
} else {
|
||||||
innerScript.WriteString(command)
|
innerScript.WriteString(command)
|
||||||
innerScript.WriteString("\n")
|
innerScript.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ func suggestInstallCmd(features *LinuxFeatures) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readSysctl(name string) string {
|
func readSysctl(name string) string {
|
||||||
data, err := os.ReadFile("/proc/sys/" + name) //nolint:gosec // reading sysctl values - trusted kernel path
|
data, err := os.ReadFile("/proc/sys/" + name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ type LandlockRuleset struct {
|
|||||||
func NewLandlockRuleset(debug bool) (*LandlockRuleset, error) {
|
func NewLandlockRuleset(debug bool) (*LandlockRuleset, error) {
|
||||||
features := DetectLinuxFeatures()
|
features := DetectLinuxFeatures()
|
||||||
if !features.CanUseLandlock() {
|
if !features.CanUseLandlock() {
|
||||||
return nil, fmt.Errorf("landlock not available (kernel %d.%d, need 5.13+)",
|
return nil, fmt.Errorf("Landlock not available (kernel %d.%d, need 5.13+)",
|
||||||
features.KernelMajor, features.KernelMinor)
|
features.KernelMajor, features.KernelMinor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +438,7 @@ func (l *LandlockRuleset) addPathRule(path string, access uint64) error {
|
|||||||
// Apply applies the Landlock ruleset to the current process.
|
// Apply applies the Landlock ruleset to the current process.
|
||||||
func (l *LandlockRuleset) Apply() error {
|
func (l *LandlockRuleset) Apply() error {
|
||||||
if !l.initialized {
|
if !l.initialized {
|
||||||
return fmt.Errorf("landlock ruleset not initialized")
|
return fmt.Errorf("Landlock ruleset not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set NO_NEW_PRIVS first (required for Landlock)
|
// Set NO_NEW_PRIVS first (required for Landlock)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (m *Manager) Initialize() error {
|
|||||||
bridge, err := NewProxyBridge(m.config.Network.ProxyURL, m.debug)
|
bridge, err := NewProxyBridge(m.config.Network.ProxyURL, m.debug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if m.tun2socksPath != "" {
|
if m.tun2socksPath != "" {
|
||||||
_ = os.Remove(m.tun2socksPath)
|
os.Remove(m.tun2socksPath)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to initialize proxy bridge: %w", err)
|
return fmt.Errorf("failed to initialize proxy bridge: %w", err)
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ func (m *Manager) Initialize() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
m.proxyBridge.Cleanup()
|
m.proxyBridge.Cleanup()
|
||||||
if m.tun2socksPath != "" {
|
if m.tun2socksPath != "" {
|
||||||
_ = os.Remove(m.tun2socksPath)
|
os.Remove(m.tun2socksPath)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to initialize DNS bridge: %w", err)
|
return fmt.Errorf("failed to initialize DNS bridge: %w", err)
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ func (m *Manager) Initialize() error {
|
|||||||
m.proxyBridge.Cleanup()
|
m.proxyBridge.Cleanup()
|
||||||
}
|
}
|
||||||
if m.tun2socksPath != "" {
|
if m.tun2socksPath != "" {
|
||||||
_ = os.Remove(m.tun2socksPath)
|
os.Remove(m.tun2socksPath)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to initialize reverse bridge: %w", err)
|
return fmt.Errorf("failed to initialize reverse bridge: %w", err)
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ func (m *Manager) wrapCommandLearning(command string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create strace log file: %w", err)
|
return "", fmt.Errorf("failed to create strace log file: %w", err)
|
||||||
}
|
}
|
||||||
_ = tmpFile.Close()
|
tmpFile.Close()
|
||||||
m.straceLogPath = tmpFile.Name()
|
m.straceLogPath = tmpFile.Name()
|
||||||
|
|
||||||
m.logDebug("Strace log file: %s", m.straceLogPath)
|
m.logDebug("Strace log file: %s", m.straceLogPath)
|
||||||
@@ -193,7 +193,7 @@ func (m *Manager) GenerateLearnedTemplate(cmdName string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up strace log since we've processed it
|
// Clean up strace log since we've processed it
|
||||||
_ = os.Remove(m.straceLogPath)
|
os.Remove(m.straceLogPath)
|
||||||
m.straceLogPath = ""
|
m.straceLogPath = ""
|
||||||
|
|
||||||
return templatePath, nil
|
return templatePath, nil
|
||||||
@@ -211,10 +211,10 @@ func (m *Manager) Cleanup() {
|
|||||||
m.proxyBridge.Cleanup()
|
m.proxyBridge.Cleanup()
|
||||||
}
|
}
|
||||||
if m.tun2socksPath != "" {
|
if m.tun2socksPath != "" {
|
||||||
_ = os.Remove(m.tun2socksPath)
|
os.Remove(m.tun2socksPath)
|
||||||
}
|
}
|
||||||
if m.straceLogPath != "" {
|
if m.straceLogPath != "" {
|
||||||
_ = os.Remove(m.straceLogPath)
|
os.Remove(m.straceLogPath)
|
||||||
m.straceLogPath = ""
|
m.straceLogPath = ""
|
||||||
}
|
}
|
||||||
m.logDebug("Sandbox manager cleaned up")
|
m.logDebug("Sandbox manager cleaned up")
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ func extractTun2Socks() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tmpFile.Write(data); err != nil {
|
if _, err := tmpFile.Write(data); err != nil {
|
||||||
_ = tmpFile.Close()
|
tmpFile.Close()
|
||||||
_ = os.Remove(tmpFile.Name())
|
os.Remove(tmpFile.Name())
|
||||||
return "", fmt.Errorf("tun2socks: failed to write binary: %w", err)
|
return "", fmt.Errorf("tun2socks: failed to write binary: %w", err)
|
||||||
}
|
}
|
||||||
_ = tmpFile.Close()
|
tmpFile.Close()
|
||||||
|
|
||||||
if err := os.Chmod(tmpFile.Name(), 0o755); err != nil { //nolint:gosec // executable binary needs execute permission
|
if err := os.Chmod(tmpFile.Name(), 0o755); err != nil {
|
||||||
_ = os.Remove(tmpFile.Name())
|
os.Remove(tmpFile.Name())
|
||||||
return "", fmt.Errorf("tun2socks: failed to make executable: %w", err)
|
return "", fmt.Errorf("tun2socks: failed to make executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,5 +149,5 @@ git push origin "$NEW_VERSION"
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
info "✓ Released $NEW_VERSION"
|
info "✓ Released $NEW_VERSION"
|
||||||
info "Gitea Actions will now build and publish the release."
|
info "GitHub Actions will now build and publish the release."
|
||||||
info "Watch progress at: https://gitea.app.monadical.io/monadical/greywall/actions"
|
info "Watch progress at: https://gitea.app.monadical.io/monadical/greywall/actions"
|
||||||
|
|||||||
Reference in New Issue
Block a user