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:
Keith Zantow
2024-10-07 16:11:37 -04:00
committed by GitHub
co-authored by Alex Goodman
parent 4d7ed9f749
commit ccbee94b87
145 changed files with 4420 additions and 233 deletions
+1 -1
View File
@@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.17"
JSONSchemaVersion = "16.0.18"
)
+3 -3
View File
@@ -1,7 +1,6 @@
package file
import (
"fmt"
"os"
"sort"
"strings"
@@ -19,12 +18,13 @@ func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
zipReader, err := OpenZip(archivePath)
manifest := make(ZipFileManifest)
if err != nil {
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
log.Debugf("unable to open zip archive (%s): %v", archivePath, err)
return manifest, err
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Warnf("unable to close zip archive (%s): %+v", archivePath, err)
log.Debugf("unable to close zip archive (%s): %+v", archivePath, err)
}
}()
+6 -2
View File
@@ -8,6 +8,8 @@ import (
"io"
"math"
"os"
"github.com/anchore/syft/internal/log"
)
// directoryEndLen, readByf, directoryEnd, and findSignatureInBlock were copied from the golang stdlib, specifically:
@@ -46,7 +48,8 @@ func OpenZip(filepath string) (*ZipReadCloser, error) {
// need to find the start of the archive and keep track of this offset.
offset, err := findArchiveStartOffset(f, fi.Size())
if err != nil {
return nil, fmt.Errorf("cannot find beginning of zip archive=%q : %w", filepath, err)
log.Debugf("cannot find beginning of zip archive=%q : %v", filepath, err)
return nil, err
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
@@ -62,7 +65,8 @@ func OpenZip(filepath string) (*ZipReadCloser, error) {
r, err := zip.NewReader(io.NewSectionReader(f, offset64, size), size)
if err != nil {
return nil, fmt.Errorf("unable to open ZipReadCloser @ %q: %w", filepath, err)
log.Debugf("unable to open ZipReadCloser @ %q: %v", filepath, err)
return nil, err
}
return &ZipReadCloser{
+31 -3
View File
@@ -11,8 +11,10 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
)
type Executor struct {
@@ -35,6 +37,12 @@ func NewTaskExecutor(tasks []Task, numWorkers int) *Executor {
}
func (p *Executor) Execute(ctx context.Context, resolver file.Resolver, s sbomsync.Builder, prog *monitor.CatalogerTaskProgress) error {
var lock sync.Mutex
withLock := func(fn func()) {
lock.Lock()
defer lock.Unlock()
fn()
}
var errs error
wg := &sync.WaitGroup{}
for i := 0; i < p.numWorkers; i++ {
@@ -48,9 +56,16 @@ func (p *Executor) Execute(ctx context.Context, resolver file.Resolver, s sbomsy
return
}
if err := runTaskSafely(ctx, tsk, resolver, s); err != nil {
errs = multierror.Append(errs, fmt.Errorf("failed to run task: %w", err))
prog.SetError(err)
err := runTaskSafely(ctx, tsk, resolver, s)
unknowns, err := unknown.ExtractCoordinateErrors(err)
if len(unknowns) > 0 {
appendUnknowns(s, tsk.Name(), unknowns)
}
if err != nil {
withLock(func() {
errs = multierror.Append(errs, fmt.Errorf("failed to run task: %w", err))
prog.SetError(err)
})
}
prog.Increment()
}
@@ -62,6 +77,19 @@ func (p *Executor) Execute(ctx context.Context, resolver file.Resolver, s sbomsy
return errs
}
func appendUnknowns(builder sbomsync.Builder, taskName string, unknowns []unknown.CoordinateError) {
if accessor, ok := builder.(sbomsync.Accessor); ok {
accessor.WriteToSBOM(func(sb *sbom.SBOM) {
for _, u := range unknowns {
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))
}
})
}
}
func runTaskSafely(ctx context.Context, t Task, resolver file.Resolver, s sbomsync.Builder) (err error) {
// handle individual cataloger panics
defer func() {
+4 -17
View File
@@ -3,7 +3,6 @@ package task
import (
"context"
"crypto"
"fmt"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/artifact"
@@ -32,15 +31,12 @@ func NewFileDigestCatalogerTask(selection file.Selection, hashers ...crypto.Hash
}
result, err := digestsCataloger.Catalog(ctx, resolver, coordinates...)
if err != nil {
return fmt.Errorf("unable to catalog file digests: %w", err)
}
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
sbom.Artifacts.FileDigests = result
})
return nil
return err
}
return NewTask("file-digest-cataloger", fn)
@@ -62,15 +58,12 @@ func NewFileMetadataCatalogerTask(selection file.Selection) Task {
}
result, err := metadataCataloger.Catalog(ctx, resolver, coordinates...)
if err != nil {
return err
}
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
sbom.Artifacts.FileMetadata = result
})
return nil
return err
}
return NewTask("file-metadata-cataloger", fn)
@@ -87,15 +80,12 @@ func NewFileContentCatalogerTask(cfg filecontent.Config) Task {
accessor := builder.(sbomsync.Accessor)
result, err := cat.Catalog(ctx, resolver)
if err != nil {
return err
}
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
sbom.Artifacts.FileContents = result
})
return nil
return err
}
return NewTask("file-content-cataloger", fn)
@@ -112,15 +102,12 @@ func NewExecutableCatalogerTask(selection file.Selection, cfg executable.Config)
accessor := builder.(sbomsync.Accessor)
result, err := cat.Catalog(resolver)
if err != nil {
return err
}
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
sbom.Artifacts.Executables = result
})
return nil
return err
}
return NewTask("file-executable-cataloger", fn)
+1 -4
View File
@@ -103,9 +103,6 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string
t := bus.StartCatalogerTask(info, -1, "")
pkgs, relationships, err := c.Catalog(ctx, resolver)
if err != nil {
return fmt.Errorf("unable to catalog packages with %q: %w", catalogerName, err)
}
log.WithFields("cataloger", catalogerName).Debugf("discovered %d packages", len(pkgs))
@@ -120,7 +117,7 @@ func NewPackageTask(cfg CatalogingFactoryConfig, c pkg.Cataloger, tags ...string
t.SetCompleted()
log.WithFields("name", catalogerName).Trace("package cataloger completed")
return nil
return err
}
tags = append(tags, pkgcataloging.PackageTag)
+114
View File
@@ -0,0 +1,114 @@
package task
import (
"context"
"strings"
"github.com/mholt/archiver/v3"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
const unknownsLabelerTaskName = "unknowns-labeler"
func NewUnknownsLabelerTask(cfg cataloging.UnknownsConfig) Task {
return NewTask(unknownsLabelerTaskName, unknownsLabelerTask{cfg}.processUnknowns)
}
type unknownsLabelerTask struct {
cataloging.UnknownsConfig
}
// processUnknowns removes unknown entries that have valid packages reported for the locations
func (c unknownsLabelerTask) processUnknowns(_ context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
accessor := builder.(sbomsync.Accessor)
accessor.WriteToSBOM(func(s *sbom.SBOM) {
c.finalize(resolver, s)
})
return nil
}
func (c unknownsLabelerTask) finalize(resolver file.Resolver, s *sbom.SBOM) {
hasPackageReference := coordinateReferenceLookup(resolver, s)
if c.RemoveWhenPackagesDefined {
for coords := range s.Artifacts.Unknowns {
if !hasPackageReference(coords) {
continue
}
delete(s.Artifacts.Unknowns, coords)
}
}
if s.Artifacts.Unknowns == nil {
s.Artifacts.Unknowns = map[file.Coordinates][]string{}
}
if c.IncludeExecutablesWithoutPackages {
for coords := range s.Artifacts.Executables {
if !hasPackageReference(coords) {
s.Artifacts.Unknowns[coords] = append(s.Artifacts.Unknowns[coords], formatUnknown("no package identified in executable file", unknownsLabelerTaskName))
}
}
}
if c.IncludeUnexpandedArchives {
for coords := range s.Artifacts.FileMetadata {
unarchiver, notArchiveErr := archiver.ByExtension(coords.RealPath)
if unarchiver != nil && notArchiveErr == nil && !hasPackageReference(coords) {
s.Artifacts.Unknowns[coords] = append(s.Artifacts.Unknowns[coords], "archive not cataloged")
}
}
}
}
func formatUnknown(err string, task ...string) string {
return strings.Join(task, "/") + ": " + err
}
func coordinateReferenceLookup(resolver file.Resolver, s *sbom.SBOM) func(coords file.Coordinates) bool {
allPackageCoords := file.NewCoordinateSet()
// include all directly included locations that result in packages
for p := range s.Artifacts.Packages.Enumerate() {
allPackageCoords.Add(p.Locations.CoordinateSet().ToSlice()...)
}
// include owned files, for example specified by package managers.
// relationships for these owned files may be disabled, but we always want to include them
for p := range s.Artifacts.Packages.Enumerate() {
if f, ok := p.Metadata.(pkg.FileOwner); ok {
for _, ownedFilePath := range f.OwnedFiles() {
// resolve these owned files, as they may have symlinks
// but coordinates we will test against are always absolute paths
locations, err := resolver.FilesByPath(ownedFilePath)
if err != nil {
log.Debugf("unable to resolve owned file '%s': %v", ownedFilePath, err)
}
for _, loc := range locations {
allPackageCoords.Add(loc.Coordinates)
}
}
}
}
// include relationships
for _, r := range s.Relationships {
_, fromPkgOk := r.From.(pkg.Package)
fromFile, fromFileOk := r.From.(file.Coordinates)
_, toPkgOk := r.To.(pkg.Package)
toFile, toFileOk := r.To.(file.Coordinates)
if fromPkgOk && toFileOk {
allPackageCoords.Add(toFile)
} else if fromFileOk && toPkgOk {
allPackageCoords.Add(fromFile)
}
}
return allPackageCoords.Contains
}
+201
View File
@@ -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)
}
+236
View File
@@ -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())
}