diff --git a/internal/task/executor.go b/internal/task/executor.go index 0d8754b9d..70727cf17 100644 --- a/internal/task/executor.go +++ b/internal/task/executor.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "runtime/debug" + "slices" "sync" "time" @@ -57,14 +58,14 @@ func (p *Executor) Execute(ctx context.Context, resolver file.Resolver, s sbomsy } err := runTaskSafely(ctx, tsk, resolver, s) - unknowns, err := unknown.ExtractCoordinateErrors(err) + unknowns, remainingErrors := unknown.ExtractCoordinateErrors(err) if len(unknowns) > 0 { appendUnknowns(s, tsk.Name(), unknowns) } - if err != nil { + if remainingErrors != nil { withLock(func() { - errs = multierror.Append(errs, fmt.Errorf("failed to run task: %w", err)) - prog.SetError(err) + errs = multierror.Append(errs, fmt.Errorf("failed to run task: %w", remainingErrors)) + prog.SetError(remainingErrors) }) } prog.Increment() @@ -84,7 +85,13 @@ func appendUnknowns(builder sbomsync.Builder, taskName string, unknowns []unknow if sb.Artifacts.Unknowns == nil { sb.Artifacts.Unknowns = map[file.Coordinates][]string{} } - sb.Artifacts.Unknowns[u.Coordinates] = append(sb.Artifacts.Unknowns[u.Coordinates], formatUnknown(u.Reason.Error(), taskName)) + unknownText := formatUnknown(u.Reason.Error(), taskName) + existing := sb.Artifacts.Unknowns[u.Coordinates] + // don't include duplicate unknowns + if slices.Contains(existing, unknownText) { + continue + } + sb.Artifacts.Unknowns[u.Coordinates] = append(existing, unknownText) } }) } diff --git a/internal/unknown/path_error.go b/internal/unknown/path_error.go new file mode 100644 index 000000000..5a7a9ad59 --- /dev/null +++ b/internal/unknown/path_error.go @@ -0,0 +1,33 @@ +package unknown + +import ( + "regexp" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" +) + +var pathErrorRegex = regexp.MustCompile(`.*path="([^"]+)".*`) + +// ProcessPathErrors replaces "path" errors returned from the file.Resolver into unknowns, +// and warn logs non-unknown errors, returning only the unknown errors +func ProcessPathErrors(err error) error { + if err == nil { + return nil + } + errText := err.Error() + if pathErrorRegex.MatchString(errText) { + foundPath := pathErrorRegex.ReplaceAllString(err.Error(), "$1") + if foundPath != "" { + return New(file.NewLocation(foundPath), err) + } + } + unknowns, remainingErrors := ExtractCoordinateErrors(err) + log.Warn(remainingErrors) + + var out []error + for _, u := range unknowns { + out = append(out, &u) + } + return Join(out...) +} diff --git a/internal/unknown/path_error_test.go b/internal/unknown/path_error_test.go new file mode 100644 index 000000000..d25133a7f --- /dev/null +++ b/internal/unknown/path_error_test.go @@ -0,0 +1,56 @@ +package unknown + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" +) + +func Test_ProcessPathErrors(t *testing.T) { + tests := []struct { + errorText string + expected error + }{ + { + errorText: `prefix path="/var/lib/thing" suffix`, + expected: &CoordinateError{ + Coordinates: file.Coordinates{ + RealPath: "/var/lib/thing", + }, + Reason: fmt.Errorf(`prefix path="/var/lib/thing" suffix`), + }, + }, + { + errorText: `prefix path="/var/lib/thing"`, + expected: &CoordinateError{ + Coordinates: file.Coordinates{ + RealPath: "/var/lib/thing", + }, + Reason: fmt.Errorf(`prefix path="/var/lib/thing"`), + }, + }, + { + errorText: `path="/var/lib/thing" suffix`, + expected: &CoordinateError{ + Coordinates: file.Coordinates{ + RealPath: "/var/lib/thing", + }, + Reason: fmt.Errorf(`path="/var/lib/thing" suffix`), + }, + }, + { + errorText: "all your base are belong to us", + expected: nil, + }, + } + + for _, test := range tests { + t.Run(test.errorText, func(t *testing.T) { + got := ProcessPathErrors(fmt.Errorf("%s", test.errorText)) + require.Equal(t, test.expected, got) + }) + } +} diff --git a/syft/pkg/cataloger/binary/classifier_cataloger.go b/syft/pkg/cataloger/binary/classifier_cataloger.go index 05e1c622a..162ccefb0 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger.go @@ -105,6 +105,7 @@ func catalog(resolver file.Resolver, cls Classifier) (packages []pkg.Package, er var errs error locations, err := resolver.FilesByGlob(cls.FileGlob) if err != nil { + err = unknown.ProcessPathErrors(err) // convert any file.Resolver path errors to unknowns with locations return nil, err } for _, location := range locations { diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go index f093b97ca..d477d6e7f 100644 --- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go +++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go @@ -1668,7 +1668,7 @@ func Test_Cataloger_ResilientToErrors(t *testing.T) { resolver := &panicyResolver{} _, _, err := c.Catalog(context.Background(), resolver) - assert.Error(t, err) + assert.Nil(t, err) // non-coordinate-based FindBy* errors are now logged and not returned assert.True(t, resolver.searchCalled) } diff --git a/test/cli/test-fixtures/image-unknowns/Dockerfile b/test/cli/test-fixtures/image-unknowns/Dockerfile index 503f6808d..8b3ebf776 100644 --- a/test/cli/test-fixtures/image-unknowns/Dockerfile +++ b/test/cli/test-fixtures/image-unknowns/Dockerfile @@ -1,3 +1,5 @@ FROM alpine@sha256:c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33 RUN rm -rf /lib/apk/db/installed COPY . /home/files +# add a circular reference that will result in a failure while executing FindByGlob: +RUN mkdir -p /etc/alternatives && ln -s /etc/alternatives/java2 /etc/alternatives/java && ln -s /etc/alternatives/java /etc/alternatives/java2 diff --git a/test/cli/unknowns_test.go b/test/cli/unknowns_test.go index 00eac1d24..02e1f9ecc 100644 --- a/test/cli/unknowns_test.go +++ b/test/cli/unknowns_test.go @@ -22,6 +22,7 @@ func Test_Unknowns(t *testing.T) { assertInOutput(`no package identified in executable file`), assertInOutput(`unable to read files from java archive`), assertInOutput(`no package identified in archive`), + assertInOutput(`cycle during symlink resolution`), assertSuccessfulReturnCode, }, },