5 Commits

Author SHA1 Message Date
5aeb9c86c0 fix: resolve all golangci-lint v2 warnings (29 issues)
Some checks failed
Build and test / Build (push) Successful in 11s
Build and test / Lint (push) Failing after 1m15s
Build and test / Test (Linux) (push) Failing after 42s
Migrate to golangci-lint v2 config format and fix all lint issues:
- errcheck: add explicit error handling for Close/Remove calls
- gocritic: convert if-else chains to switch statements
- gosec: tighten file permissions, add nolint for intentional cases
- staticcheck: lowercase error strings, simplify boolean returns

Also update Makefile to install golangci-lint v2 and update CLAUDE.md.
2026-02-13 19:20:40 -06:00
626eaa1895 fix: upgrade golangci
Some checks failed
Build and test / Build (push) Successful in 12s
Build and test / Lint (push) Failing after 1m15s
Build and test / Test (Linux) (push) Failing after 40s
2026-02-13 19:13:37 -06:00
18c18ec3a8 fix: avoid creating directory at file path in allowRead bwrap mounts
Some checks failed
Build and test / Build (push) Successful in 12s
Build and test / Lint (push) Failing after 1m17s
Build and test / Test (Linux) (push) Failing after 44s
intermediaryDirs() was called with the full path including the leaf
component, causing --dir to be emitted for files like ~/.npmrc. This
created a directory at that path, making the subsequent --ro-bind fail
with "Can't create file at ...: Is a directory".

Now checks isDirectory() and uses filepath.Dir() for file paths so
intermediary dirs are only created up to the parent.
2026-02-13 13:53:19 -06:00
f4c9422f77 feat: migrate CI and releases from GitHub Actions to Gitea Actions
Retarget GoReleaser to publish to Gitea (gitea_urls, release.gitea,
changelog.use: gitea). Add Gitea Actions workflows for build/test,
release, and benchmarks — adapted from GitHub equivalents with macOS
jobs and SLSA provenance dropped. Old .github/workflows/ kept in place.
2026-02-13 12:20:32 -06:00
c19370f8b3 feat: deny-by-default filesystem isolation
Some checks failed
Build and test / Lint (push) Failing after 1m16s
Build and test / Build (push) Successful in 13s
Build and test / Test (Linux) (push) Failing after 41s
Build and test / Test (macOS) (push) Has been cancelled
- Deny-by-default filesystem isolation for Linux (Landlock) and macOS (Seatbelt)
- Prevent learning mode from collapsing read paths to $HOME
- Add Linux deny-by-default lessons to experience docs
2026-02-13 11:39:18 -06:00
18 changed files with 396 additions and 93 deletions

View File

@@ -0,0 +1,101 @@
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

115
.gitea/workflows/main.yml Normal file
View File

@@ -0,0 +1,115 @@
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

View File

@@ -0,0 +1,62 @@
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

View File

@@ -1,40 +1,49 @@
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:
enable-all: false default: none
disable-all: true
enable: enable:
- staticcheck
- errcheck - errcheck
- gosimple
- govet
- unused
- ineffassign
- gosec
- gocritic - gocritic
- revive - gosec
- gofumpt - govet
- ineffassign
- misspell - misspell
- revive
issues: - staticcheck
exclude-use-default: false - unused
settings:
gocritic:
disabled-checks:
- singleCaseSwitch
revive:
rules:
- name: exported
disabled: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofumpt
settings:
gci:
sections:
- standard
- 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$

View File

@@ -1,5 +1,10 @@
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
@@ -42,7 +47,7 @@ checksum:
changelog: changelog:
sort: asc sort: asc
use: github use: gitea
format: "{{ .SHA }}: {{ .Message }}{{ with .AuthorUsername }} (@{{ . }}){{ end }}" format: "{{ .SHA }}: {{ .Message }}{{ with .AuthorUsername }} (@{{ . }}){{ end }}"
filters: filters:
exclude: exclude:
@@ -76,7 +81,7 @@ changelog:
order: 9999 order: 9999
release: release:
github: gitea:
owner: monadical owner: monadical
name: greywall name: greywall
draft: false draft: false

View File

@@ -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` v1.64.8 (config in `.golangci.yml`) - **Linter:** `golangci-lint` v2 (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`)

View File

@@ -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/cmd/golangci-lint@latest go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
@echo "Linting tools installed" @echo "Linting tools installed"
setup: deps install-lint-tools setup: deps install-lint-tools

View File

@@ -203,19 +203,20 @@ func runCommand(cmd *cobra.Command, args []string) error {
if templatePath != "" { if templatePath != "" {
learnedCfg, loadErr := config.Load(templatePath) learnedCfg, loadErr := config.Load(templatePath)
if loadErr != nil { switch {
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)
} }
} else if learnedCfg != nil { case 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)
} }
} else if templateName != "" { case 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)
} else if cmdName != "" { case 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)
} }
@@ -503,7 +504,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) data, err := os.ReadFile(templatePath) //nolint:gosec // user-specified template path - intentional
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)

View File

@@ -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), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(templatePath), 0o750); 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), 0o644); err != nil { if err := os.WriteFile(templatePath, []byte(template), 0o600); err != nil {
return "", fmt.Errorf("failed to write template: %w", err) return "", fmt.Errorf("failed to write template: %w", err)
} }
@@ -305,11 +305,7 @@ func isSensitivePath(path, home string) bool {
// Check GPG // Check GPG
gnupgDir := filepath.Join(home, ".gnupg") gnupgDir := filepath.Join(home, ".gnupg")
if strings.HasPrefix(path, gnupgDir+"/") { return strings.HasPrefix(path, gnupgDir+"/")
return true
}
return false
} }
// getDangerousFilePatterns returns denyWrite entries for DangerousFiles. // getDangerousFilePatterns returns denyWrite entries for DangerousFiles.

View File

@@ -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 f.Close() defer func() { _ = f.Close() }()
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
seenWrite := make(map[string]bool) seenWrite := make(map[string]bool)

View File

@@ -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), 0o644); err != nil { if err := os.WriteFile(logFile, []byte(logContent), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -70,9 +70,7 @@ 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
origHome := os.Getenv("HOME") t.Setenv("HOME", "/home/testuser")
os.Setenv("HOME", "/home/testuser")
defer os.Setenv("HOME", origHome)
tests := []struct { tests := []struct {
name string name string
@@ -319,9 +317,7 @@ 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()
origConfigDir := os.Getenv("XDG_CONFIG_HOME") t.Setenv("XDG_CONFIG_HOME", tmpDir)
os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer os.Setenv("XDG_CONFIG_HOME", origConfigDir)
// Initially empty // Initially empty
templates, err := ListLearnedTemplates() templates, err := ListLearnedTemplates()
@@ -334,10 +330,18 @@ func TestListLearnedTemplates(t *testing.T) {
// Create some templates // Create some templates
dir := LearnedTemplateDir() dir := LearnedTemplateDir()
os.MkdirAll(dir, 0o755) if err := os.MkdirAll(dir, 0o750); err != nil {
os.WriteFile(filepath.Join(dir, "opencode.json"), []byte("{}"), 0o644) t.Fatal(err)
os.WriteFile(filepath.Join(dir, "myapp.json"), []byte("{}"), 0o644) }
os.WriteFile(filepath.Join(dir, "notjson.txt"), []byte(""), 0o644) // should be ignored if err := os.WriteFile(filepath.Join(dir, "opencode.json"), []byte("{}"), 0o600); err != nil {
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 {
@@ -415,9 +419,7 @@ 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()
origConfigDir := os.Getenv("XDG_CONFIG_HOME") t.Setenv("XDG_CONFIG_HOME", tmpDir)
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()
@@ -430,7 +432,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), 0o644); err != nil { if err := os.WriteFile(logFile, []byte(logContent), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -444,7 +446,7 @@ func TestGenerateLearnedTemplate(t *testing.T) {
} }
// Read and verify template // Read and verify template
data, err := os.ReadFile(templatePath) data, err := os.ReadFile(templatePath) //nolint:gosec // reading test-generated template file
if err != nil { if err != nil {
t.Fatalf("failed to read template: %v", err) t.Fatalf("failed to read template: %v", err)
} }

View File

@@ -519,8 +519,14 @@ 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 _, dir := range intermediaryDirs("/", p) { // For files, only create dirs up to the parent to avoid
// 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)
} }
@@ -533,7 +539,11 @@ 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
for _, dir := range intermediaryDirs("/", normalized) { dirTarget := 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)
} }
@@ -554,7 +564,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) _ = os.WriteFile(emptyFile, nil, 0o444) //nolint:gosec // intentionally world-readable empty file for bind-mount masking
} }
args = append(args, "--ro-bind", emptyFile, p) args = append(args, "--ro-bind", emptyFile, p)
if debug { if debug {
@@ -675,15 +685,16 @@ func WrapCommandLinuxWithOptions(cfg *config.Config, command string, proxyBridge
defaultDenyRead := cfg != nil && cfg.Filesystem.IsDefaultDenyRead() defaultDenyRead := cfg != nil && cfg.Filesystem.IsDefaultDenyRead()
if opts.Learning { switch {
case opts.Learning:
// Skip defaultDenyRead logic in learning mode (already set up above) // Skip defaultDenyRead logic in learning mode (already set up above)
} else if defaultDenyRead { case 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)...)
} else { default:
// 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", "/", "/")
} }
@@ -919,7 +930,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 {
@@ -1067,7 +1078,8 @@ 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).
if opts.Learning && opts.StraceLogPath != "" { switch {
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=$?
@@ -1079,7 +1091,7 @@ exit $GREYWALL_STRACE_EXIT
`, `,
ShellQuoteSingle(opts.StraceLogPath), command, ShellQuoteSingle(opts.StraceLogPath), command,
)) ))
} else if useLandlockWrapper { case 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
@@ -1100,7 +1112,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)))
} else { default:
innerScript.WriteString(command) innerScript.WriteString(command)
innerScript.WriteString("\n") innerScript.WriteString("\n")
} }

View File

@@ -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) data, err := os.ReadFile("/proc/sys/" + name) //nolint:gosec // reading sysctl values - trusted kernel path
if err != nil { if err != nil {
return "" return ""
} }

View File

@@ -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)

View File

@@ -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")

View File

@@ -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 { if err := os.Chmod(tmpFile.Name(), 0o755); err != nil { //nolint:gosec // executable binary needs execute permission
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)
} }

View File

@@ -149,5 +149,5 @@ git push origin "$NEW_VERSION"
echo "" echo ""
info "✓ Released $NEW_VERSION" info "✓ Released $NEW_VERSION"
info "GitHub Actions will now build and publish the release." info "Gitea 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"