From dce4f3c3e8ea91328015861c7dd223053140a332 Mon Sep 17 00:00:00 2001 From: Lukas Wuttke Date: Wed, 29 Jul 2026 11:21:47 +0200 Subject: [PATCH] chore(gosec): reviewed per-site #nosec waivers for the 8 reported findings (18 real sites) Each gosec finding got an individually reviewed inline waiver with a site-specific justification, so the advisory golangci job can go to zero and backend#1305 can later drop --issues-exit-code=0. The advertised backlog of 8 was an artifact of golangci-lint's default issues.max-same-issues=3: the 13 G304s share one message text, so only 3 surfaced per run (which 3 flapped with cache state). The real, uncapped backlog is 18: 4x G204, 13x G304, 1x G115. All 18 are waived here; waiving only the visible 8 would have been whack-a-mole. Hardening was considered per site and deliberately not bolted on: the meaningful control (symlink rejection on the dataset walk, re-checked at stream time) already exists, and filepath.Clean wrappers would silence gosec without confining anything. Comment-only change; no behavior touched. Verified with the CI-pinned golangci-lint v2.12.2 (go1.26.3): 0 issues with --max-same-issues=0 --max-issues-per-linter=0. Part of tracebloc/backend#1305 (epic #930, Layer 1). Co-Authored-By: Claude Fable 5 --- internal/cli/home_local_fallback.go | 2 +- internal/cli/ingest.go | 2 +- internal/cli/installlog.go | 2 +- internal/cli/prepare_host.go | 2 +- internal/cli/update_check.go | 2 +- internal/cli/upgrade.go | 2 +- internal/config/config.go | 2 +- internal/helm/upgrade.go | 2 +- internal/nodeboot/nodeboot.go | 2 +- internal/push/detect.go | 2 +- internal/push/image_resolution.go | 2 +- internal/push/preflight.go | 6 +++--- internal/push/stream.go | 2 +- internal/push/tabular.go | 2 +- internal/push/text.go | 2 +- internal/slug/slug.go | 2 +- 16 files changed, 18 insertions(+), 18 deletions(-) diff --git a/internal/cli/home_local_fallback.go b/internal/cli/home_local_fallback.go index 405660fc..0afa1f6b 100644 --- a/internal/cli/home_local_fallback.go +++ b/internal/cli/home_local_fallback.go @@ -110,7 +110,7 @@ func tbAliasAvailable() bool { // different tracebloc at another path (Bugbot). Case-insensitive: .cmd is a // Windows artifact and NTFS paths are case-insensitive. func tbCmdAliasOurs(dir, exe string) bool { - b, err := os.ReadFile(filepath.Join(dir, binTB+".cmd")) + b, err := os.ReadFile(filepath.Join(dir, binTB+".cmd")) // #nosec G304 -- fixed name next to os.Executable(): inspects the install dir's own tb.cmd shim; whoever controls that dir already controls the binary. if err != nil { return false } diff --git a/internal/cli/ingest.go b/internal/cli/ingest.go index 748b827f..59354072 100644 --- a/internal/cli/ingest.go +++ b/internal/cli/ingest.go @@ -75,7 +75,7 @@ Exit codes: func runIngestValidate(cmd *cobra.Command, args []string) error { path := args[0] - body, err := os.ReadFile(path) + body, err := os.ReadFile(path) // #nosec G304 -- reading the ingest.yaml the operator named as the positional arg is this command's documented job; local CLI, invoking user's privileges. if err != nil { // fileError is exit-code 3 territory. We use a sentinel // exit-coded error so cobra propagates the right code via diff --git a/internal/cli/installlog.go b/internal/cli/installlog.go index 8c45a232..c85ab75c 100644 --- a/internal/cli/installlog.go +++ b/internal/cli/installlog.go @@ -34,7 +34,7 @@ func newInstallLog() (*installLog, string) { return nil, "" } path := filepath.Join(dir, "install-"+time.Now().UTC().Format("20060102-150405")+".log") - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) // #nosec G304 -- creates (never reads) the install log at a timestamp-generated name under the CLI's own 0700 config dir, mode 0600. if err != nil { // No file was created — return an empty path so the caller never // advertises a "Full log:" location that doesn't exist (Bugbot). diff --git a/internal/cli/prepare_host.go b/internal/cli/prepare_host.go index d946aeba..cdfd8052 100644 --- a/internal/cli/prepare_host.go +++ b/internal/cli/prepare_host.go @@ -110,7 +110,7 @@ func prepareHostEnv(user string) []string { // that traps signals. We rely on the default SIGKILL rather than a custom // SIGINT-only Cancel (which a privileged child could ignore, hanging Wait). func prepareHostCmd(ctx context.Context) *exec.Cmd { - c := exec.CommandContext(ctx, "bash", "-c", prepareHostInstallerCmd) + c := exec.CommandContext(ctx, "bash", "-c", prepareHostInstallerCmd) // #nosec G204 -- argv is compile-time constant: literal "bash" -c installerRunScript("prepare-host"), built only from the installerURL const; no runtime input. c.WaitDelay = 5 * time.Second return c } diff --git a/internal/cli/update_check.go b/internal/cli/update_check.go index 8f317c48..f8214bd4 100644 --- a/internal/cli/update_check.go +++ b/internal/cli/update_check.go @@ -164,7 +164,7 @@ func readUpdateCache(path string) (updateCache, bool) { if path == "" { return updateCache{}, false } - raw, err := os.ReadFile(path) + raw, err := os.ReadFile(path) // #nosec G304 -- the CLI's own throttle cache: config.Dir() + the constant updateCacheFile name; contents JSON-validated before use. if err != nil { return updateCache{}, false } diff --git a/internal/cli/upgrade.go b/internal/cli/upgrade.go index 9c0d8b6f..686c1057 100644 --- a/internal/cli/upgrade.go +++ b/internal/cli/upgrade.go @@ -120,7 +120,7 @@ Safe to run anytime; safe to re-run.`, // Stream the installer straight to the user's terminal, and keep // stdin wired so its interactive prompts (sign-in, etc.) still work. ctx := cmd.Context() - c := exec.CommandContext(ctx, plan.name, plan.args...) + c := exec.CommandContext(ctx, plan.name, plan.args...) // #nosec G204 -- upgradePlanFor(runtime.GOOS) yields compile-time constants: "bash" -c installerRunScript(""); only the GOOS branch varies, no user input. c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr if err := c.Run(); err != nil { // User aborted (Ctrl-C) or the parent context was cancelled: exit diff --git a/internal/config/config.go b/internal/config/config.go index 5490ff3d..1e020c7d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -122,7 +122,7 @@ func Load() (*Config, error) { if err != nil { return nil, err } - data, err := os.ReadFile(path) + data, err := os.ReadFile(path) // #nosec G304 -- the CLI's own config: Dir()/config.json under ~/.tracebloc or the operator's explicit $TRACEBLOC_CONFIG_DIR override, read as the invoking user. if errors.Is(err, fs.ErrNotExist) { return &Config{Version: schemaVersion, Profiles: map[string]*Profile{}}, nil } diff --git a/internal/helm/upgrade.go b/internal/helm/upgrade.go index 67e713b7..aa0f8429 100644 --- a/internal/helm/upgrade.go +++ b/internal/helm/upgrade.go @@ -53,7 +53,7 @@ const ( // var so tests substitute a fake without spawning real helm. Mirrors // nodeboot.Runner exactly. var Runner = func(ctx context.Context, name string, args ...string) (string, error) { - out, err := exec.CommandContext(ctx, name, args...).CombinedOutput() + out, err := exec.CommandContext(ctx, name, args...).CombinedOutput() // #nosec G204 -- test seam: every caller passes the literal "helm"; args are the operator's own release/kubeconfig flags, exec'd as an argv array, no shell. return string(out), err } diff --git a/internal/nodeboot/nodeboot.go b/internal/nodeboot/nodeboot.go index 82599d84..db29f968 100644 --- a/internal/nodeboot/nodeboot.go +++ b/internal/nodeboot/nodeboot.go @@ -32,7 +32,7 @@ const imageReference = "ghcr.io/tracebloc/*" // Runner executes an external command and returns its combined output. A package // var so tests can substitute a fake without spawning real k3d/helm/docker. var Runner = func(ctx context.Context, name string, args ...string) (string, error) { - out, err := exec.CommandContext(ctx, name, args...).CombinedOutput() + out, err := exec.CommandContext(ctx, name, args...).CombinedOutput() // #nosec G204 -- test seam: callers pass literal tool names (k3d/helm/docker) with argv from package consts and the operator's own cluster/release names; no shell. return string(out), err } diff --git a/internal/push/detect.go b/internal/push/detect.go index cdcf5444..b82031c0 100644 --- a/internal/push/detect.go +++ b/internal/push/detect.go @@ -27,7 +27,7 @@ import ( // default, advising --target-size. (Since Discover only yields the // ingestor's accept-set — .jpg/.jpeg/.png — that path is defensive.) func DetectImageSize(path string) (width, height int, err error) { - f, err := os.Open(path) + f, err := os.Open(path) // #nosec G304 -- decodes the header of an image the symlink-rejecting dataset walk found under the operator-chosen root; operator's own file. if err != nil { return 0, 0, err } diff --git a/internal/push/image_resolution.go b/internal/push/image_resolution.go index e33d8af8..64dcdf59 100644 --- a/internal/push/image_resolution.go +++ b/internal/push/image_resolution.go @@ -27,7 +27,7 @@ import ( func scanImageResolutions(paths []string, expectedW, expectedH, minW, minH int) (broken, tooSmall, mismatched []string) { for _, path := range paths { name := filepath.Base(path) - f, err := os.Open(path) + f, err := os.Open(path) // #nosec G304 -- paths come from the symlink-rejecting dataset walk of the operator-chosen root; mirrors the in-cluster validator on the operator's own files. if err != nil { broken = append(broken, fmt.Sprintf("%s (unreadable: %v)", name, err)) continue diff --git a/internal/push/preflight.go b/internal/push/preflight.go index 3315b275..8dc11305 100644 --- a/internal/push/preflight.go +++ b/internal/push/preflight.go @@ -42,7 +42,7 @@ var utf8BOM = []byte{0xEF, 0xBB, 0xBF} // non-EOF Read error is only ever a genuine I/O failure, so callers can treat // it as fail-closed. The caller closes the returned Closer. func openCSVReader(path string) (*csv.Reader, io.Closer, error) { - f, err := os.Open(path) + f, err := os.Open(path) // #nosec G304 -- opens the operator's own dataset CSV to mirror the ingestor's checks locally before upload; no privilege boundary crossed. if err != nil { return nil, nil, err } @@ -78,7 +78,7 @@ func matchColumnIndex(header []string, want string) int { // HasBOM reports whether the file starts with a UTF-8 BOM. func HasBOM(path string) (bool, error) { - f, err := os.Open(path) + f, err := os.Open(path) // #nosec G304 -- 3-byte BOM sniff of the same operator-supplied dataset CSV openCSVReader reads; local preflight as the invoking user. if err != nil { return false, err } @@ -802,7 +802,7 @@ func hasKnownExtension(name string) bool { // the FIRST gate validate_data runs in-cluster: the CSV must be valid UTF-8 // and free of NUL bytes, or the whole run aborts (after the upload). func CheckCSVEncoding(path string) error { - f, err := os.Open(path) + f, err := os.Open(path) // #nosec G304 -- encoding-check of the operator's own dataset CSV, size-capped by LimitReader; same local-preflight threat model as openCSVReader. if err != nil { return fmt.Errorf("reading %s: %w", filepath.Base(path), err) } diff --git a/internal/push/stream.go b/internal/push/stream.go index f26233cd..74330224 100644 --- a/internal/push/stream.go +++ b/internal/push/stream.go @@ -469,7 +469,7 @@ func writeTarFile(tw *tar.Writer, src, dst string) (int64, error) { if err := tw.WriteHeader(hdr); err != nil { return 0, err } - f, err := os.Open(src) + f, err := os.Open(src) // #nosec G304 -- src came from the symlink-rejecting walk and the Lstat guard above re-rejects symlinks at stream time; reads the operator's own dataset file. if err != nil { return 0, err } diff --git a/internal/push/tabular.go b/internal/push/tabular.go index 05d52e92..319465f1 100644 --- a/internal/push/tabular.go +++ b/internal/push/tabular.go @@ -388,7 +388,7 @@ type SchemaInference struct { // redeclares them. The risky cases (empty-in-sample, id-like) are returned // alongside the schema so the caller can surface them as warnings. func InferSchema(csvPath string) (*SchemaInference, error) { - f, err := os.Open(csvPath) + f, err := os.Open(csvPath) // #nosec G304 -- csvPath is the dataset CSV DiscoverTabular's symlink-rejecting walk found under the operator-chosen root; local read as the invoking user. if err != nil { return nil, err } diff --git a/internal/push/text.go b/internal/push/text.go index 6cb73e13..6cfb089d 100644 --- a/internal/push/text.go +++ b/internal/push/text.go @@ -162,7 +162,7 @@ func validateTextRecords(csvPath, dirName string, files []string, rf RecordForma if path == "" { continue // manifest names a file not on disk — a missing-file check's job, not ours } - content, err := os.ReadFile(path) + content, err := os.ReadFile(path) // #nosec G304 -- path comes from byBase/byStem, keyed only by files the symlink-vetted walk found in the dataset dir; a manifest entry selects among them, it cannot point elsewhere. if err != nil { return fmt.Errorf("reading %s: %w", filepath.Join(dirName, filepath.Base(path)), err) } diff --git a/internal/slug/slug.go b/internal/slug/slug.go index fb3f8a08..17091a84 100644 --- a/internal/slug/slug.go +++ b/internal/slug/slug.go @@ -87,7 +87,7 @@ func toASCII(s string) string { var b strings.Builder for _, r := range norm.NFKD.String(s) { if r < 128 { - b.WriteByte(byte(r)) + b.WriteByte(byte(r)) // #nosec G115 -- false positive: range-over-string runes are non-negative and the r < 128 guard bounds them, so byte(r) is a lossless ASCII conversion. } } return b.String()