Skip to content

Commit 96a8f2e

Browse files
authored
fix(golang): warn instead of error on transitive co-update requirements (#60)
* fix(golang): warn instead of error on transitive co-update requirements * fix(lint): remove unused nolint directives and convert checkMissingTransitiveDeps to void * fix(security): resolve gosec G703/G704/prealloc lint findings * fix(security): construct proxy URL from struct fields to eliminate G704 taint path * fix(security): parse proxy path through url.Parse to break G704 taint chain
1 parent 2d6ae7c commit 96a8f2e

6 files changed

Lines changed: 43 additions & 37 deletions

File tree

.golangci.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ linters:
1515
- dupl
1616
- gosec
1717
- goconst
18+
# G704 taint analysis false positive: fetchFromProxy constructs the URL
19+
# with a hardcoded scheme and host (proxyHost constant); only the path
20+
# component is dynamic (module-escaped package path and version).
21+
- path: 'pkg/languages/golang/indirect_resolver\.go'
22+
linters:
23+
- gosec
24+
text: 'G704'
1825
enable:
1926
# Error handling
2027
- errcheck # Check for unchecked errors

pkg/languages/golang/golang.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ var (
3030

3131
// ErrUnexpectedGoListOutput is returned when go list output has unexpected format.
3232
ErrUnexpectedGoListOutput = errors.New("unexpected go list output")
33-
34-
// ErrTransitiveDepsRequired is returned when updating a package requires co-updating other dependencies.
35-
ErrTransitiveDepsRequired = errors.New("transitive dependencies need co-updating")
3633
)
3734

3835
// Golang implements the Language interface for Go projects.
@@ -408,17 +405,15 @@ func resolveAndFilterPackages(ctx context.Context, packages map[string]*Package,
408405
log.Infof("Will update %s from %s to %s", name, currentVersion, resolvedVersion)
409406
}
410407

411-
if err := checkMissingTransitiveDeps(ctx, filtered, modFile); err != nil {
412-
return nil, err
413-
}
408+
checkMissingTransitiveDeps(ctx, filtered, modFile)
414409

415410
return filtered, nil
416411
}
417412

418413
// checkMissingTransitiveDeps checks all packages being updated for transitive dependency
419-
// requirements not satisfied by the current go.mod, and returns an error with co-update
414+
// requirements not satisfied by the current go.mod, and logs a warning with co-update
420415
// recommendations if any are found.
421-
func checkMissingTransitiveDeps(ctx context.Context, filtered map[string]*Package, modFile *modfile.File) error {
416+
func checkMissingTransitiveDeps(ctx context.Context, filtered map[string]*Package, modFile *modfile.File) {
422417
log := clog.FromContext(ctx)
423418

424419
// Build set of packages being updated
@@ -459,7 +454,7 @@ func checkMissingTransitiveDeps(ctx context.Context, filtered map[string]*Packag
459454
}
460455

461456
if len(allMissingDeps) == 0 && len(apiCompatibilityAlerts) == 0 {
462-
return nil
457+
return
463458
}
464459

465460
var msg strings.Builder
@@ -514,17 +509,12 @@ func checkMissingTransitiveDeps(ctx context.Context, filtered map[string]*Packag
514509
fmt.Fprintf(&msg, "\n")
515510
}
516511

517-
// Only error if there are required co-updates; API alerts are informational
518512
if len(allMissingDeps) > 0 {
519513
fmt.Fprintf(&msg, "SUGGESTED UPDATE COMMAND\n\n")
520514
fmt.Fprintf(&msg, "%s", buildSuggestedCommand(filtered, allMissingDeps, apiCompatibilityAlerts, modFile))
521515
fmt.Fprintf(&msg, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
522-
return fmt.Errorf("%w:%s", ErrTransitiveDepsRequired, msg.String())
523516
}
524-
525-
// If only API alerts (no required co-updates), just log as warning
526-
log.Warnf("API compatibility alerts:\n%s", msg.String())
527-
return nil
517+
log.Warnf("%s", msg.String())
528518
}
529519

530520
// buildSuggestedCommand builds the omnibump --packages "..." command string.

pkg/languages/golang/golang_test.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,21 +1525,9 @@ require (
15251525
},
15261526
}
15271527

1528-
// This should fail with missing transitive dependencies (unless the package doesn't have any)
1528+
// Transitive co-update issues are now warnings, not errors.
15291529
err := g.Update(context.Background(), cfg)
1530-
// The error message format is what we're testing
1531-
// It should contain the formatted sections if there are missing deps
1532-
if err != nil {
1533-
errMsg := err.Error()
1534-
// If the error is about transitive dependencies, verify the format
1535-
if strings.Contains(errMsg, "transitive dependencies need co-updating") {
1536-
// Check for formatted sections in the error message
1537-
require.Contains(t, errMsg, "REQUIRED CO-UPDATES")
1538-
require.Contains(t, errMsg, "────────────────────")
1539-
require.Contains(t, errMsg, "SUGGESTED UPDATE COMMAND")
1540-
require.Contains(t, errMsg, "━━━━━━━━━━━━━━━━━━━━━━")
1541-
}
1542-
}
1530+
require.NoError(t, err)
15431531
}
15441532

15451533
func TestGolang_Update_WithDuplicatePackagesDeduplicates(t *testing.T) {

pkg/languages/golang/indirect_resolver.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"io"
1212
"net/http"
13+
"net/url"
1314
"os"
1415
"os/exec"
1516
"path/filepath"
@@ -360,7 +361,10 @@ func checkModFileForIndirectDep(
360361
}
361362

362363
// goProxyBase is the base URL for the Go module proxy.
363-
const goProxyBase = "https://proxy.golang.org"
364+
const (
365+
goProxyBase = "https://proxy.golang.org"
366+
proxyHost = "proxy.golang.org"
367+
)
364368

365369
// proxyClient is used for all Go module proxy requests with a reasonable timeout.
366370
var proxyClient = &http.Client{Timeout: 30 * time.Second}
@@ -432,12 +436,23 @@ func (c goModCache) has(pkg, ver string) bool {
432436
// fetchFromProxy performs an HTTP GET request to the Go module proxy and returns the response body.
433437
// path must begin with "/" and is appended to goProxyBase.
434438
func fetchFromProxy(ctx context.Context, path string) ([]byte, error) {
435-
req, err := http.NewRequestWithContext(ctx, "GET", goProxyBase+path, nil)
439+
// Parse the path to validate it before use; only .Path is taken so the
440+
// host component of the final request always comes from the proxyHost constant.
441+
parsedPath, err := url.Parse(path)
442+
if err != nil {
443+
return nil, fmt.Errorf("invalid proxy path: %w", err)
444+
}
445+
u := &url.URL{
446+
Scheme: "https",
447+
Host: proxyHost,
448+
Path: parsedPath.Path,
449+
}
450+
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
436451
if err != nil {
437452
return nil, err
438453
}
439454

440-
resp, err := proxyClient.Do(req) //nolint:gosec // G704: URL is always goProxyBase + an escaped module path/version
455+
resp, err := proxyClient.Do(req)
441456
if err != nil {
442457
return nil, err
443458
}

pkg/languages/java/java.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (j *Java) Detect(ctx context.Context, dir string) (bool, error) {
5757
// GetManifestFiles returns Java manifest files from the detected build tool.
5858
func (j *Java) GetManifestFiles() []string {
5959
// Return all possible manifest files from all build tools
60-
var files []string //nolint: prealloc
60+
files := make([]string, 0, len(registeredBuildTools))
6161
for _, tool := range registeredBuildTools {
6262
files = append(files, tool.GetManifestFiles()...)
6363
}

pkg/languages/java/maven/updater.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,11 @@ func ParsePom(pomPath string) (*gopom.Project, error) {
227227
// parsePatchesFromFile reads and parses patches from a YAML file.
228228
func parsePatchesFromFile(ctx context.Context, patchFile string) ([]Patch, error) {
229229
var patchList PatchList
230-
// filepath.Clean sanitizes the path to prevent traversal attacks
231-
file, err := os.Open(filepath.Clean(patchFile)) //nolint:gosec // G703: filepath.Clean() sanitizes user input
230+
absPath, err := filepath.Abs(patchFile)
231+
if err != nil {
232+
return nil, fmt.Errorf("failed to resolve patch file path: %w", err)
233+
}
234+
file, err := os.Open(absPath) //nolint:gosec // G304: path is a user-supplied CLI flag resolved to absolute
232235
if err != nil {
233236
return nil, fmt.Errorf("failed reading file: %w", err)
234237
}
@@ -294,8 +297,11 @@ func parsePatches(ctx context.Context, patchFile, patchFlag string) ([]Patch, er
294297
// parsePropertiesFromFile reads and parses properties from a YAML file.
295298
func parsePropertiesFromFile(ctx context.Context, propertyFile string) (map[string]string, error) {
296299
var propertyList PropertyList
297-
// filepath.Clean sanitizes the path to prevent traversal attacks
298-
file, err := os.Open(filepath.Clean(propertyFile)) //nolint:gosec // G703: filepath.Clean() sanitizes user input
300+
absPath, err := filepath.Abs(propertyFile)
301+
if err != nil {
302+
return nil, fmt.Errorf("failed to resolve properties file path: %w", err)
303+
}
304+
file, err := os.Open(absPath) //nolint:gosec // G304: path is a user-supplied CLI flag resolved to absolute
299305
if err != nil {
300306
return nil, fmt.Errorf("failed reading file: %w", err)
301307
}

0 commit comments

Comments
 (0)