diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b65fa87..6425f1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,12 @@ jobs: uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version-file: go.mod + + - name: Lint + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 + with: + # This version number must be kept in sync with Makefile lint one. + version: v2.10.1 - name: Test run: go test ./... diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..cc54ea9 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,58 @@ +version: "2" + +run: + timeout: 5m + +linters: + default: none + enable: + - errcheck + - errorlint + - govet + - ineffassign + - gosec + - staticcheck + - unused + settings: + errorlint: + errorf: true + asserts: true + comparison: true + staticcheck: + checks: + - all + gosec: + excludes: + # Integer overflow conversion is acceptable in BPF context (e.g. mntns_id, pid). + - G115 + exclusions: + generated: lax + rules: + # Ignore check: Packages must have a package comment + - linters: + - staticcheck + text: "ST1000: at least one file in a package should have a package comment" + # Ignore rule about ID vs Id: https://github.com/golang/lint/issues/89 + - linters: + - staticcheck + text: "ST1003:" + paths: + # Exclude generated build directory + - ^build/ + +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + local-prefixes: + - github.com/micromize-dev/micromize + exclusions: + generated: lax + paths: + - ^build/ + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/Dockerfiles/linter.Dockerfile b/Dockerfiles/linter.Dockerfile new file mode 100644 index 0000000..6803aa1 --- /dev/null +++ b/Dockerfiles/linter.Dockerfile @@ -0,0 +1,6 @@ +ARG IMAGE +FROM ${IMAGE} + +# The timeout specified below is used by 'make lint'. Please keep in sync with +# the timeout specified in .golangci.yml used by the CI. +ENTRYPOINT ["golangci-lint", "run", "--timeout=5m0s"] diff --git a/Makefile b/Makefile index 0e8d962..ec3eea9 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ LDFLAGS := -X github.com/inspektor-gadget/inspektor-gadget/internal/version.vers GADGETS := fs-restrict cap-restrict ptrace-restrict binary-attestation CONFORM_VERSION ?= v0.1.0-alpha.30 +# This version number must be kept in sync with CI workflow lint one. +LINTER_IMAGE ?= golangci/golangci-lint:v2.10.1 + .PHONY: setup-hooks setup-hooks: go install github.com/siderolabs/conform/cmd/conform@$(CONFORM_VERSION) @@ -26,6 +29,22 @@ license-add: @go run github.com/google/addlicense@v1.2.0 -y "" -l apache -c "The micromize authors" \ $$(find . -name '*.go' -not -path './build/*') +.PHONY: lint +lint: + docker build -t linter -f Dockerfiles/linter.Dockerfile --build-arg IMAGE=$(LINTER_IMAGE) Dockerfiles + # XDG_CACHE_HOME is necessary to avoid this type of errors: + # ERRO Running error: context loading failed: failed to load packages: failed to load with go/packages: err: exit status 1: stderr: failed to initialize build cache at /.cache/go-build: mkdir /.cache: permission denied + # Process 15167 has exited with status 3 + # While GOLANGCI_LINT_CACHE is used to store golangci-lint cache. + docker run --rm --env XDG_CACHE_HOME=/tmp/xdg_home_cache \ + --env GOLANGCI_LINT_CACHE=/tmp/golangci_lint_cache \ + --user $(shell id -u):$(shell id -g) -v $(shell pwd):/app -w /app \ + linter + +.PHONY: clean +clean: + rm -rf $(OUTPUT_DIR) build/src build/gadgets + .PHONY: build-all build-all: $(GADGETS) build-app diff --git a/cmd/micromize/root.go b/cmd/micromize/root.go index 0c92ad0..723d061 100644 --- a/cmd/micromize/root.go +++ b/cmd/micromize/root.go @@ -24,12 +24,13 @@ import ( "strings" "syscall" + "github.com/spf13/cobra" + "github.com/micromize-dev/micromize/internal/gadget" "github.com/micromize-dev/micromize/internal/logger" "github.com/micromize-dev/micromize/internal/operators" "github.com/micromize-dev/micromize/internal/runtime" "github.com/micromize-dev/micromize/internal/utils" - "github.com/spf13/cobra" ) const ( diff --git a/go.mod b/go.mod index 7fad951..943f053 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,16 @@ module github.com/micromize-dev/micromize go 1.25.5 require ( + github.com/cilium/ebpf v0.20.0 + github.com/cyphar/filepath-securejoin v0.5.1 + github.com/docker/cli v29.2.0+incompatible + github.com/go-jose/go-jose/v4 v4.1.3 github.com/inspektor-gadget/inspektor-gadget v0.49.1 + github.com/opencontainers/image-spec v1.1.1 github.com/quay/claircore v1.5.45 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 + golang.org/x/sync v0.19.0 oras.land/oras-go/v2 v2.6.0 ) @@ -21,7 +27,6 @@ require ( github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cilium/ebpf v0.20.0 // indirect github.com/cloudflare/cbpfc v0.0.0-20240920015331-ff978e94500b // indirect github.com/containerd/cgroups/v3 v3.0.5 // indirect github.com/containerd/containerd v1.7.30 // indirect @@ -35,10 +40,8 @@ require ( github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect - github.com/cyphar/filepath-securejoin v0.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v29.2.0+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-connections v0.6.0 // indirect @@ -98,7 +101,6 @@ require ( github.com/notaryproject/notation-plugin-framework-go v1.0.0 // indirect github.com/notaryproject/tspclient-go v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/opencontainers/selinux v1.13.1 // indirect github.com/packetcap/go-pcap v0.0.0-20250723190045-d00b185f30b7 // indirect @@ -139,7 +141,6 @@ require ( golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.39.0 // indirect golang.org/x/text v0.33.0 // indirect diff --git a/go.sum b/go.sum index d62d14b..032b209 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl5 github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/internal/operators/operators.go b/internal/operators/operators.go index 6c58c95..4a9b400 100644 --- a/internal/operators/operators.go +++ b/internal/operators/operators.go @@ -43,7 +43,9 @@ type DataOperator = igoperators.DataOperator func NewLocalManager() (igoperators.DataOperator, error) { slog.Debug("Initializing local manager operator") - host.Init(host.Config{}) + if err := host.Init(host.Config{}); err != nil { + return nil, fmt.Errorf("init host: %w", err) + } localManagerOp := localmanager.LocalManagerOperator localManagerParams := localManagerOp.GlobalParamDescs().ToParams() @@ -244,7 +246,9 @@ func populateExpectedHashes(gadgetCtx igoperators.GadgetContext, innerMaps *sync // Insert the inner map into the outer map keyed by mntns_id if err := outerMap.Put(mntnsID, uint32(innerMap.FD())); err != nil { slog.Error("Failed to insert inner map into expected_hashes", "mntns_id", mntnsID, "error", err) - innerMap.Close() + if err := innerMap.Close(); err != nil { + slog.Error("Failed to close inner BPF map", "mntns_id", mntnsID, "error", err) + } return } @@ -281,7 +285,9 @@ func handleContainerRemoved(gadgetCtx igoperators.GadgetContext, innerMaps *sync if val, loaded := innerMaps.LoadAndDelete(mntnsID); loaded { if m, ok := val.(*ebpf.Map); ok && m != nil { - m.Close() + if err := m.Close(); err != nil { + slog.Debug("Failed to close inner BPF map on container removal", "mntns_id", mntnsID, "error", err) + } } } diff --git a/internal/operators/output.go b/internal/operators/output.go index d884d36..9ba0ed0 100644 --- a/internal/operators/output.go +++ b/internal/operators/output.go @@ -237,7 +237,9 @@ func formatAndPrintEvent(f *eventFields, data datasource.Data) { } outputMu.Lock() - fmt.Fprintln(os.Stdout, sb.String()) + if _, err := fmt.Fprintln(os.Stdout, sb.String()); err != nil { + slog.Error("Failed to write event output", "error", err) + } outputMu.Unlock() } diff --git a/internal/runtime/manager.go b/internal/runtime/manager.go index df24cc8..2c3a2ea 100644 --- a/internal/runtime/manager.go +++ b/internal/runtime/manager.go @@ -49,5 +49,7 @@ func (m *Manager) RunGadget(gadgetCtx *gadgetcontext.GadgetContext, params map[s // Close cleans up runtime resources func (m *Manager) Close() { slog.Debug("Closing runtime manager") - m.runtime.Close() + if err := m.runtime.Close(); err != nil { + slog.Error("Error closing runtime", "error", err) + } } diff --git a/internal/sbom/sbom.go b/internal/sbom/sbom.go index b884402..e5421ac 100644 --- a/internal/sbom/sbom.go +++ b/internal/sbom/sbom.go @@ -28,6 +28,7 @@ import ( "strings" "sync" + securejoin "github.com/cyphar/filepath-securejoin" dockerconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -135,8 +136,11 @@ func ImageRefFromDockerConfig(containerID string) string { } for _, root := range dockerDataRootsFn() { - cfgPath := filepath.Join(root, "containers", containerID, "config.v2.json") - data, err := os.ReadFile(cfgPath) + cfgPath, err := securejoin.SecureJoin(root, filepath.Join("containers", containerID, "config.v2.json")) + if err != nil { + continue + } + data, err := os.ReadFile(filepath.Clean(cfgPath)) if err != nil { continue } @@ -335,7 +339,11 @@ func fetchCosignAttestation(ctx context.Context, repo *remote.Repository, imageD if err != nil { return nil, fmt.Errorf("fetching cosign attestation manifest: %w", err) } - defer rc.Close() + defer func() { + if err := rc.Close(); err != nil { + slog.Debug("Failed to close cosign attestation manifest reader", "error", err) + } + }() manifestBytes, err := io.ReadAll(rc) if err != nil { @@ -368,7 +376,11 @@ func fetchDSSEEnvelope(ctx context.Context, repo *remote.Repository, layer ocisp if err != nil { return nil, fmt.Errorf("fetching DSSE envelope: %w", err) } - defer layerRC.Close() + defer func() { + if err := layerRC.Close(); err != nil { + slog.Debug("Failed to close DSSE envelope reader", "error", err) + } + }() envelopeBytes, err := io.ReadAll(layerRC) if err != nil { @@ -459,10 +471,14 @@ func loadDockerConfig() (*configfile.ConfigFile, error) { return cfg, nil } - sudoHome := filepath.Join("/home", sudoUser) - sudoCfg, err := dockerconfig.Load(filepath.Join(sudoHome, ".docker")) + sudoHome, err := securejoin.SecureJoin("/home", sudoUser) + if err != nil { + slog.Debug("Could not resolve home directory for SUDO_USER", "error", err) + return cfg, nil + } + sudoCfg, err := dockerconfig.Load(filepath.Clean(filepath.Join(sudoHome, ".docker"))) if err != nil { - slog.Debug("Could not load Docker config for SUDO_USER", "user", sudoUser, "error", err) + slog.Debug("Could not load Docker config for SUDO_USER") return cfg, nil } diff --git a/internal/sbom/sbom_test.go b/internal/sbom/sbom_test.go index 1ac6267..83e6af2 100644 --- a/internal/sbom/sbom_test.go +++ b/internal/sbom/sbom_test.go @@ -20,6 +20,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/go-jose/go-jose/v4/testutils/require" ) func TestNormalizeImageRef(t *testing.T) { @@ -99,14 +101,17 @@ func TestImageRefFromDockerConfig(t *testing.T) { tmpDir := t.TempDir() containerID := "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" containerDir := filepath.Join(tmpDir, "containers", containerID) - if err := os.MkdirAll(containerDir, 0o755); err != nil { + if err := os.MkdirAll(containerDir, 0o750); err != nil { t.Fatal(err) } writeConfig := func(image string) { cfg := map[string]any{"Config": map[string]string{"Image": image}} - data, _ := json.Marshal(cfg) - os.WriteFile(filepath.Join(containerDir, "config.v2.json"), data, 0o644) + data, err := json.Marshal(cfg) + require.NoError(t, err) + if err := os.WriteFile(filepath.Join(containerDir, "config.v2.json"), data, 0o600); err != nil { + t.Fatal(err) + } } // Temporarily override dockerDataRoots to use our tmpDir. diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 30cbf96..71127be 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -59,7 +59,7 @@ func GetHostPidNamespaceID() (uint64, error) { func ValidateBPFLSM() error { data, err := os.ReadFile(lsmFilePath) if err != nil { - return fmt.Errorf("failed to read %s: %w\nBPF LSM support cannot be verified. Ensure the kernel has LSM support enabled.", lsmFilePath, err) + return fmt.Errorf("failed to read %s: Unable to verify whether BPF LSM is enabled: %w", lsmFilePath, err) } lsmList := strings.TrimSpace(string(data)) diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 534a571..f6adc0c 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -69,7 +69,7 @@ func TestValidateBPFLSM(t *testing.T) { tmpDir := t.TempDir() tmpFile := filepath.Join(tmpDir, "lsm") - if err := os.WriteFile(tmpFile, []byte(tt.lsmContent), 0644); err != nil { + if err := os.WriteFile(tmpFile, []byte(tt.lsmContent), 0600); err != nil { t.Fatalf("Failed to create temp file: %v", err) }