mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 23:16:30 +01:00
feat: report unknowns in sbom (#2998)
Signed-off-by: Keith Zantow <kzantow@gmail.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
co-authored by
Alex Goodman
parent
4d7ed9f749
commit
ccbee94b87
@@ -0,0 +1,201 @@
|
||||
package unknown
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
type hasCoordinates interface {
|
||||
GetCoordinates() file.Coordinates
|
||||
}
|
||||
|
||||
type CoordinateError struct {
|
||||
Coordinates file.Coordinates
|
||||
Reason error
|
||||
}
|
||||
|
||||
var _ error = (*CoordinateError)(nil)
|
||||
|
||||
func (u *CoordinateError) Error() string {
|
||||
if u.Coordinates.FileSystemID == "" {
|
||||
return fmt.Sprintf("%s: %v", u.Coordinates.RealPath, u.Reason)
|
||||
}
|
||||
return fmt.Sprintf("%s (%s): %v", u.Coordinates.RealPath, u.Coordinates.FileSystemID, u.Reason)
|
||||
}
|
||||
|
||||
// New returns a new CoordinateError unless the reason is a CoordinateError itself, in which case
|
||||
// reason will be returned directly or if reason is nil, nil will be returned
|
||||
func New(coords hasCoordinates, reason error) *CoordinateError {
|
||||
if reason == nil {
|
||||
return nil
|
||||
}
|
||||
coordinates := coords.GetCoordinates()
|
||||
reasonCoordinateError := &CoordinateError{}
|
||||
if errors.As(reason, &reasonCoordinateError) {
|
||||
// if the reason is already a coordinate error, it is potentially for a different location,
|
||||
// so we do not want to surface this location having an error
|
||||
return reasonCoordinateError
|
||||
}
|
||||
return &CoordinateError{
|
||||
Coordinates: coordinates,
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
// Newf returns a new CoordinateError with a reason of an error created from given format and args
|
||||
func Newf(coords hasCoordinates, format string, args ...any) *CoordinateError {
|
||||
return New(coords, fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// Append returns an error joined to the first error/set of errors, with a new CoordinateError appended to the end
|
||||
func Append(errs error, coords hasCoordinates, reason error) error {
|
||||
return Join(errs, New(coords, reason))
|
||||
}
|
||||
|
||||
// Appendf returns an error joined to the first error/set of errors, with a new CoordinateError appended to the end,
|
||||
// created from the given reason and args
|
||||
func Appendf(errs error, coords hasCoordinates, format string, args ...any) error {
|
||||
return Append(errs, coords, fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// Join joins the provided sets of errors together in a flattened manner, taking into account nested errors created
|
||||
// from other sources, including errors.Join, multierror.Append, and unknown.Join
|
||||
func Join(errs ...error) error {
|
||||
var out []error
|
||||
for _, err := range errs {
|
||||
// append errors, de-duplicated
|
||||
for _, e := range flatten(err) {
|
||||
if containsErr(out, e) {
|
||||
continue
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
if len(out) == 1 {
|
||||
return out[0]
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.Join(out...)
|
||||
}
|
||||
|
||||
// Joinf joins the provided sets of errors together in a flattened manner, taking into account nested errors created
|
||||
// from other sources, including errors.Join, multierror.Append, and unknown.Join and appending a new error,
|
||||
// created from the format and args provided -- the error is NOT a CoordinateError
|
||||
func Joinf(errs error, format string, args ...any) error {
|
||||
return Join(errs, fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// IfEmptyf returns a new Errorf-formatted error, only when the provided slice is empty or nil when
|
||||
// the slice has entries
|
||||
func IfEmptyf[T any](emptyTest []T, format string, args ...any) error {
|
||||
if len(emptyTest) == 0 {
|
||||
return fmt.Errorf(format, args...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractCoordinateErrors extracts all coordinate errors returned, and any _additional_ errors in the graph
|
||||
// are encapsulated in the second, error return parameter
|
||||
func ExtractCoordinateErrors(err error) (coordinateErrors []CoordinateError, remainingErrors error) {
|
||||
remainingErrors = visitErrors(err, func(e error) error {
|
||||
if coordinateError, _ := e.(*CoordinateError); coordinateError != nil {
|
||||
coordinateErrors = append(coordinateErrors, *coordinateError)
|
||||
return nil
|
||||
}
|
||||
return e
|
||||
})
|
||||
return coordinateErrors, remainingErrors
|
||||
}
|
||||
|
||||
func flatten(errs ...error) []error {
|
||||
var out []error
|
||||
for _, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
// turn all errors nested under a coordinate error to individual coordinate errors
|
||||
if e, ok := err.(*CoordinateError); ok {
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
for _, r := range flatten(e.Reason) {
|
||||
out = append(out, New(e.Coordinates, r))
|
||||
}
|
||||
} else
|
||||
// from multierror.Append
|
||||
if e, ok := err.(interface{ WrappedErrors() []error }); ok {
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, flatten(e.WrappedErrors()...)...)
|
||||
} else
|
||||
// from errors.Join
|
||||
if e, ok := err.(interface{ Unwrap() []error }); ok {
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, flatten(e.Unwrap()...)...)
|
||||
} else {
|
||||
out = append(out, err)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// containsErr returns true if a duplicate error is found
|
||||
func containsErr(out []error, err error) bool {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Tracef("error comparing errors: %v", err)
|
||||
}
|
||||
}()
|
||||
for _, e := range out {
|
||||
if e == err {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// visitErrors visits every wrapped error. the returned error replaces the provided error, null errors are omitted from
|
||||
// the object graph
|
||||
func visitErrors(err error, fn func(error) error) error {
|
||||
// unwrap errors from errors.Join
|
||||
if errs, ok := err.(interface{ Unwrap() []error }); ok {
|
||||
var out []error
|
||||
for _, e := range errs.Unwrap() {
|
||||
out = append(out, visitErrors(e, fn))
|
||||
}
|
||||
// errors.Join omits nil errors and will return nil if all passed errors are nil
|
||||
return errors.Join(out...)
|
||||
}
|
||||
// unwrap errors from multierror.Append -- these also implement Unwrap() error, so check this first
|
||||
if errs, ok := err.(interface{ WrappedErrors() []error }); ok {
|
||||
var out []error
|
||||
for _, e := range errs.WrappedErrors() {
|
||||
out = append(out, visitErrors(e, fn))
|
||||
}
|
||||
// errors.Join omits nil errors and will return nil if all passed errors are nil
|
||||
return errors.Join(out...)
|
||||
}
|
||||
// unwrap singly wrapped errors
|
||||
if e, ok := err.(interface{ Unwrap() error }); ok {
|
||||
wrapped := e.Unwrap()
|
||||
got := visitErrors(wrapped, fn)
|
||||
if got == nil {
|
||||
return nil
|
||||
}
|
||||
if wrapped.Error() != got.Error() {
|
||||
prefix := strings.TrimSuffix(err.Error(), wrapped.Error())
|
||||
return fmt.Errorf("%s%w", prefix, got)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return fn(err)
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package unknown
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func Test_visitErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in error
|
||||
transform func(error) error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "return",
|
||||
in: fmt.Errorf("err1"),
|
||||
transform: func(e error) error {
|
||||
return e
|
||||
},
|
||||
expected: "err1",
|
||||
},
|
||||
{
|
||||
name: "omit",
|
||||
in: fmt.Errorf("err1"),
|
||||
transform: func(_ error) error {
|
||||
return nil
|
||||
},
|
||||
expected: "<nil>",
|
||||
},
|
||||
{
|
||||
name: "wrapped return",
|
||||
in: fmt.Errorf("wrapped: %w", fmt.Errorf("err1")),
|
||||
transform: func(e error) error {
|
||||
return e
|
||||
},
|
||||
expected: "wrapped: err1",
|
||||
},
|
||||
{
|
||||
name: "wrapped omit",
|
||||
in: fmt.Errorf("wrapped: %w", fmt.Errorf("err1")),
|
||||
transform: func(e error) error {
|
||||
if e.Error() == "err1" {
|
||||
return nil
|
||||
}
|
||||
return e
|
||||
},
|
||||
expected: "<nil>",
|
||||
},
|
||||
{
|
||||
name: "joined return",
|
||||
in: errors.Join(fmt.Errorf("err1"), fmt.Errorf("err2")),
|
||||
transform: func(e error) error {
|
||||
return e
|
||||
},
|
||||
expected: "err1\nerr2",
|
||||
},
|
||||
{
|
||||
name: "joined omit",
|
||||
in: errors.Join(fmt.Errorf("err1"), fmt.Errorf("err2")),
|
||||
transform: func(_ error) error {
|
||||
return nil
|
||||
},
|
||||
expected: "<nil>",
|
||||
},
|
||||
{
|
||||
name: "joined omit first",
|
||||
in: errors.Join(fmt.Errorf("err1"), fmt.Errorf("err2")),
|
||||
transform: func(e error) error {
|
||||
if e.Error() == "err1" {
|
||||
return nil
|
||||
}
|
||||
return e
|
||||
},
|
||||
expected: "err2",
|
||||
},
|
||||
{
|
||||
name: "joined wrapped return",
|
||||
in: errors.Join(fmt.Errorf("wrapped: %w", fmt.Errorf("err1")), fmt.Errorf("err2")),
|
||||
transform: func(e error) error {
|
||||
return e
|
||||
},
|
||||
expected: "wrapped: err1\nerr2",
|
||||
},
|
||||
{
|
||||
name: "joined wrapped omit first",
|
||||
in: errors.Join(fmt.Errorf("wrapped: %w", fmt.Errorf("err1")), fmt.Errorf("err2")),
|
||||
transform: func(e error) error {
|
||||
if e.Error() == "err1" {
|
||||
return nil
|
||||
}
|
||||
return e
|
||||
},
|
||||
expected: "err2",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
gotErr := visitErrors(test.in, test.transform)
|
||||
got := fmt.Sprintf("%v", gotErr)
|
||||
require.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Join(t *testing.T) {
|
||||
err1 := fmt.Errorf("err1")
|
||||
err2 := fmt.Errorf("err2")
|
||||
|
||||
tests := []struct {
|
||||
name string ``
|
||||
in []error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
in: []error{fmt.Errorf("err")},
|
||||
expected: "err",
|
||||
},
|
||||
{
|
||||
name: "wrapped",
|
||||
in: []error{fmt.Errorf("outer: %w", fmt.Errorf("err"))},
|
||||
expected: "outer: err",
|
||||
},
|
||||
{
|
||||
name: "wrapped joined",
|
||||
in: []error{errors.Join(fmt.Errorf("outer: %w", fmt.Errorf("err1")), fmt.Errorf("err2"))},
|
||||
expected: "outer: err1\nerr2",
|
||||
},
|
||||
{
|
||||
name: "duplicates",
|
||||
in: []error{err1, err1, err2},
|
||||
expected: "err1\nerr2",
|
||||
},
|
||||
{
|
||||
name: "nested duplicates",
|
||||
in: []error{errors.Join(err1, err2), err1, err2},
|
||||
expected: "err1\nerr2",
|
||||
},
|
||||
{
|
||||
name: "nested duplicates coords",
|
||||
in: []error{New(file.NewLocation("l1"), errors.Join(fmt.Errorf("err1"), fmt.Errorf("err2"))), fmt.Errorf("err1"), fmt.Errorf("err2")},
|
||||
expected: "l1: err1\nl1: err2\nerr1\nerr2",
|
||||
},
|
||||
{
|
||||
name: "all nil",
|
||||
in: []error{nil, nil, nil},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := Join(test.in...)
|
||||
if test.expected == "" {
|
||||
require.Nil(t, got)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, test.expected, got.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_flatten(t *testing.T) {
|
||||
coords := file.Coordinates{
|
||||
RealPath: "real/path",
|
||||
}
|
||||
e1 := fmt.Errorf("e1")
|
||||
e2 := fmt.Errorf("e2")
|
||||
c1 := New(coords, fmt.Errorf("c1"))
|
||||
c2 := New(coords, fmt.Errorf("c2"))
|
||||
tests := []struct {
|
||||
name string ``
|
||||
in error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
in: errors.Join(e1, e2),
|
||||
expected: "e1//e2",
|
||||
},
|
||||
{
|
||||
name: "coords",
|
||||
in: New(coords, e1),
|
||||
expected: "real/path: e1",
|
||||
},
|
||||
{
|
||||
name: "coords with joined children",
|
||||
in: New(coords, errors.Join(e1, e2)),
|
||||
expected: "real/path: e1//real/path: e2",
|
||||
},
|
||||
{
|
||||
name: "very nested",
|
||||
in: errors.Join(errors.Join(errors.Join(errors.Join(e1, c1), e2), c2), e2),
|
||||
expected: "e1//real/path: c1//e2//real/path: c2//e2",
|
||||
},
|
||||
}
|
||||
toString := func(errs ...error) string {
|
||||
var parts []string
|
||||
for _, e := range errs {
|
||||
parts = append(parts, e.Error())
|
||||
}
|
||||
return strings.Join(parts, "//")
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := flatten(test.in)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, test.expected, toString(got...))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Append(t *testing.T) {
|
||||
e1 := New(file.NewLocation("l1"), fmt.Errorf("e1"))
|
||||
e2 := Append(e1, file.NewLocation("l2"), fmt.Errorf("e2"))
|
||||
e3 := Appendf(e2, file.NewLocation("l3"), "%s", "e3")
|
||||
require.Equal(t, "l1: e1\nl2: e2\nl3: e3", e3.Error())
|
||||
|
||||
e1 = New(file.NewLocation("l1"), nil)
|
||||
require.Nil(t, e1)
|
||||
e2 = Append(e1, file.NewLocation("l2"), fmt.Errorf("e2"))
|
||||
e3 = Appendf(e2, file.NewLocation("l3"), "%s", "e3")
|
||||
require.Equal(t, "l2: e2\nl3: e3", e3.Error())
|
||||
|
||||
e1 = New(file.NewLocation("l1"), fmt.Errorf("e1"))
|
||||
e2 = Append(e1, file.NewLocation("l2"), nil)
|
||||
e3 = Appendf(e2, file.NewLocation("l3"), "%s", "e3")
|
||||
require.Equal(t, "l1: e1\nl3: e3", e3.Error())
|
||||
}
|
||||
Reference in New Issue
Block a user