Projects STRLCPY syft Commits 1002a322
🤬
  • ■ ■ ■ ■ ■
    .gitignore
     1 +/.bin
    1 2  CHANGELOG.md
    2 3  VERSION
    3 4  /test/results
    skipped 42 lines
  • ■ ■ ■ ■ ■ ■
    cmd/syft/cli/attest/attest.go
    skipped 23 lines
    24 24   "github.com/anchore/syft/syft/formats/syftjson"
    25 25   "github.com/anchore/syft/syft/formats/table"
    26 26   "github.com/anchore/syft/syft/sbom"
    27  - "github.com/anchore/syft/syft/source"
     27 + "github.com/anchore/syft/syft/source/scheme"
    28 28  )
    29 29   
    30 30  func Run(_ context.Context, app *config.Application, args []string) error {
    skipped 16 lines
    47 47   // could be an image or a directory, with or without a scheme
    48 48   // TODO: validate that source is image
    49 49   userInput := args[0]
    50  - si, err := source.ParseInputWithName(userInput, app.Platform, app.Name, app.DefaultImagePullSource)
     50 + si, err := scheme.Parse(userInput, app.Platform, app.Name, app.DefaultImagePullSource)
    51 51   if err != nil {
    52 52   return fmt.Errorf("could not generate source input for packages command: %w", err)
    53 53   }
    54 54   
    55  - if si.Scheme != source.ImageScheme {
     55 + if si.Scheme != scheme.ContainerImageScheme {
    56 56   return fmt.Errorf("attestations are only supported for oci images at this time")
    57 57   }
    58 58   
    skipped 11 lines
    70 70   )
    71 71  }
    72 72   
    73  -func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, errs chan error) ([]byte, error) {
    74  - src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
    75  - if cleanup != nil {
    76  - defer cleanup()
     73 +func buildSBOM(app *config.Application, si scheme.Input, writer sbom.Writer, errs chan error) ([]byte, error) {
     74 + src, err := scheme.NewSource(si, app.Registry.ToOptions(), app.Exclusions)
     75 + if src != nil {
     76 + defer src.Close()
    77 77   }
    78 78   if err != nil {
    79 79   return nil, fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
    skipped 18 lines
    98 98  }
    99 99   
    100 100  //nolint:funlen
    101  -func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
     101 +func execWorker(app *config.Application, si scheme.Input, writer sbom.Writer) <-chan error {
    102 102   errs := make(chan error)
    103 103   go func() {
    104 104   defer close(errs)
    skipped 141 lines
  • ■ ■ ■ ■ ■ ■
    cmd/syft/cli/eventloop/tasks.go
    skipped 15 lines
    16 16   "github.com/anchore/syft/syft/source"
    17 17  )
    18 18   
    19  -type Task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
     19 +type Task func(*sbom.Artifacts, source.Source) ([]artifact.Relationship, error)
    20 20   
    21 21  func Tasks(app *config.Application) ([]Task, error) {
    22 22   var tasks []Task
    skipped 25 lines
    48 48   return nil, nil
    49 49   }
    50 50   
    51  - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
     51 + task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
    52 52   packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.ToCatalogerConfig())
    53 53   
    54 54   results.Packages = packageCatalog
    skipped 12 lines
    67 67   
    68 68   metadataCataloger := filemetadata.NewCataloger()
    69 69   
    70  - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
     70 + task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
    71 71   resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
    72 72   if err != nil {
    73 73   return nil, err
    skipped 36 lines
    110 110   
    111 111   digestsCataloger := filedigest.NewCataloger(hashes)
    112 112   
    113  - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
     113 + task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
    114 114   resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
    115 115   if err != nil {
    116 116   return nil, err
    skipped 25 lines
    142 142   return nil, err
    143 143   }
    144 144   
    145  - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
     145 + task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
    146 146   resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt)
    147 147   if err != nil {
    148 148   return nil, err
    skipped 20 lines
    169 169   return nil, err
    170 170   }
    171 171   
    172  - task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
     172 + task := func(results *sbom.Artifacts, src source.Source) ([]artifact.Relationship, error) {
    173 173   resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt)
    174 174   if err != nil {
    175 175   return nil, err
    skipped 10 lines
    186 186   return task, nil
    187 187  }
    188 188   
    189  -func RunTask(t Task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
     189 +func RunTask(t Task, a *sbom.Artifacts, src source.Source, c chan<- artifact.Relationship, errs chan<- error) {
    190 190   defer close(c)
    191 191   
    192 192   relationships, err := t(a, src)
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    cmd/syft/cli/packages/packages.go
    skipped 20 lines
    21 21   "github.com/anchore/syft/syft/formats/template"
    22 22   "github.com/anchore/syft/syft/sbom"
    23 23   "github.com/anchore/syft/syft/source"
     24 + "github.com/anchore/syft/syft/source/scheme"
    24 25  )
    25 26   
    26 27  func Run(_ context.Context, app *config.Application, args []string) error {
    skipped 15 lines
    42 43   
    43 44   // could be an image or a directory, with or without a scheme
    44 45   userInput := args[0]
    45  - si, err := source.ParseInputWithName(userInput, app.Platform, app.Name, app.DefaultImagePullSource)
     46 + si, err := scheme.Parse(userInput, app.Platform, app.Name, app.DefaultImagePullSource)
    46 47   if err != nil {
    47 48   return fmt.Errorf("could not generate source input for packages command: %w", err)
    48 49   }
    skipped 12 lines
    61 62   )
    62 63  }
    63 64   
    64  -func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
     65 +func execWorker(app *config.Application, si scheme.Input, writer sbom.Writer) <-chan error {
    65 66   errs := make(chan error)
    66 67   go func() {
    67 68   defer close(errs)
    68 69   
    69  - src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
    70  - if cleanup != nil {
    71  - defer cleanup()
     70 + src, err := scheme.NewSource(si, app.Registry.ToOptions(), app.Exclusions)
     71 + if src != nil {
     72 + defer src.Close()
    72 73   }
    73 74   if err != nil {
    74 75   errs <- fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
    skipped 18 lines
    93 94   return errs
    94 95  }
    95 96   
    96  -func GenerateSBOM(src *source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
     97 +func GenerateSBOM(src source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
    97 98   tasks, err := eventloop.Tasks(app)
    98 99   if err != nil {
    99 100   return nil, err
    100 101   }
    101 102   
    102 103   s := sbom.SBOM{
    103  - Source: src.Metadata,
     104 + Source: src.Describe(),
    104 105   Descriptor: sbom.Descriptor{
    105 106   Name: internal.ApplicationName,
    106 107   Version: version.FromBuild().Version,
    skipped 6 lines
    113 114   return &s, nil
    114 115  }
    115 116   
    116  -func buildRelationships(s *sbom.SBOM, src *source.Source, tasks []eventloop.Task, errs chan error) {
     117 +func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task, errs chan error) {
    117 118   var relationships []<-chan artifact.Relationship
    118 119   for _, task := range tasks {
    119 120   c := make(chan artifact.Relationship)
    skipped 33 lines
  • ■ ■ ■ ■ ■ ■
    cmd/syft/cli/poweruser/poweruser.go
    skipped 22 lines
    23 23   "github.com/anchore/syft/syft/event"
    24 24   "github.com/anchore/syft/syft/formats/syftjson"
    25 25   "github.com/anchore/syft/syft/sbom"
    26  - "github.com/anchore/syft/syft/source"
     26 + "github.com/anchore/syft/syft/source/scheme"
    27 27  )
    28 28   
    29 29  func Run(_ context.Context, app *config.Application, args []string) error {
    skipped 17 lines
    47 47   }()
    48 48   
    49 49   userInput := args[0]
    50  - si, err := source.ParseInputWithName(userInput, app.Platform, app.Name, app.DefaultImagePullSource)
     50 + si, err := scheme.Parse(userInput, app.Platform, app.Name, app.DefaultImagePullSource)
    51 51   if err != nil {
    52 52   return fmt.Errorf("could not generate source input for packages command: %w", err)
    53 53   }
    skipped 12 lines
    66 66   )
    67 67  }
    68 68   
    69  -func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
     69 +func execWorker(app *config.Application, si scheme.Input, writer sbom.Writer) <-chan error {
    70 70   errs := make(chan error)
    71 71   go func() {
    72 72   defer close(errs)
    skipped 8 lines
    81 81   return
    82 82   }
    83 83   
    84  - src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
     84 + src, err := scheme.NewSource(si, app.Registry.ToOptions(), app.Exclusions)
     85 + if src != nil {
     86 + defer src.Close()
     87 + }
    85 88   if err != nil {
    86 89   errs <- err
    87 90   return
    88 91   }
    89  - if cleanup != nil {
    90  - defer cleanup()
    91  - }
    92 92   
    93 93   s := sbom.SBOM{
    94  - Source: src.Metadata,
     94 + Source: src.Describe(),
    95 95   Descriptor: sbom.Descriptor{
    96 96   Name: internal.ApplicationName,
    97 97   Version: version.FromBuild().Version,
    skipped 23 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/cyclonedxhelpers/decoder.go
    skipped 228 lines
    229 229   }
    230 230  }
    231 231   
    232  -func extractComponents(meta *cyclonedx.Metadata) source.Metadata {
     232 +func extractComponents(meta *cyclonedx.Metadata) source.Description {
    233 233   if meta == nil || meta.Component == nil {
    234  - return source.Metadata{}
     234 + return source.Description{}
    235 235   }
    236 236   c := meta.Component
    237 237   
    238  - image := source.ImageMetadata{
    239  - UserInput: c.Name,
    240  - ID: c.BOMRef,
    241  - ManifestDigest: c.Version,
    242  - }
    243  - 
    244 238   switch c.Type {
    245 239   case cyclonedx.ComponentTypeContainer:
    246  - return source.Metadata{
    247  - Scheme: source.ImageScheme,
    248  - ImageMetadata: image,
     240 + return source.Description{
     241 + ID: "",
     242 + Name: c.Name,
     243 + Metadata: source.ImageMetadata{
     244 + UserInput: c.Name,
     245 + ID: c.BOMRef,
     246 + ManifestDigest: c.Version,
     247 + },
    249 248   }
    250 249   case cyclonedx.ComponentTypeFile:
    251  - return source.Metadata{
    252  - Scheme: source.FileScheme, // or source.DirectoryScheme
    253  - Path: c.Name,
    254  - ImageMetadata: image,
     250 + // TODO: this is lossy... we can't know if this is a file or a directory
     251 + return source.Description{
     252 + ID: "",
     253 + Name: c.Name,
     254 + Metadata: source.FileMetadata{Path: c.Name},
    255 255   }
    256 256   }
    257  - return source.Metadata{}
     257 + return source.Description{}
    258 258  }
    259 259   
    260 260  // if there is more than one tool in meta.Tools' list the last item will be used
    skipped 14 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/cyclonedxhelpers/format.go
    skipped 109 lines
    110 110  }
    111 111   
    112 112  // NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
    113  -func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclonedx.Metadata {
     113 +func toBomDescriptor(name, version string, srcMetadata source.Description) *cyclonedx.Metadata {
    114 114   return &cyclonedx.Metadata{
    115 115   Timestamp: time.Now().Format(time.RFC3339),
    116 116   Tools: &[]cyclonedx.Tool{
    skipped 53 lines
    170 170   return result
    171 171  }
    172 172   
    173  -func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component {
     173 +func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Component {
    174 174   name := srcMetadata.Name
    175  - switch srcMetadata.Scheme {
    176  - case source.ImageScheme:
     175 + switch metadata := srcMetadata.Metadata.(type) {
     176 + case source.ImageMetadata:
    177 177   if name == "" {
    178  - name = srcMetadata.ImageMetadata.UserInput
     178 + name = metadata.UserInput
    179 179   }
    180  - bomRef, err := artifact.IDByHash(srcMetadata.ImageMetadata.ID)
     180 + bomRef, err := artifact.IDByHash(metadata.ID)
    181 181   if err != nil {
    182  - log.Warnf("unable to get fingerprint of image metadata=%s: %+v", srcMetadata.ImageMetadata.ID, err)
     182 + log.Warnf("unable to get fingerprint of source image metadata=%s: %+v", metadata.ID, err)
    183 183   }
    184 184   return &cyclonedx.Component{
    185 185   BOMRef: string(bomRef),
    186 186   Type: cyclonedx.ComponentTypeContainer,
    187 187   Name: name,
    188  - Version: srcMetadata.ImageMetadata.ManifestDigest,
     188 + Version: metadata.ManifestDigest,
     189 + }
     190 + case source.DirectoryMetadata:
     191 + if name == "" {
     192 + name = metadata.Path
     193 + }
     194 + bomRef, err := artifact.IDByHash(metadata.Path)
     195 + if err != nil {
     196 + log.Warnf("unable to get fingerprint of source directory metadata path=%s: %+v", metadata.Path, err)
    189 197   }
    190  - case source.DirectoryScheme, source.FileScheme:
     198 + return &cyclonedx.Component{
     199 + BOMRef: string(bomRef),
     200 + // TODO: this is lossy... we can't know if this is a file or a directory
     201 + Type: cyclonedx.ComponentTypeFile,
     202 + Name: name,
     203 + }
     204 + case source.FileMetadata:
    191 205   if name == "" {
    192  - name = srcMetadata.Path
     206 + name = metadata.Path
    193 207   }
    194  - bomRef, err := artifact.IDByHash(srcMetadata.Path)
     208 + bomRef, err := artifact.IDByHash(metadata.Path)
    195 209   if err != nil {
    196  - log.Warnf("unable to get fingerprint of source metadata path=%s: %+v", srcMetadata.Path, err)
     210 + log.Warnf("unable to get fingerprint of source file metadata path=%s: %+v", metadata.Path, err)
    197 211   }
    198 212   return &cyclonedx.Component{
    199 213   BOMRef: string(bomRef),
    200  - Type: cyclonedx.ComponentTypeFile,
    201  - Name: name,
     214 + // TODO: this is lossy... we can't know if this is a file or a directory
     215 + Type: cyclonedx.ComponentTypeFile,
     216 + Name: name,
    202 217   }
    203 218   }
    204 219   
    skipped 3 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/spdxhelpers/document_name.go
    skipped 3 lines
    4 4   "github.com/anchore/syft/syft/source"
    5 5  )
    6 6   
    7  -func DocumentName(srcMetadata source.Metadata) string {
     7 +func DocumentName(srcMetadata source.Description) string {
    8 8   if srcMetadata.Name != "" {
    9 9   return srcMetadata.Name
    10 10   }
    11 11   
    12  - switch srcMetadata.Scheme {
    13  - case source.ImageScheme:
    14  - return srcMetadata.ImageMetadata.UserInput
    15  - case source.DirectoryScheme, source.FileScheme:
    16  - return srcMetadata.Path
     12 + switch metadata := srcMetadata.Metadata.(type) {
     13 + case source.ImageMetadata:
     14 + return metadata.UserInput
     15 + case source.DirectoryMetadata:
     16 + return metadata.Path
     17 + case source.FileMetadata:
     18 + return metadata.Path
    17 19   default:
    18 20   return "unknown"
    19 21   }
    skipped 2 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/spdxhelpers/document_name_test.go
    skipped 8 lines
    9 9   "github.com/stretchr/testify/assert"
    10 10   
    11 11   "github.com/anchore/syft/syft/source"
     12 + "github.com/anchore/syft/syft/source/scheme"
    12 13  )
    13 14   
    14 15  func Test_DocumentName(t *testing.T) {
    15 16   allSchemes := strset.New()
    16  - for _, s := range source.AllSchemes {
     17 + for _, s := range scheme.AllSchemes {
    17 18   allSchemes.Add(string(s))
    18 19   }
    19 20   testedSchemes := strset.New()
    skipped 8 lines
    28 29   name: "image",
    29 30   inputName: "my-name",
    30 31   srcMetadata: source.Metadata{
    31  - Scheme: source.ImageScheme,
     32 + Scheme: scheme.ContainerImageScheme,
    32 33   ImageMetadata: source.ImageMetadata{
    33 34   UserInput: "image-repo/name:tag",
    34 35   ID: "id",
    skipped 6 lines
    41 42   name: "directory",
    42 43   inputName: "my-name",
    43 44   srcMetadata: source.Metadata{
    44  - Scheme: source.DirectoryScheme,
     45 + Scheme: scheme.DirectoryScheme,
    45 46   Path: "some/path/to/place",
    46 47   },
    47 48   expected: "some/path/to/place",
    skipped 2 lines
    50 51   name: "file",
    51 52   inputName: "my-name",
    52 53   srcMetadata: source.Metadata{
    53  - Scheme: source.FileScheme,
     54 + Scheme: scheme.FileScheme,
    54 55   Path: "some/path/to/place",
    55 56   },
    56 57   expected: "some/path/to/place",
    skipped 16 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/spdxhelpers/document_namespace.go
    skipped 17 lines
    18 18   inputFile = "file"
    19 19  )
    20 20   
    21  -func DocumentNameAndNamespace(srcMetadata source.Metadata) (string, string) {
    22  - name := DocumentName(srcMetadata)
    23  - return name, DocumentNamespace(name, srcMetadata)
     21 +func DocumentNameAndNamespace(src source.Description) (string, string) {
     22 + name := DocumentName(src)
     23 + return name, DocumentNamespace(name, src)
    24 24  }
    25 25   
    26  -func DocumentNamespace(name string, srcMetadata source.Metadata) string {
     26 +func DocumentNamespace(name string, src source.Description) string {
    27 27   name = cleanName(name)
    28 28   input := "unknown-source-type"
    29  - switch srcMetadata.Scheme {
    30  - case source.ImageScheme:
     29 + switch src.Metadata.(type) {
     30 + case source.ImageMetadata:
    31 31   input = inputImage
    32  - case source.DirectoryScheme:
     32 + case source.DirectoryMetadata:
    33 33   input = inputDirectory
    34  - case source.FileScheme:
     34 + case source.FileMetadata:
    35 35   input = inputFile
    36 36   }
    37 37   
    skipped 25 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/spdxhelpers/document_namespace_test.go
    skipped 8 lines
    9 9   "github.com/stretchr/testify/assert"
    10 10   
    11 11   "github.com/anchore/syft/syft/source"
     12 + "github.com/anchore/syft/syft/source/scheme"
    12 13  )
    13 14   
    14 15  func Test_documentNamespace(t *testing.T) {
    15 16   allSchemes := strset.New()
    16  - for _, s := range source.AllSchemes {
     17 + for _, s := range scheme.AllSchemes {
    17 18   allSchemes.Add(string(s))
    18 19   }
    19 20   testedSchemes := strset.New()
    20 21   
    21 22   tests := []struct {
    22  - name string
    23  - inputName string
    24  - srcMetadata source.Metadata
    25  - expected string
     23 + name string
     24 + inputName string
     25 + src source.Description
     26 + expected string
    26 27   }{
    27 28   {
    28 29   name: "image",
    29 30   inputName: "my-name",
    30  - srcMetadata: source.Metadata{
    31  - Scheme: source.ImageScheme,
    32  - ImageMetadata: source.ImageMetadata{
     31 + src: source.Description{
     32 + Metadata: source.ImageMetadata{
    33 33   UserInput: "image-repo/name:tag",
    34 34   ID: "id",
    35 35   ManifestDigest: "digest",
    skipped 4 lines
    40 40   {
    41 41   name: "directory",
    42 42   inputName: "my-name",
    43  - srcMetadata: source.Metadata{
    44  - Scheme: source.DirectoryScheme,
    45  - Path: "some/path/to/place",
     43 + src: source.Description{
     44 + Metadata: source.DirectoryMetadata{
     45 + Path: "some/path/to/place",
     46 + },
    46 47   },
    47 48   expected: "https://anchore.com/syft/dir/my-name-",
    48 49   },
    49 50   {
    50 51   name: "file",
    51 52   inputName: "my-name",
    52  - srcMetadata: source.Metadata{
    53  - Scheme: source.FileScheme,
    54  - Path: "some/path/to/place",
     53 + src: source.Description{
     54 + Metadata: source.FileMetadata{
     55 + Path: "some/path/to/place",
     56 + },
    55 57   },
    56 58   expected: "https://anchore.com/syft/file/my-name-",
    57 59   },
    58 60   }
    59 61   for _, test := range tests {
    60 62   t.Run(test.name, func(t *testing.T) {
    61  - actual := DocumentNamespace(test.inputName, test.srcMetadata)
     63 + actual := DocumentNamespace(test.inputName, test.src)
    62 64   // note: since the namespace ends with a UUID we check the prefix
    63 65   assert.True(t, strings.HasPrefix(actual, test.expected), fmt.Sprintf("actual namespace %q", actual))
    64 66   
     67 + // TODO: can we get a collection of known possible options here?
    65 68   // track each scheme tested (passed or not)
    66  - testedSchemes.Add(string(test.srcMetadata.Scheme))
     69 + testedSchemes.Add(string(test.src.Scheme))
    67 70   })
    68 71   }
    69 72   
    skipped 4 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/spdxhelpers/to_syft_model.go
    skipped 27 lines
    28 28   
    29 29   spdxIDMap := make(map[string]interface{})
    30 30   
    31  - src := source.Metadata{Scheme: source.UnknownScheme}
    32  - src.Scheme = extractSchemeFromNamespace(doc.DocumentNamespace)
     31 + src := extractSourceFromNamespace(doc.DocumentNamespace)
    33 32   
    34 33   s := &sbom.SBOM{
    35 34   Source: src,
    skipped 18 lines
    54 53  // image, directory, for example. This is our best effort to determine
    55 54  // the scheme. Syft-generated SBOMs have in the namespace
    56 55  // field a type encoded, which we try to identify here.
    57  -func extractSchemeFromNamespace(ns string) source.Scheme {
     56 +func extractSourceFromNamespace(ns string) source.Description {
    58 57   u, err := url.Parse(ns)
    59 58   if err != nil {
    60  - return source.UnknownScheme
     59 + return source.Description{
     60 + Metadata: nil,
     61 + }
    61 62   }
    62 63   
    63 64   parts := strings.Split(u.Path, "/")
    64 65   for _, p := range parts {
    65 66   switch p {
    66 67   case inputFile:
    67  - return source.FileScheme
     68 + return source.Description{
     69 + Metadata: source.FileMetadata{},
     70 + }
    68 71   case inputImage:
    69  - return source.ImageScheme
     72 + return source.Description{
     73 + Metadata: source.ImageMetadata{},
     74 + }
    70 75   case inputDirectory:
    71  - return source.DirectoryScheme
     76 + return source.Description{
     77 + Metadata: source.DirectoryMetadata{},
     78 + }
    72 79   }
    73 80   }
    74  - return source.UnknownScheme
     81 + return source.Description{}
    75 82  }
    76 83   
    77 84  func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
    skipped 348 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/common/spdxhelpers/to_syft_model_test.go
    skipped 10 lines
    11 11   "github.com/anchore/syft/syft/artifact"
    12 12   "github.com/anchore/syft/syft/file"
    13 13   "github.com/anchore/syft/syft/pkg"
    14  - "github.com/anchore/syft/syft/source"
     14 + "github.com/anchore/syft/syft/source/scheme"
    15 15  )
    16 16   
    17 17  func TestToSyftModel(t *testing.T) {
    skipped 179 lines
    197 197  func TestExtractSourceFromNamespaces(t *testing.T) {
    198 198   tests := []struct {
    199 199   namespace string
    200  - expected source.Scheme
     200 + expected scheme.Scheme
    201 201   }{
    202 202   {
    203 203   namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3",
    204  - expected: source.FileScheme,
     204 + expected: scheme.FileScheme,
    205 205   },
    206 206   {
    207 207   namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3",
    208  - expected: source.ImageScheme,
     208 + expected: scheme.ContainerImageScheme,
    209 209   },
    210 210   {
    211 211   namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3",
    212  - expected: source.DirectoryScheme,
     212 + expected: scheme.DirectoryScheme,
    213 213   },
    214 214   {
    215 215   namespace: "https://another-host/blob/123",
    216  - expected: source.UnknownScheme,
     216 + expected: scheme.UnknownScheme,
    217 217   },
    218 218   {
    219 219   namespace: "bla bla",
    220  - expected: source.UnknownScheme,
     220 + expected: scheme.UnknownScheme,
    221 221   },
    222 222   {
    223 223   namespace: "",
    224  - expected: source.UnknownScheme,
     224 + expected: scheme.UnknownScheme,
    225 225   },
    226 226   }
    227 227   
    228 228   for _, tt := range tests {
    229  - require.Equal(t, tt.expected, extractSchemeFromNamespace(tt.namespace))
     229 + require.Equal(t, tt.expected, extractSourceFromNamespace(tt.namespace))
    230 230   }
    231 231  }
    232 232   
    skipped 191 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/github/encoder.go
    skipped 63 lines
    64 64   return ""
    65 65  }
    66 66   
    67  -// isArchive returns true if the path appears to be an archive
    68  -func isArchive(path string) bool {
    69  - _, err := archiver.ByExtension(path)
    70  - return err == nil
    71  -}
    72  - 
    73  -// toPath Generates a string representation of the package location, optionally including the layer hash
    74  -func toPath(s source.Metadata, p pkg.Package) string {
    75  - inputPath := strings.TrimPrefix(s.Path, "./")
    76  - if inputPath == "." {
    77  - inputPath = ""
    78  - }
    79  - locations := p.Locations.ToSlice()
    80  - if len(locations) > 0 {
    81  - location := locations[0]
    82  - packagePath := location.RealPath
    83  - if location.VirtualPath != "" {
    84  - packagePath = location.VirtualPath
    85  - }
    86  - packagePath = strings.TrimPrefix(packagePath, "/")
    87  - switch s.Scheme {
    88  - case source.ImageScheme:
    89  - image := strings.ReplaceAll(s.ImageMetadata.UserInput, ":/", "//")
    90  - return fmt.Sprintf("%s:/%s", image, packagePath)
    91  - case source.FileScheme:
    92  - if isArchive(inputPath) {
    93  - return fmt.Sprintf("%s:/%s", inputPath, packagePath)
    94  - }
    95  - return inputPath
    96  - case source.DirectoryScheme:
    97  - if inputPath != "" {
    98  - return fmt.Sprintf("%s/%s", inputPath, packagePath)
    99  - }
    100  - return packagePath
    101  - }
    102  - }
    103  - return fmt.Sprintf("%s%s", inputPath, s.ImageMetadata.UserInput)
    104  -}
    105  - 
    106 67  // toGithubManifests manifests, each of which represents a specific location that has dependencies
    107 68  func toGithubManifests(s *sbom.SBOM) Manifests {
    108 69   manifests := map[string]*Manifest{}
    skipped 35 lines
    144 105   return out
    145 106  }
    146 107   
     108 +// toPath Generates a string representation of the package location, optionally including the layer hash
     109 +func toPath(s source.Description, p pkg.Package) string {
     110 + inputPath := strings.TrimPrefix(s.Name, "./")
     111 + if inputPath == "." {
     112 + inputPath = ""
     113 + }
     114 + locations := p.Locations.ToSlice()
     115 + if len(locations) > 0 {
     116 + location := locations[0]
     117 + packagePath := location.RealPath
     118 + if location.VirtualPath != "" {
     119 + packagePath = location.VirtualPath
     120 + }
     121 + packagePath = strings.TrimPrefix(packagePath, "/")
     122 + switch metadata := s.Metadata.(type) {
     123 + case source.ImageMetadata:
     124 + image := strings.ReplaceAll(metadata.UserInput, ":/", "//")
     125 + return fmt.Sprintf("%s:/%s", image, packagePath)
     126 + case source.FileMetadata:
     127 + if isArchive(inputPath) {
     128 + return fmt.Sprintf("%s:/%s", inputPath, packagePath)
     129 + }
     130 + return inputPath
     131 + case source.DirectoryMetadata:
     132 + if inputPath != "" {
     133 + return fmt.Sprintf("%s/%s", inputPath, packagePath)
     134 + }
     135 + return packagePath
     136 + }
     137 + }
     138 + return fmt.Sprintf("%s%s", inputPath, s.Name)
     139 +}
     140 + 
     141 +// isArchive returns true if the path appears to be an archive
     142 +func isArchive(path string) bool {
     143 + _, err := archiver.ByExtension(path)
     144 + return err == nil
     145 +}
     146 + 
     147 +func toDependencies(s *sbom.SBOM, p pkg.Package) (out []string) {
     148 + for _, r := range s.Relationships {
     149 + if r.From.ID() == p.ID() {
     150 + if p, ok := r.To.(pkg.Package); ok {
     151 + out = append(out, dependencyName(p))
     152 + }
     153 + }
     154 + }
     155 + return
     156 +}
     157 + 
    147 158  // dependencyName to make things a little nicer to read; this might end up being lossy
    148 159  func dependencyName(p pkg.Package) string {
    149 160   purl, err := packageurl.FromString(p.PURL)
    skipped 22 lines
    172 183   return Metadata{}
    173 184  }
    174 185   
    175  -func toDependencies(s *sbom.SBOM, p pkg.Package) (out []string) {
    176  - for _, r := range s.Relationships {
    177  - if r.From.ID() == p.ID() {
    178  - if p, ok := r.To.(pkg.Package); ok {
    179  - out = append(out, dependencyName(p))
    180  - }
    181  - }
    182  - }
    183  - return
    184  -}
    185  - 
  • ■ ■ ■ ■ ■ ■
    syft/formats/github/encoder_test.go
    skipped 11 lines
    12 12   "github.com/anchore/syft/syft/pkg"
    13 13   "github.com/anchore/syft/syft/sbom"
    14 14   "github.com/anchore/syft/syft/source"
     15 + "github.com/anchore/syft/syft/source/scheme"
    15 16  )
    16 17   
    17 18  func Test_toGithubModel(t *testing.T) {
    18 19   s := sbom.SBOM{
    19 20   Source: source.Metadata{
    20  - Scheme: source.ImageScheme,
     21 + Scheme: scheme.ContainerImageScheme,
    21 22   ImageMetadata: source.ImageMetadata{
    22 23   UserInput: "ubuntu:18.04",
    23 24   Architecture: "amd64",
    skipped 112 lines
    136 137   
    137 138   // Just test the other schemes:
    138 139   s.Source.Path = "."
    139  - s.Source.Scheme = source.DirectoryScheme
     140 + s.Source.Scheme = scheme.DirectoryScheme
    140 141   actual = toGithubModel(&s)
    141 142   assert.Equal(t, "etc", actual.Manifests["etc"].Name)
    142 143   
    143 144   s.Source.Path = "./artifacts"
    144  - s.Source.Scheme = source.DirectoryScheme
     145 + s.Source.Scheme = scheme.DirectoryScheme
    145 146   actual = toGithubModel(&s)
    146 147   assert.Equal(t, "artifacts/etc", actual.Manifests["artifacts/etc"].Name)
    147 148   
    148 149   s.Source.Path = "/artifacts"
    149  - s.Source.Scheme = source.DirectoryScheme
     150 + s.Source.Scheme = scheme.DirectoryScheme
    150 151   actual = toGithubModel(&s)
    151 152   assert.Equal(t, "/artifacts/etc", actual.Manifests["/artifacts/etc"].Name)
    152 153   
    153 154   s.Source.Path = "./executable"
    154  - s.Source.Scheme = source.FileScheme
     155 + s.Source.Scheme = scheme.FileScheme
    155 156   actual = toGithubModel(&s)
    156 157   assert.Equal(t, "executable", actual.Manifests["executable"].Name)
    157 158   
    158 159   s.Source.Path = "./archive.tar.gz"
    159  - s.Source.Scheme = source.FileScheme
     160 + s.Source.Scheme = scheme.FileScheme
    160 161   actual = toGithubModel(&s)
    161 162   assert.Equal(t, "archive.tar.gz:/etc", actual.Manifests["archive.tar.gz:/etc"].Name)
    162 163  }
    skipped 1 lines
  • ■ ■ ■ ■ ■
    syft/formats/spdxtagvalue/encoder_test.go
    skipped 8 lines
    9 9   "github.com/anchore/syft/syft/pkg"
    10 10   "github.com/anchore/syft/syft/sbom"
    11 11   "github.com/anchore/syft/syft/source"
     12 + "github.com/anchore/syft/syft/source/scheme"
    12 13  )
    13 14   
    14 15  var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv encoders")
    skipped 38 lines
    53 54   },
    54 55   Relationships: nil,
    55 56   Source: source.Metadata{
    56  - Scheme: source.DirectoryScheme,
     57 + Scheme: scheme.DirectoryScheme,
    57 58   Path: "foobar/baz", // in this case, foobar is used as the spdx docment name
    58 59   },
    59 60   Descriptor: sbom.Descriptor{
    skipped 38 lines
  • ■ ■ ■ ■ ■
    syft/formats/syftjson/encoder_test.go
    skipped 13 lines
    14 14   "github.com/anchore/syft/syft/pkg"
    15 15   "github.com/anchore/syft/syft/sbom"
    16 16   "github.com/anchore/syft/syft/source"
     17 + "github.com/anchore/syft/syft/source/scheme"
    17 18  )
    18 19   
    19 20  var updateJson = flag.Bool("update-json", false, "update the *.golden files for json encoders")
    skipped 158 lines
    178 179   },
    179 180   Source: source.Metadata{
    180 181   ID: "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
    181  - Scheme: source.ImageScheme,
     182 + Scheme: scheme.ContainerImageScheme,
    182 183   ImageMetadata: source.ImageMetadata{
    183 184   UserInput: "user-image-input",
    184 185   ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
    skipped 43 lines
  • ■ ■ ■ ■ ■
    syft/formats/syftjson/model/source.go
    skipped 2 lines
    3 3  import (
    4 4   "encoding/json"
    5 5   "fmt"
    6  - "strconv"
    7 6   
    8 7   "github.com/anchore/syft/syft/source"
    9 8  )
    10 9   
     10 +type SourceType string
     11 + 
     12 +const (
     13 + DirectorySourceType SourceType = "dir"
     14 + FileSourceType SourceType = "file"
     15 + ImageSourceType SourceType = "image"
     16 +)
     17 + 
     18 +func AllSourceTypes() []SourceType {
     19 + return []SourceType{
     20 + DirectorySourceType,
     21 + FileSourceType,
     22 + ImageSourceType,
     23 + }
     24 +}
     25 + 
    11 26  // Source object represents the thing that was cataloged
    12 27  type Source struct {
    13 28   ID string `json:"id"`
    14  - Type string `json:"type"`
     29 + Type SourceType `json:"type"`
    15 30   Target interface{} `json:"target"`
    16 31  }
    17 32   
    skipped 11 lines
    29 44   return err
    30 45   }
    31 46   
    32  - s.Type = unpacker.Type
     47 + s.Type = SourceType(unpacker.Type)
    33 48   s.ID = unpacker.ID
    34 49   
    35 50   switch s.Type {
    36  - case "directory", "file":
    37  - if target, err := strconv.Unquote(string(unpacker.Target)); err == nil {
    38  - s.Target = target
    39  - } else {
    40  - s.Target = string(unpacker.Target[:])
     51 + case DirectorySourceType:
     52 + var payload source.DirectoryMetadata
     53 + if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
     54 + return err
     55 + }
     56 + s.Target = payload
     57 + case FileSourceType:
     58 + var payload source.FileMetadata
     59 + if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
     60 + return err
    41 61   }
     62 + s.Target = payload
    42 63   
    43  - case "image":
     64 + case ImageSourceType:
    44 65   var payload source.ImageMetadata
    45 66   if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
    46 67   return err
    skipped 10 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/syftjson/to_format_model.go
    skipped 266 lines
    267 267  }
    268 268   
    269 269  // toSourceModel creates a new source object to be represented into JSON.
    270  -func toSourceModel(src source.Metadata) (model.Source, error) {
    271  - switch src.Scheme {
    272  - case source.ImageScheme:
    273  - metadata := src.ImageMetadata
     270 +func toSourceModel(src source.Description) (model.Source, error) {
     271 + switch metadata := src.Metadata.(type) {
     272 + case source.ImageMetadata:
    274 273   // ensure that empty collections are not shown as null
    275 274   if metadata.RepoDigests == nil {
    276 275   metadata.RepoDigests = []string{}
    skipped 3 lines
    280 279   }
    281 280   return model.Source{
    282 281   ID: src.ID,
    283  - Type: "image",
     282 + Type: model.ImageSourceType,
    284 283   Target: metadata,
    285 284   }, nil
    286  - case source.DirectoryScheme:
     285 + case source.DirectoryMetadata:
    287 286   return model.Source{
    288 287   ID: src.ID,
    289  - Type: "directory",
    290  - Target: src.Path,
     288 + Type: model.DirectorySourceType,
     289 + Target: metadata.Path,
    291 290   }, nil
    292  - case source.FileScheme:
     291 + case source.FileMetadata:
    293 292   return model.Source{
    294 293   ID: src.ID,
    295  - Type: "file",
    296  - Target: src.Path,
     294 + Type: model.FileSourceType,
     295 + Target: metadata.Path,
    297 296   }, nil
    298 297   default:
    299  - return model.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme)
     298 + return model.Source{}, fmt.Errorf("unsupported source: %T", metadata)
    300 299   }
    301 300  }
    302 301   
  • ■ ■ ■ ■ ■ ■
    syft/formats/syftjson/to_format_model_test.go
    skipped 10 lines
    11 11   "github.com/anchore/syft/syft/file"
    12 12   "github.com/anchore/syft/syft/formats/syftjson/model"
    13 13   "github.com/anchore/syft/syft/source"
     14 + "github.com/anchore/syft/syft/source/scheme"
    14 15  )
    15 16   
    16 17  func Test_toSourceModel(t *testing.T) {
    17 18   allSchemes := strset.New()
    18  - for _, s := range source.AllSchemes {
     19 + for _, s := range scheme.AllSchemes {
    19 20   allSchemes.Add(string(s))
    20 21   }
    21 22   testedSchemes := strset.New()
    skipped 7 lines
    29 30   name: "directory",
    30 31   src: source.Metadata{
    31 32   ID: "test-id",
    32  - Scheme: source.DirectoryScheme,
     33 + Scheme: scheme.DirectoryScheme,
    33 34   Path: "some/path",
    34 35   },
    35 36   expected: model.Source{
    skipped 6 lines
    42 43   name: "file",
    43 44   src: source.Metadata{
    44 45   ID: "test-id",
    45  - Scheme: source.FileScheme,
     46 + Scheme: scheme.FileScheme,
    46 47   Path: "some/path",
    47 48   },
    48 49   expected: model.Source{
    skipped 6 lines
    55 56   name: "image",
    56 57   src: source.Metadata{
    57 58   ID: "test-id",
    58  - Scheme: source.ImageScheme,
     59 + Scheme: scheme.ContainerImageScheme,
    59 60   ImageMetadata: source.ImageMetadata{
    60 61   UserInput: "user-input",
    61 62   ID: "id...",
    skipped 137 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/syftjson/to_syft_model.go
    skipped 201 lines
    202 202   return out, conversionErrors
    203 203  }
    204 204   
    205  -func toSyftSource(s model.Source) *source.Source {
    206  - newSrc := &source.Source{
    207  - Metadata: *toSyftSourceData(s),
     205 +func toSyftSource(s model.Source) source.Source {
     206 + description := toSyftSourceData(s)
     207 + if description == nil {
     208 + return nil
    208 209   }
    209  - newSrc.SetID()
    210  - return newSrc
     210 + return source.FromDescription(*description)
    211 211  }
    212 212   
    213 213  func toSyftRelationship(idMap map[string]interface{}, relationship model.Relationship, idAliases map[string]string) (*artifact.Relationship, error) {
    skipped 43 lines
    257 257   }
    258 258  }
    259 259   
    260  -func toSyftSourceData(s model.Source) *source.Metadata {
     260 +func toSyftSourceData(s model.Source) *source.Description {
    261 261   switch s.Type {
    262  - case "directory":
    263  - path, ok := s.Target.(string)
     262 + case model.DirectorySourceType:
     263 + metadata, ok := s.Target.(source.DirectoryMetadata)
    264 264   if !ok {
    265 265   log.Warnf("unable to parse source target as string: %+v", s.Target)
    266 266   return nil
    267 267   }
    268  - return &source.Metadata{
    269  - ID: s.ID,
    270  - Scheme: source.DirectoryScheme,
    271  - Path: path,
     268 + return &source.Description{
     269 + ID: s.ID,
     270 + Name: metadata.Path,
     271 + Metadata: metadata,
    272 272   }
    273  - case "file":
    274  - path, ok := s.Target.(string)
     273 + case model.FileSourceType:
     274 + metadata, ok := s.Target.(source.DirectoryMetadata)
    275 275   if !ok {
    276 276   log.Warnf("unable to parse source target as string: %+v", s.Target)
    277 277   return nil
    278 278   }
    279  - return &source.Metadata{
    280  - ID: s.ID,
    281  - Scheme: source.FileScheme,
    282  - Path: path,
     279 + return &source.Description{
     280 + ID: s.ID,
     281 + Name: metadata.Path,
     282 + Metadata: metadata,
    283 283   }
    284  - case "image":
     284 + case model.ImageSourceType:
    285 285   metadata, ok := s.Target.(source.ImageMetadata)
    286 286   if !ok {
    287 287   log.Warnf("unable to parse source target as image metadata: %+v", s.Target)
    288 288   return nil
    289 289   }
    290  - return &source.Metadata{
    291  - ID: s.ID,
    292  - Scheme: source.ImageScheme,
    293  - ImageMetadata: metadata,
     290 + return &source.Description{
     291 + ID: s.ID,
     292 + Name: metadata.UserInput,
     293 + Metadata: metadata,
    294 294   }
    295 295   }
    296 296   return nil
    skipped 50 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/syftjson/to_syft_model_test.go
    skipped 17 lines
    18 18   
    19 19  func Test_toSyftSourceData(t *testing.T) {
    20 20   allSchemes := strset.New()
    21  - for _, s := range source.AllSchemes {
     21 + for _, s := range model.AllSourceTypes() {
    22 22   allSchemes.Add(string(s))
    23 23   }
    24 24   testedSchemes := strset.New()
    skipped 1 lines
    26 26   tests := []struct {
    27 27   name string
    28 28   src model.Source
    29  - expected source.Metadata
     29 + expected *source.Description
    30 30   }{
    31 31   {
    32 32   name: "directory",
    33  - expected: source.Metadata{
    34  - Scheme: source.DirectoryScheme,
    35  - Path: "some/path",
     33 + expected: &source.Description{
     34 + ID: "the-id",
     35 + Name: "some/path",
     36 + Metadata: source.FileMetadata{
     37 + Path: "some/path",
     38 + },
    36 39   },
    37 40   src: model.Source{
    38  - Type: "directory",
     41 + Type: model.DirectorySourceType,
    39 42   Target: "some/path",
    40 43   },
    41 44   },
    42 45   {
    43 46   name: "file",
    44  - expected: source.Metadata{
    45  - Scheme: source.FileScheme,
    46  - Path: "some/path",
     47 + expected: &source.Description{
     48 + ID: "the-id",
     49 + Name: "some/path",
     50 + Metadata: source.FileMetadata{
     51 + Path: "some/path",
     52 + },
    47 53   },
    48 54   src: model.Source{
    49  - Type: "file",
     55 + Type: model.FileSourceType,
    50 56   Target: "some/path",
    51 57   },
    52 58   },
    53 59   {
    54 60   name: "image",
    55  - expected: source.Metadata{
    56  - Scheme: source.ImageScheme,
    57  - ImageMetadata: source.ImageMetadata{
     61 + expected: &source.Description{
     62 + ID: "the-id",
     63 + Name: "user-input",
     64 + Metadata: source.ImageMetadata{
    58 65   UserInput: "user-input",
    59 66   ID: "id...",
    60 67   ManifestDigest: "digest...",
    skipped 1 lines
    62 69   },
    63 70   },
    64 71   src: model.Source{
    65  - Type: "image",
     72 + Type: model.ImageSourceType,
    66 73   Target: source.ImageMetadata{
    67 74   UserInput: "user-input",
    68 75   ID: "id...",
    skipped 10 lines
    79 86   assert.Equal(t, test.expected, *actual)
    80 87   
    81 88   // track each scheme tested (passed or not)
    82  - testedSchemes.Add(string(test.expected.Scheme))
     89 + testedSchemes.Add(string(test.src.Type))
    83 90   })
    84 91   }
    85 92   
    skipped 267 lines
  • ■ ■ ■ ■ ■ ■
    syft/formats/text/encoder.go
    skipped 13 lines
    14 14   w := new(tabwriter.Writer)
    15 15   w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
    16 16   
    17  - switch s.Source.Scheme {
    18  - case source.DirectoryScheme, source.FileScheme:
    19  - fmt.Fprintf(w, "[Path: %s]\n", s.Source.Path)
    20  - case source.ImageScheme:
     17 + switch metadata := s.Source.Metadata.(type) {
     18 + case source.DirectoryMetadata:
     19 + fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
     20 + case source.FileMetadata:
     21 + fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
     22 + case source.ImageMetadata:
    21 23   fmt.Fprintln(w, "[Image]")
    22 24   
    23  - for idx, l := range s.Source.ImageMetadata.Layers {
     25 + for idx, l := range metadata.Layers {
    24 26   fmt.Fprintln(w, " Layer:\t", idx)
    25 27   fmt.Fprintln(w, " Digest:\t", l.Digest)
    26 28   fmt.Fprintln(w, " Size:\t", l.Size)
    skipped 2 lines
    29 31   w.Flush()
    30 32   }
    31 33   default:
    32  - return fmt.Errorf("unsupported source: %T", s.Source.Scheme)
     34 + return fmt.Errorf("unsupported source: %T", s.Source.Metadata)
    33 35   }
    34 36   
    35 37   // populate artifacts...
    skipped 19 lines
  • ■ ■ ■ ■ ■ ■
    syft/internal/fileresolver/container_image_squash_test.go
    skipped 5 lines
    6 6   "testing"
    7 7   
    8 8   "github.com/google/go-cmp/cmp"
     9 + "github.com/google/go-cmp/cmp/cmpopts"
    9 10   "github.com/scylladb/go-set/strset"
    10 11   "github.com/stretchr/testify/assert"
    11 12   "github.com/stretchr/testify/require"
    skipped 497 lines
    509 510   
    510 511   compareLocations(t, test.expected, actual)
    511 512   })
     513 + }
     514 + 
     515 +}
     516 + 
     517 +func compareLocations(t *testing.T, expected, actual []file.Location) {
     518 + t.Helper()
     519 + ignoreUnexported := cmpopts.IgnoreFields(file.LocationData{}, "ref")
     520 + ignoreMetadata := cmpopts.IgnoreFields(file.LocationMetadata{}, "Annotations")
     521 + ignoreFS := cmpopts.IgnoreFields(file.Coordinates{}, "FileSystemID")
     522 + 
     523 + sort.Sort(file.Locations(expected))
     524 + sort.Sort(file.Locations(actual))
     525 + 
     526 + if d := cmp.Diff(expected, actual,
     527 + ignoreUnexported,
     528 + ignoreFS,
     529 + ignoreMetadata,
     530 + ); d != "" {
     531 + 
     532 + t.Errorf("unexpected locations (-want +got):\n%s", d)
    512 533   }
    513 534   
    514 535  }
    skipped 28 lines
  • ■ ■ ■ ■ ■ ■
    syft/internal/fileresolver/excluding_file.go
    skipped 15 lines
    16 16   excludeFn excludeFn
    17 17  }
    18 18   
    19  -// NewExcluding create a new resolver which wraps the provided delegate and excludes
     19 +// NewExcludingDecorator create a new resolver which wraps the provided delegate and excludes
    20 20  // entries based on a provided path exclusion function
    21  -func NewExcluding(delegate file.Resolver, excludeFn excludeFn) file.Resolver {
     21 +func NewExcludingDecorator(delegate file.Resolver, excludeFn excludeFn) file.Resolver {
    22 22   return &excluding{
    23 23   delegate,
    24 24   excludeFn,
    skipped 80 lines
  • ■ ■ ■ ■
    syft/internal/fileresolver/excluding_file_test.go
    skipped 55 lines
    56 56   resolver := &mockResolver{
    57 57   locations: test.locations,
    58 58   }
    59  - er := NewExcluding(resolver, test.excludeFn)
     59 + er := NewExcludingDecorator(resolver, test.excludeFn)
    60 60   
    61 61   locations, _ := er.FilesByPath()
    62 62   assert.ElementsMatch(t, locationPaths(locations), test.expected)
    skipped 134 lines
  • ■ ■ ■ ■ ■ ■
    syft/lib.go
    skipped 33 lines
    34 34  // CatalogPackages takes an inventory of packages from the given image from a particular perspective
    35 35  // (e.g. squashed source, all-layers source). Returns the discovered set of packages, the identified Linux
    36 36  // distribution, and the source object used to wrap the data source.
    37  -func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Collection, []artifact.Relationship, *linux.Release, error) {
     37 +func CatalogPackages(src source.Source, cfg cataloger.Config) (*pkg.Collection, []artifact.Relationship, *linux.Release, error) {
    38 38   resolver, err := src.FileResolver(cfg.Search.Scope)
    39 39   if err != nil {
    40 40   return nil, nil, nil, fmt.Errorf("unable to determine resolver while cataloging packages: %w", err)
    skipped 13 lines
    54 54   catalogers = cataloger.AllCatalogers(cfg)
    55 55   } else {
    56 56   // otherwise conditionally use the correct set of loggers based on the input type (container image or directory)
    57  - switch src.Metadata.Scheme {
    58  - case source.ImageScheme:
     57 + 
     58 + // TODO: this is bad, we should not be using the concrete type to determine the cataloger set
     59 + // instead this should be a caller concern (pass the catalogers you want to use). The SBOM build PR will do this.
     60 + switch src.(type) {
     61 + 
     62 + case *source.StereoscopeImageSource:
    59 63   log.Info("cataloging image")
    60 64   catalogers = cataloger.ImageCatalogers(cfg)
    61  - case source.FileScheme:
     65 + case *source.FileSource:
    62 66   log.Info("cataloging file")
    63 67   catalogers = cataloger.AllCatalogers(cfg)
    64  - case source.DirectoryScheme:
     68 + case *source.DirectorySource:
    65 69   log.Info("cataloging directory")
    66 70   catalogers = cataloger.DirectoryCatalogers(cfg)
    67 71   default:
    68  - return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
     72 + // TODO: no
     73 + panic("REMOVE ME")
    69 74   }
    70 75   }
    71 76   
    skipped 4 lines
    76 81   return catalog, relationships, release, err
    77 82  }
    78 83   
    79  -func newSourceRelationshipsFromCatalog(src *source.Source, c *pkg.Collection) []artifact.Relationship {
     84 +func newSourceRelationshipsFromCatalog(src source.Source, c *pkg.Collection) []artifact.Relationship {
    80 85   relationships := make([]artifact.Relationship, 0) // Should we pre-allocate this by giving catalog a Len() method?
    81 86   for p := range c.Enumerate() {
    82 87   relationships = append(relationships, artifact.Relationship{
    skipped 19 lines
  • ■ ■ ■ ■ ■
    syft/pkg/cataloger/search_config.go
    1 1  package cataloger
    2 2   
    3  -import "github.com/anchore/syft/syft/source"
     3 +import (
     4 + "github.com/anchore/syft/syft/source"
     5 +)
    4 6   
    5 7  type SearchConfig struct {
    6 8   IncludeIndexedArchives bool
    skipped 12 lines
  • ■ ■ ■ ■
    syft/sbom/sbom.go
    skipped 14 lines
    15 15  type SBOM struct {
    16 16   Artifacts Artifacts
    17 17   Relationships []artifact.Relationship
    18  - Source source.Metadata
     18 + Source source.Description
    19 19   Descriptor Descriptor
    20 20  }
    21 21   
    skipped 91 lines
  • ■ ■ ■ ■ ■ ■
    syft/source/deprecated.go
    skipped 7 lines
    8 8   "github.com/anchore/syft/syft/file"
    9 9  )
    10 10   
    11  -// Deprecated: use file.Metadata instead
    12  -type FileMetadata = file.Metadata
    13  - 
    14 11  type (
    15 12   // Deprecated: use file.Coordinates instead
    16 13   Coordinates = file.Coordinates
    skipped 104 lines
  • ■ ■ ■ ■ ■ ■
    syft/source/description.go
     1 +package source
     2 + 
     3 +// Description represents any static source data that helps describe "what" was cataloged.
     4 +type Description struct {
     5 + ID string `hash:"ignore"` // the id generated from the parent source struct
     6 + Name string
     7 + Metadata interface{}
     8 +}
     9 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/digest_utils.go
     1 +package source
     2 + 
     3 +import (
     4 + "strings"
     5 + 
     6 + "github.com/anchore/syft/syft/artifact"
     7 +)
     8 + 
     9 +func artifactIDFromDigest(input string) artifact.ID {
     10 + return artifact.ID(strings.TrimPrefix(input, "sha256:"))
     11 +}
     12 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/directory_source.go
     1 +package source
     2 + 
     3 +import (
     4 + "fmt"
     5 + "os"
     6 + "path/filepath"
     7 + "strings"
     8 + "sync"
     9 + 
     10 + "github.com/bmatcuk/doublestar/v4"
     11 + "github.com/opencontainers/go-digest"
     12 + 
     13 + "github.com/anchore/syft/syft/artifact"
     14 + "github.com/anchore/syft/syft/file"
     15 + "github.com/anchore/syft/syft/internal/fileresolver"
     16 +)
     17 + 
     18 +var _ Source = (*DirectorySource)(nil)
     19 + 
     20 +type DirectoryConfig struct {
     21 + Path string
     22 + Base string
     23 + Exclude ExcludeConfig
     24 + Name string // ? can this be done differently?
     25 +}
     26 + 
     27 +type DirectoryMetadata struct {
     28 + Path string
     29 + Base string
     30 +}
     31 + 
     32 +type DirectorySource struct {
     33 + id artifact.ID
     34 + config DirectoryConfig
     35 + resolver *fileresolver.Directory
     36 + mutex *sync.Mutex
     37 + 
     38 + // implements PathInterpreter
     39 +}
     40 + 
     41 +func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
     42 + fi, err := os.Stat(cfg.Path)
     43 + if err != nil {
     44 + return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
     45 + }
     46 + 
     47 + if !fi.IsDir() {
     48 + return nil, fmt.Errorf("given path is not a directory (path=%q): %w", cfg.Path, err)
     49 + }
     50 + 
     51 + return &DirectorySource{
     52 + id: artifactIDFromDigest(digest.FromString(filepath.Clean(cfg.Path)).String()),
     53 + config: cfg,
     54 + mutex: &sync.Mutex{},
     55 + }, nil
     56 +}
     57 + 
     58 +func (s DirectorySource) ID() artifact.ID {
     59 + return s.id
     60 +}
     61 + 
     62 +func (s DirectorySource) Describe() Description {
     63 + return Description{
     64 + ID: string(s.id),
     65 + Name: s.config.Path,
     66 + Metadata: DirectoryMetadata{
     67 + Path: s.config.Path,
     68 + Base: s.config.Base,
     69 + },
     70 + }
     71 +}
     72 + 
     73 +func (s *DirectorySource) FileResolver(scope Scope) (file.Resolver, error) {
     74 + s.mutex.Lock()
     75 + defer s.mutex.Unlock()
     76 + 
     77 + if s.resolver == nil {
     78 + exclusionFunctions, err := getDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
     79 + if err != nil {
     80 + return nil, err
     81 + }
     82 + 
     83 + res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...)
     84 + if err != nil {
     85 + return nil, fmt.Errorf("unable to create directory resolver: %w", err)
     86 + }
     87 + 
     88 + s.resolver = res
     89 + }
     90 + 
     91 + return s.resolver, nil
     92 +}
     93 + 
     94 +func (s *DirectorySource) Close() error {
     95 + s.mutex.Lock()
     96 + defer s.mutex.Unlock()
     97 + s.resolver = nil
     98 + return nil
     99 +}
     100 + 
     101 +func getDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, error) {
     102 + if len(exclusions) == 0 {
     103 + return nil, nil
     104 + }
     105 + 
     106 + // this is what directoryResolver.indexTree is doing to get the absolute path:
     107 + root, err := filepath.Abs(root)
     108 + if err != nil {
     109 + return nil, err
     110 + }
     111 + 
     112 + // this handles Windows file paths by converting them to C:/something/else format
     113 + root = filepath.ToSlash(root)
     114 + 
     115 + if !strings.HasSuffix(root, "/") {
     116 + root += "/"
     117 + }
     118 + 
     119 + var errors []string
     120 + for idx, exclusion := range exclusions {
     121 + // check exclusions for supported paths, these are all relative to the "scan root"
     122 + if strings.HasPrefix(exclusion, "./") || strings.HasPrefix(exclusion, "*/") || strings.HasPrefix(exclusion, "**/") {
     123 + exclusion = strings.TrimPrefix(exclusion, "./")
     124 + exclusions[idx] = root + exclusion
     125 + } else {
     126 + errors = append(errors, exclusion)
     127 + }
     128 + }
     129 + 
     130 + if errors != nil {
     131 + return nil, fmt.Errorf("invalid exclusion pattern(s): '%s' (must start with one of: './', '*/', or '**/')", strings.Join(errors, "', '"))
     132 + }
     133 + 
     134 + return []fileresolver.PathIndexVisitor{
     135 + func(path string, info os.FileInfo, _ error) error {
     136 + for _, exclusion := range exclusions {
     137 + // this is required to handle Windows filepaths
     138 + path = filepath.ToSlash(path)
     139 + matches, err := doublestar.Match(exclusion, path)
     140 + if err != nil {
     141 + return nil
     142 + }
     143 + if matches {
     144 + if info != nil && info.IsDir() {
     145 + return filepath.SkipDir
     146 + }
     147 + return fileresolver.ErrSkipPath
     148 + }
     149 + }
     150 + return nil
     151 + },
     152 + }, nil
     153 +}
     154 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/directory_source_test.go
     1 +package source
     2 + 
     3 +import (
     4 + "io/fs"
     5 + "os"
     6 + "testing"
     7 + 
     8 + "github.com/google/go-cmp/cmp"
     9 + "github.com/stretchr/testify/assert"
     10 + "github.com/stretchr/testify/require"
     11 + 
     12 + "github.com/anchore/stereoscope/pkg/file"
     13 + "github.com/anchore/syft/syft/artifact"
     14 + "github.com/anchore/syft/syft/internal/fileresolver"
     15 +)
     16 + 
     17 +func TestNewFromDirectory(t *testing.T) {
     18 + testCases := []struct {
     19 + desc string
     20 + input string
     21 + expString string
     22 + inputPaths []string
     23 + expectedRefs int
     24 + expectedErr bool
     25 + }{
     26 + {
     27 + desc: "no paths exist",
     28 + input: "foobar/",
     29 + inputPaths: []string{"/opt/", "/other"},
     30 + expectedErr: true,
     31 + },
     32 + {
     33 + desc: "path detected",
     34 + input: "test-fixtures",
     35 + inputPaths: []string{"path-detected/.vimrc"},
     36 + expectedRefs: 1,
     37 + },
     38 + {
     39 + desc: "directory ignored",
     40 + input: "test-fixtures",
     41 + inputPaths: []string{"path-detected"},
     42 + expectedRefs: 0,
     43 + },
     44 + {
     45 + desc: "no files-by-path detected",
     46 + input: "test-fixtures",
     47 + inputPaths: []string{"no-path-detected"},
     48 + expectedRefs: 0,
     49 + },
     50 + }
     51 + for _, test := range testCases {
     52 + t.Run(test.desc, func(t *testing.T) {
     53 + src, err := NewFromDirectory(DirectoryConfig{
     54 + Path: test.input,
     55 + })
     56 + require.NoError(t, err)
     57 + t.Cleanup(func() {
     58 + require.NoError(t, src.Close())
     59 + })
     60 + assert.Equal(t, test.input, src.Describe().Metadata.(DirectoryMetadata).Path)
     61 + 
     62 + res, err := src.FileResolver(SquashedScope)
     63 + if test.expectedErr {
     64 + assert.Error(t, err)
     65 + return
     66 + }
     67 + require.NoError(t, err)
     68 + 
     69 + refs, err := res.FilesByPath(test.inputPaths...)
     70 + require.NoError(t, err)
     71 + 
     72 + if len(refs) != test.expectedRefs {
     73 + t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expectedRefs)
     74 + }
     75 + 
     76 + })
     77 + }
     78 +}
     79 + 
     80 +func Test_DirectorySource_FilesByGlob(t *testing.T) {
     81 + testCases := []struct {
     82 + desc string
     83 + input string
     84 + glob string
     85 + expected int
     86 + }{
     87 + {
     88 + input: "test-fixtures",
     89 + desc: "no matches",
     90 + glob: "bar/foo",
     91 + expected: 0,
     92 + },
     93 + {
     94 + input: "test-fixtures/path-detected",
     95 + desc: "a single match",
     96 + glob: "**/*vimrc",
     97 + expected: 1,
     98 + },
     99 + {
     100 + input: "test-fixtures/path-detected",
     101 + desc: "multiple matches",
     102 + glob: "**",
     103 + expected: 2,
     104 + },
     105 + }
     106 + for _, test := range testCases {
     107 + t.Run(test.desc, func(t *testing.T) {
     108 + src, err := NewFromDirectory(DirectoryConfig{Path: test.input})
     109 + require.NoError(t, err)
     110 + 
     111 + res, err := src.FileResolver(SquashedScope)
     112 + require.NoError(t, err)
     113 + t.Cleanup(func() {
     114 + require.NoError(t, src.Close())
     115 + })
     116 + 
     117 + contents, err := res.FilesByGlob(test.glob)
     118 + require.NoError(t, err)
     119 + if len(contents) != test.expected {
     120 + t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected)
     121 + }
     122 + 
     123 + })
     124 + }
     125 +}
     126 + 
     127 +func Test_DirectorySource_Exclusions(t *testing.T) {
     128 + testCases := []struct {
     129 + desc string
     130 + input string
     131 + glob string
     132 + expected []string
     133 + exclusions []string
     134 + err bool
     135 + }{
     136 + {
     137 + input: "test-fixtures/system_paths",
     138 + desc: "exclude everything",
     139 + glob: "**",
     140 + expected: nil,
     141 + exclusions: []string{"**/*"},
     142 + },
     143 + {
     144 + input: "test-fixtures/image-simple",
     145 + desc: "a single path excluded",
     146 + glob: "**",
     147 + expected: []string{
     148 + "Dockerfile",
     149 + "file-1.txt",
     150 + "file-2.txt",
     151 + },
     152 + exclusions: []string{"**/target/**"},
     153 + },
     154 + {
     155 + input: "test-fixtures/image-simple",
     156 + desc: "exclude explicit directory relative to the root",
     157 + glob: "**",
     158 + expected: []string{
     159 + "Dockerfile",
     160 + "file-1.txt",
     161 + "file-2.txt",
     162 + //"target/really/nested/file-3.txt", // explicitly skipped
     163 + },
     164 + exclusions: []string{"./target"},
     165 + },
     166 + {
     167 + input: "test-fixtures/image-simple",
     168 + desc: "exclude explicit file relative to the root",
     169 + glob: "**",
     170 + expected: []string{
     171 + "Dockerfile",
     172 + //"file-1.txt", // explicitly skipped
     173 + "file-2.txt",
     174 + "target/really/nested/file-3.txt",
     175 + },
     176 + exclusions: []string{"./file-1.txt"},
     177 + },
     178 + {
     179 + input: "test-fixtures/image-simple",
     180 + desc: "exclude wildcard relative to the root",
     181 + glob: "**",
     182 + expected: []string{
     183 + "Dockerfile",
     184 + //"file-1.txt", // explicitly skipped
     185 + //"file-2.txt", // explicitly skipped
     186 + "target/really/nested/file-3.txt",
     187 + },
     188 + exclusions: []string{"./*.txt"},
     189 + },
     190 + {
     191 + input: "test-fixtures/image-simple",
     192 + desc: "exclude files deeper",
     193 + glob: "**",
     194 + expected: []string{
     195 + "Dockerfile",
     196 + "file-1.txt",
     197 + "file-2.txt",
     198 + //"target/really/nested/file-3.txt", // explicitly skipped
     199 + },
     200 + exclusions: []string{"**/really/**"},
     201 + },
     202 + {
     203 + input: "test-fixtures/image-simple",
     204 + desc: "files excluded with extension",
     205 + glob: "**",
     206 + expected: []string{
     207 + "Dockerfile",
     208 + //"file-1.txt", // explicitly skipped
     209 + //"file-2.txt", // explicitly skipped
     210 + //"target/really/nested/file-3.txt", // explicitly skipped
     211 + },
     212 + exclusions: []string{"**/*.txt"},
     213 + },
     214 + {
     215 + input: "test-fixtures/image-simple",
     216 + desc: "keep files with different extensions",
     217 + glob: "**",
     218 + expected: []string{
     219 + "Dockerfile",
     220 + "file-1.txt",
     221 + "file-2.txt",
     222 + "target/really/nested/file-3.txt",
     223 + },
     224 + exclusions: []string{"**/target/**/*.jar"},
     225 + },
     226 + {
     227 + input: "test-fixtures/path-detected",
     228 + desc: "file directly excluded",
     229 + glob: "**",
     230 + expected: []string{
     231 + ".vimrc",
     232 + },
     233 + exclusions: []string{"**/empty"},
     234 + },
     235 + {
     236 + input: "test-fixtures/path-detected",
     237 + desc: "pattern error containing **/",
     238 + glob: "**",
     239 + expected: []string{
     240 + ".vimrc",
     241 + },
     242 + exclusions: []string{"/**/empty"},
     243 + err: true,
     244 + },
     245 + {
     246 + input: "test-fixtures/path-detected",
     247 + desc: "pattern error incorrect start",
     248 + glob: "**",
     249 + expected: []string{
     250 + ".vimrc",
     251 + },
     252 + exclusions: []string{"empty"},
     253 + err: true,
     254 + },
     255 + {
     256 + input: "test-fixtures/path-detected",
     257 + desc: "pattern error starting with /",
     258 + glob: "**",
     259 + expected: []string{
     260 + ".vimrc",
     261 + },
     262 + exclusions: []string{"/empty"},
     263 + err: true,
     264 + },
     265 + }
     266 + 
     267 + for _, test := range testCases {
     268 + t.Run(test.desc, func(t *testing.T) {
     269 + src, err := NewFromDirectory(DirectoryConfig{
     270 + Path: test.input,
     271 + Exclude: ExcludeConfig{
     272 + Paths: test.exclusions,
     273 + },
     274 + })
     275 + require.NoError(t, err)
     276 + t.Cleanup(func() {
     277 + require.NoError(t, src.Close())
     278 + })
     279 + 
     280 + if test.err {
     281 + _, err = src.FileResolver(SquashedScope)
     282 + require.Error(t, err)
     283 + return
     284 + }
     285 + require.NoError(t, err)
     286 + 
     287 + res, err := src.FileResolver(SquashedScope)
     288 + require.NoError(t, err)
     289 + 
     290 + locations, err := res.FilesByGlob(test.glob)
     291 + require.NoError(t, err)
     292 + 
     293 + var actual []string
     294 + for _, l := range locations {
     295 + actual = append(actual, l.RealPath)
     296 + }
     297 + 
     298 + assert.ElementsMatchf(t, test.expected, actual, "diff \n"+cmp.Diff(test.expected, actual))
     299 + })
     300 + }
     301 +}
     302 + 
     303 +func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
     304 + testCases := []struct {
     305 + desc string
     306 + root string
     307 + path string
     308 + finfo os.FileInfo
     309 + exclude string
     310 + walkHint error
     311 + }{
     312 + {
     313 + desc: "directory exclusion",
     314 + root: "/",
     315 + path: "/usr/var/lib",
     316 + exclude: "**/var/lib",
     317 + finfo: file.ManualInfo{ModeValue: os.ModeDir},
     318 + walkHint: fs.SkipDir,
     319 + },
     320 + {
     321 + desc: "no file info",
     322 + root: "/",
     323 + path: "/usr/var/lib",
     324 + exclude: "**/var/lib",
     325 + walkHint: fileresolver.ErrSkipPath,
     326 + },
     327 + // linux specific tests...
     328 + {
     329 + desc: "linux doublestar",
     330 + root: "/usr",
     331 + path: "/usr/var/lib/etc.txt",
     332 + exclude: "**/*.txt",
     333 + finfo: file.ManualInfo{},
     334 + walkHint: fileresolver.ErrSkipPath,
     335 + },
     336 + {
     337 + desc: "linux relative",
     338 + root: "/usr/var/lib",
     339 + path: "/usr/var/lib/etc.txt",
     340 + exclude: "./*.txt",
     341 + finfo: file.ManualInfo{},
     342 + 
     343 + walkHint: fileresolver.ErrSkipPath,
     344 + },
     345 + {
     346 + desc: "linux one level",
     347 + root: "/usr",
     348 + path: "/usr/var/lib/etc.txt",
     349 + exclude: "*/*.txt",
     350 + finfo: file.ManualInfo{},
     351 + walkHint: nil,
     352 + },
     353 + // NOTE: since these tests will run in linux and macOS, the windows paths will be
     354 + // considered relative if they do not start with a forward slash and paths with backslashes
     355 + // won't be modified by the filepath.ToSlash call, so these are emulating the result of
     356 + // filepath.ToSlash usage
     357 + 
     358 + // windows specific tests...
     359 + {
     360 + desc: "windows doublestar",
     361 + root: "/C:/User/stuff",
     362 + path: "/C:/User/stuff/thing.txt",
     363 + exclude: "**/*.txt",
     364 + finfo: file.ManualInfo{},
     365 + walkHint: fileresolver.ErrSkipPath,
     366 + },
     367 + {
     368 + desc: "windows relative",
     369 + root: "/C:/User/stuff",
     370 + path: "/C:/User/stuff/thing.txt",
     371 + exclude: "./*.txt",
     372 + finfo: file.ManualInfo{},
     373 + walkHint: fileresolver.ErrSkipPath,
     374 + },
     375 + {
     376 + desc: "windows one level",
     377 + root: "/C:/User/stuff",
     378 + path: "/C:/User/stuff/thing.txt",
     379 + exclude: "*/*.txt",
     380 + finfo: file.ManualInfo{},
     381 + walkHint: nil,
     382 + },
     383 + }
     384 + 
     385 + for _, test := range testCases {
     386 + t.Run(test.desc, func(t *testing.T) {
     387 + fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude})
     388 + require.NoError(t, err)
     389 + 
     390 + for _, f := range fns {
     391 + result := f(test.path, test.finfo, nil)
     392 + require.Equal(t, test.walkHint, result)
     393 + }
     394 + })
     395 + }
     396 +}
     397 + 
     398 +func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
     399 + testCases := []struct {
     400 + desc string
     401 + input string
     402 + path string
     403 + expected string
     404 + }{
     405 + {
     406 + input: "test-fixtures/path-detected",
     407 + desc: "path does not exist",
     408 + path: "foo",
     409 + },
     410 + }
     411 + for _, test := range testCases {
     412 + t.Run(test.desc, func(t *testing.T) {
     413 + src, err := NewFromDirectory(DirectoryConfig{Path: test.input})
     414 + require.NoError(t, err)
     415 + t.Cleanup(func() {
     416 + require.NoError(t, src.Close())
     417 + })
     418 + 
     419 + res, err := src.FileResolver(SquashedScope)
     420 + require.NoError(t, err)
     421 + 
     422 + refs, err := res.FilesByPath(test.path)
     423 + require.NoError(t, err)
     424 + 
     425 + assert.Len(t, refs, 0)
     426 + })
     427 + }
     428 +}
     429 + 
     430 +func Test_DirectorySource_ID(t *testing.T) {
     431 + tests := []struct {
     432 + name string
     433 + cfg DirectoryConfig
     434 + want artifact.ID
     435 + wantErr require.ErrorAssertionFunc
     436 + }{
     437 + {
     438 + name: "empty",
     439 + cfg: DirectoryConfig{},
     440 + wantErr: require.Error,
     441 + },
     442 + {
     443 + name: "to file",
     444 + cfg: DirectoryConfig{
     445 + Path: "./test-fixtures/image-simple/Dockerfile",
     446 + },
     447 + wantErr: require.Error,
     448 + },
     449 + {
     450 + name: "with path",
     451 + cfg: DirectoryConfig{Path: "./test-fixtures"},
     452 + want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
     453 + },
     454 + {
     455 + name: "with clean path",
     456 + cfg: DirectoryConfig{Path: "test-fixtures/cache/.."},
     457 + want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
     458 + },
     459 + {
     460 + name: "other fields do not affect ID",
     461 + cfg: DirectoryConfig{
     462 + Path: "test-fixtures",
     463 + Base: "a-base!",
     464 + Exclude: ExcludeConfig{
     465 + Paths: []string{"a", "b"},
     466 + },
     467 + Name: "name-me!",
     468 + },
     469 + want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
     470 + },
     471 + }
     472 + for _, tt := range tests {
     473 + t.Run(tt.name, func(t *testing.T) {
     474 + if tt.wantErr == nil {
     475 + tt.wantErr = require.NoError
     476 + }
     477 + s, err := NewFromDirectory(tt.cfg)
     478 + tt.wantErr(t, err)
     479 + if err != nil {
     480 + return
     481 + }
     482 + assert.Equalf(t, tt.want, s.ID(), "ID()")
     483 + })
     484 + }
     485 +}
     486 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/source_win_test.go syft/source/directory_source_win_test.go
    1  -//go:build windows
    2  -// +build windows
    3  - 
    4 1  package source
    5 2   
    6 3  import (
    skipped 2 lines
    9 6   "github.com/stretchr/testify/require"
    10 7  )
    11 8   
    12  -func Test_crossPlatformExclusions(t *testing.T) {
     9 +func Test_DirectorySource_crossPlatformExclusions(t *testing.T) {
    13 10   testCases := []struct {
    14 11   desc string
    15 12   root string
    skipped 30 lines
    46 43   require.NoError(t, err)
    47 44   
    48 45   for _, f := range fns {
    49  - result := f(test.path, nil)
     46 + result := f(test.path, nil, nil)
    50 47   require.Equal(t, test.match, result)
    51 48   }
    52 49   })
    skipped 3 lines
  • ■ ■ ■ ■ ■ ■
    syft/source/exclude.go
     1 +package source
     2 + 
     3 +type ExcludeConfig struct {
     4 + Paths []string
     5 +}
     6 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/file_source.go
     1 +package source
     2 + 
     3 +import (
     4 + "fmt"
     5 + "os"
     6 + "sync"
     7 + 
     8 + "github.com/mholt/archiver/v3"
     9 + "github.com/opencontainers/go-digest"
     10 + 
     11 + "github.com/anchore/syft/internal/log"
     12 + "github.com/anchore/syft/syft/artifact"
     13 + "github.com/anchore/syft/syft/file"
     14 + "github.com/anchore/syft/syft/internal/fileresolver"
     15 +)
     16 + 
     17 +var _ Source = (*FileSource)(nil)
     18 + 
     19 +type FileConfig struct {
     20 + Path string
     21 + Exclude ExcludeConfig
     22 + Name string // ? can this be done differently?
     23 + // base??
     24 +}
     25 + 
     26 +type FileMetadata struct {
     27 + Path string
     28 +}
     29 + 
     30 +type FileSource struct {
     31 + id artifact.ID
     32 + config FileConfig
     33 + resolver *fileresolver.Directory
     34 + mutex *sync.Mutex
     35 + closer func() error
     36 + analysisPath string
     37 +}
     38 + 
     39 +func NewFromFile(cfg FileConfig) (*FileSource, error) {
     40 + fileMeta, err := os.Stat(cfg.Path)
     41 + if err != nil {
     42 + return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
     43 + }
     44 + 
     45 + if fileMeta.IsDir() {
     46 + return nil, fmt.Errorf("given path is a directory (path=%q): %w", cfg.Path, err)
     47 + }
     48 + 
     49 + analysisPath, cleanupFn := fileAnalysisPath(cfg.Path)
     50 + 
     51 + return &FileSource{
     52 + id: artifactIDFromDigest(digestOfFileContents(cfg.Path)),
     53 + config: FileConfig{},
     54 + mutex: &sync.Mutex{},
     55 + closer: cleanupFn,
     56 + analysisPath: analysisPath,
     57 + }, nil
     58 +}
     59 + 
     60 +func (s FileSource) ID() artifact.ID {
     61 + return s.id
     62 +}
     63 + 
     64 +func (s FileSource) Describe() Description {
     65 + return Description{
     66 + ID: string(s.id),
     67 + Name: s.config.Path,
     68 + Metadata: FileMetadata{
     69 + Path: s.config.Path,
     70 + },
     71 + }
     72 +}
     73 + 
     74 +func (s FileSource) FileResolver(scope Scope) (file.Resolver, error) {
     75 + s.mutex.Lock()
     76 + defer s.mutex.Unlock()
     77 + 
     78 + if s.resolver == nil {
     79 + exclusionFunctions, err := getDirectoryExclusionFunctions(s.analysisPath, s.config.Exclude.Paths)
     80 + if err != nil {
     81 + return nil, err
     82 + }
     83 + 
     84 + res, err := fileresolver.NewFromDirectory(s.analysisPath, "", exclusionFunctions...)
     85 + if err != nil {
     86 + return nil, fmt.Errorf("unable to create directory resolver: %w", err)
     87 + }
     88 + 
     89 + s.resolver = res
     90 + }
     91 + 
     92 + return s.resolver, nil
     93 +}
     94 + 
     95 +func (s FileSource) Close() error {
     96 + if s.closer == nil {
     97 + return nil
     98 + }
     99 + s.resolver = nil
     100 + return s.closer()
     101 +}
     102 + 
     103 +// fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive
     104 +// contents have been made available. A cleanup function is provided for any temp files created (if any).
     105 +func fileAnalysisPath(path string) (string, func() error) {
     106 + var analysisPath = path
     107 + var cleanupFn = func() error { return nil }
     108 + 
     109 + // if the given file is an archive (as indicated by the file extension and not MIME type) then unarchive it and
     110 + // use the contents as the source. Note: this does NOT recursively unarchive contents, only the given path is
     111 + // unarchived.
     112 + envelopedUnarchiver, err := archiver.ByExtension(path)
     113 + if unarchiver, ok := envelopedUnarchiver.(archiver.Unarchiver); err == nil && ok {
     114 + if tar, ok := unarchiver.(*archiver.Tar); ok {
     115 + // when tar files are extracted, if there are multiple entries at the same
     116 + // location, the last entry wins
     117 + // NOTE: this currently does not display any messages if an overwrite happens
     118 + tar.OverwriteExisting = true
     119 + }
     120 + unarchivedPath, tmpCleanup, err := unarchiveToTmp(path, unarchiver)
     121 + if err != nil {
     122 + log.Warnf("file could not be unarchived: %+v", err)
     123 + } else {
     124 + log.Debugf("source path is an archive")
     125 + analysisPath = unarchivedPath
     126 + }
     127 + if tmpCleanup != nil {
     128 + cleanupFn = tmpCleanup
     129 + }
     130 + }
     131 + 
     132 + return analysisPath, cleanupFn
     133 +}
     134 + 
     135 +func digestOfFileContents(path string) string {
     136 + file, err := os.Open(path)
     137 + if err != nil {
     138 + return digest.FromString(path).String()
     139 + }
     140 + defer file.Close()
     141 + di, err := digest.FromReader(file)
     142 + if err != nil {
     143 + return digest.FromString(path).String()
     144 + }
     145 + return di.String()
     146 +}
     147 + 
     148 +func unarchiveToTmp(path string, unarchiver archiver.Unarchiver) (string, func() error, error) {
     149 + tempDir, err := os.MkdirTemp("", "syft-archive-contents-")
     150 + if err != nil {
     151 + return "", func() error { return nil }, fmt.Errorf("unable to create tempdir for archive processing: %w", err)
     152 + }
     153 + 
     154 + cleanupFn := func() error {
     155 + return os.RemoveAll(tempDir)
     156 + }
     157 + 
     158 + return tempDir, cleanupFn, unarchiver.Unarchive(path, tempDir) // TODO: does not work, use v4 for io.FS support
     159 +}
     160 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/file_source_test.go
     1 +package source
     2 + 
     3 +import (
     4 + "io"
     5 + "os"
     6 + "os/exec"
     7 + "path"
     8 + "path/filepath"
     9 + "syscall"
     10 + "testing"
     11 + 
     12 + "github.com/stretchr/testify/assert"
     13 + "github.com/stretchr/testify/require"
     14 + 
     15 + "github.com/anchore/syft/syft/artifact"
     16 +)
     17 + 
     18 +func TestNewFromFile(t *testing.T) {
     19 + testCases := []struct {
     20 + desc string
     21 + input string
     22 + expString string
     23 + inputPaths []string
     24 + expRefs int
     25 + }{
     26 + {
     27 + desc: "path detected",
     28 + input: "test-fixtures/path-detected",
     29 + inputPaths: []string{"/.vimrc"},
     30 + expRefs: 1,
     31 + },
     32 + }
     33 + for _, test := range testCases {
     34 + t.Run(test.desc, func(t *testing.T) {
     35 + src, err := NewFromFile(FileConfig{
     36 + Path: test.input,
     37 + })
     38 + require.NoError(t, err)
     39 + t.Cleanup(func() {
     40 + require.NoError(t, src.Close())
     41 + })
     42 + 
     43 + assert.Equal(t, test.input, src.Describe().Metadata.(FileMetadata).Path)
     44 + 
     45 + res, err := src.FileResolver(SquashedScope)
     46 + require.NoError(t, err)
     47 + 
     48 + refs, err := res.FilesByPath(test.inputPaths...)
     49 + require.NoError(t, err)
     50 + assert.Len(t, refs, test.expRefs)
     51 + 
     52 + })
     53 + }
     54 +}
     55 + 
     56 +func TestNewFromFile_WithArchive(t *testing.T) {
     57 + testCases := []struct {
     58 + desc string
     59 + input string
     60 + expString string
     61 + inputPaths []string
     62 + expRefs int
     63 + layer2 bool
     64 + contents string
     65 + }{
     66 + {
     67 + desc: "path detected",
     68 + input: "test-fixtures/path-detected",
     69 + inputPaths: []string{"/.vimrc"},
     70 + expRefs: 1,
     71 + },
     72 + {
     73 + desc: "lest entry for duplicate paths",
     74 + input: "test-fixtures/path-detected",
     75 + inputPaths: []string{"/.vimrc"},
     76 + expRefs: 1,
     77 + layer2: true,
     78 + contents: "Another .vimrc file",
     79 + },
     80 + }
     81 + for _, test := range testCases {
     82 + t.Run(test.desc, func(t *testing.T) {
     83 + archivePath := setupArchiveTest(t, test.input, test.layer2)
     84 + 
     85 + src, err := NewFromFile(FileConfig{
     86 + Path: archivePath,
     87 + })
     88 + require.NoError(t, err)
     89 + t.Cleanup(func() {
     90 + require.NoError(t, src.Close())
     91 + })
     92 + 
     93 + assert.Equal(t, archivePath, src.Describe().Metadata.(FileMetadata).Path)
     94 + 
     95 + res, err := src.FileResolver(SquashedScope)
     96 + require.NoError(t, err)
     97 + 
     98 + refs, err := res.FilesByPath(test.inputPaths...)
     99 + require.NoError(t, err)
     100 + assert.Len(t, refs, test.expRefs)
     101 + 
     102 + if test.contents != "" {
     103 + reader, err := res.FileContentsByLocation(refs[0])
     104 + require.NoError(t, err)
     105 + 
     106 + data, err := io.ReadAll(reader)
     107 + require.NoError(t, err)
     108 + 
     109 + assert.Equal(t, test.contents, string(data))
     110 + }
     111 + 
     112 + })
     113 + }
     114 +}
     115 + 
     116 +// setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function,
     117 +// which should be called (typically deferred) by the caller, the path of the created tar archive, and an error,
     118 +// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
     119 +// (even if there's an error), and it should always be called.
     120 +func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string {
     121 + t.Helper()
     122 + 
     123 + archivePrefix, err := os.CreateTemp("", "syft-archive-TEST-")
     124 + require.NoError(t, err)
     125 + 
     126 + t.Cleanup(func() {
     127 + assert.NoError(t, os.Remove(archivePrefix.Name()))
     128 + })
     129 + 
     130 + destinationArchiveFilePath := archivePrefix.Name() + ".tar"
     131 + t.Logf("archive path: %s", destinationArchiveFilePath)
     132 + createArchive(t, sourceDirPath, destinationArchiveFilePath, layer2)
     133 + 
     134 + t.Cleanup(func() {
     135 + assert.NoError(t, os.Remove(destinationArchiveFilePath))
     136 + })
     137 + 
     138 + cwd, err := os.Getwd()
     139 + require.NoError(t, err)
     140 + 
     141 + t.Logf("running from: %s", cwd)
     142 + 
     143 + return destinationArchiveFilePath
     144 +}
     145 + 
     146 +// createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath.
     147 +func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, layer2 bool) {
     148 + t.Helper()
     149 + 
     150 + cwd, err := os.Getwd()
     151 + if err != nil {
     152 + t.Fatalf("unable to get cwd: %+v", err)
     153 + }
     154 + 
     155 + cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
     156 + cmd.Dir = filepath.Join(cwd, "test-fixtures")
     157 + 
     158 + if err := cmd.Start(); err != nil {
     159 + t.Fatalf("unable to start generate zip fixture script: %+v", err)
     160 + }
     161 + 
     162 + if err := cmd.Wait(); err != nil {
     163 + if exiterr, ok := err.(*exec.ExitError); ok {
     164 + // The program has exited with an exit code != 0
     165 + 
     166 + // This works on both Unix and Windows. Although package
     167 + // syscall is generally platform dependent, WaitStatus is
     168 + // defined for both Unix and Windows and in both cases has
     169 + // an ExitStatus() method with the same signature.
     170 + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
     171 + if status.ExitStatus() != 0 {
     172 + t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
     173 + }
     174 + }
     175 + } else {
     176 + t.Fatalf("unable to get generate fixture script result: %+v", err)
     177 + }
     178 + }
     179 + 
     180 + if layer2 {
     181 + cmd = exec.Command("tar", "-rvf", destinationArchivePath, ".")
     182 + cmd.Dir = filepath.Join(cwd, "test-fixtures", path.Base(sourceDirPath+"-2"))
     183 + if err := cmd.Start(); err != nil {
     184 + t.Fatalf("unable to start tar appending fixture script: %+v", err)
     185 + }
     186 + _ = cmd.Wait()
     187 + }
     188 +}
     189 + 
     190 +func Test_FileSource_ID(t *testing.T) {
     191 + tests := []struct {
     192 + name string
     193 + cfg FileConfig
     194 + want artifact.ID
     195 + wantErr require.ErrorAssertionFunc
     196 + }{
     197 + {
     198 + name: "empty",
     199 + cfg: FileConfig{},
     200 + wantErr: require.Error,
     201 + },
     202 + {
     203 + name: "to dir",
     204 + cfg: FileConfig{
     205 + Path: "./test-fixtures/image-simple",
     206 + },
     207 + wantErr: require.Error,
     208 + },
     209 + {
     210 + name: "with path",
     211 + cfg: FileConfig{Path: "./test-fixtures/image-simple/Dockerfile"},
     212 + want: artifact.ID("38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f"),
     213 + },
     214 + {
     215 + name: "other fields do not affect ID",
     216 + cfg: FileConfig{
     217 + Path: "test-fixtures/image-simple/Dockerfile",
     218 + Exclude: ExcludeConfig{
     219 + Paths: []string{"a", "b"},
     220 + },
     221 + Name: "name-me!",
     222 + },
     223 + want: artifact.ID("38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f"),
     224 + },
     225 + }
     226 + for _, tt := range tests {
     227 + t.Run(tt.name, func(t *testing.T) {
     228 + if tt.wantErr == nil {
     229 + tt.wantErr = require.NoError
     230 + }
     231 + s, err := NewFromFile(tt.cfg)
     232 + tt.wantErr(t, err)
     233 + if err != nil {
     234 + return
     235 + }
     236 + assert.Equalf(t, tt.want, s.ID(), "ID()")
     237 + })
     238 + }
     239 +}
     240 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/metadata.go
    1  -package source
    2  - 
    3  -// Metadata represents any static source data that helps describe "what" was cataloged.
    4  -type Metadata struct {
    5  - ID string `hash:"ignore"` // the id generated from the parent source struct
    6  - Scheme Scheme // the source data scheme type (directory or image)
    7  - ImageMetadata ImageMetadata // all image info (image only)
    8  - Path string // the root path to be cataloged (directory only)
    9  - Base string // the base path to be cataloged (directory only)
    10  - Name string
    11  -}
    12  - 
  • ■ ■ ■ ■ ■ ■
    syft/source/scheme/scheme.go
     1 +package scheme
     2 + 
     3 +import (
     4 + "fmt"
     5 + "strings"
     6 + 
     7 + "github.com/mitchellh/go-homedir"
     8 + "github.com/spf13/afero"
     9 + 
     10 + "github.com/anchore/stereoscope/pkg/image"
     11 +)
     12 + 
     13 +// Scheme represents the optional prefixed string at the beginning of a user request (e.g. "docker:").
     14 +type Scheme string
     15 + 
     16 +type sourceDetector func(string) (image.Source, string, error)
     17 + 
     18 +const (
     19 + // UnknownScheme is the default scheme
     20 + UnknownScheme Scheme = "UnknownScheme"
     21 + // DirectoryScheme indicates the source being cataloged is a directory on the root filesystem
     22 + DirectoryScheme Scheme = "DirectoryScheme"
     23 + // ContainerImageScheme indicates the source being cataloged is a container image
     24 + ContainerImageScheme Scheme = "ContainerImageScheme"
     25 + // FileScheme indicates the source being cataloged is a single file
     26 + FileScheme Scheme = "FileScheme"
     27 +)
     28 + 
     29 +var AllSchemes = []Scheme{
     30 + DirectoryScheme,
     31 + ContainerImageScheme,
     32 + FileScheme,
     33 +}
     34 + 
     35 +// Input is an object that captures the detected user input regarding source location, scheme, and provider type.
     36 +// It acts as a struct input for some source constructors.
     37 +type Input struct {
     38 + UserInput string
     39 + Scheme Scheme
     40 + ImageSource image.Source
     41 + Location string
     42 + Platform string
     43 + Name string
     44 +}
     45 + 
     46 +// Parse generates a source Input that can be used as an argument to generate a new source
     47 +// from specific providers including a registry, with an explicit name.
     48 +func Parse(userInput string, platform, name, defaultImageSource string) (*Input, error) {
     49 + fs := afero.NewOsFs()
     50 + scheme, source, location, err := detect(fs, image.DetectSource, userInput)
     51 + if err != nil {
     52 + return nil, err
     53 + }
     54 + 
     55 + if source == image.UnknownSource {
     56 + // only run for these two scheme
     57 + // only check on packages command, attest we automatically try to pull from userInput
     58 + switch scheme {
     59 + case ContainerImageScheme, UnknownScheme:
     60 + scheme = ContainerImageScheme
     61 + location = userInput
     62 + if defaultImageSource != "" {
     63 + source = parseDefaultImageSource(defaultImageSource)
     64 + } else {
     65 + source = image.DetermineDefaultImagePullSource(userInput)
     66 + }
     67 + default:
     68 + }
     69 + }
     70 + 
     71 + if scheme != ContainerImageScheme && platform != "" {
     72 + return nil, fmt.Errorf("cannot specify a platform for a non-image source")
     73 + }
     74 + 
     75 + // collect user input for downstream consumption
     76 + return &Input{
     77 + UserInput: userInput,
     78 + Scheme: scheme,
     79 + ImageSource: source,
     80 + Location: location,
     81 + Platform: platform,
     82 + Name: name,
     83 + }, nil
     84 +}
     85 + 
     86 +func detect(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, image.Source, string, error) {
     87 + switch {
     88 + case strings.HasPrefix(userInput, "dir:"):
     89 + dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:"))
     90 + if err != nil {
     91 + return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
     92 + }
     93 + return DirectoryScheme, image.UnknownSource, dirLocation, nil
     94 + 
     95 + case strings.HasPrefix(userInput, "file:"):
     96 + fileLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "file:"))
     97 + if err != nil {
     98 + return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
     99 + }
     100 + return FileScheme, image.UnknownSource, fileLocation, nil
     101 + }
     102 + 
     103 + // try the most specific sources first and move out towards more generic sources.
     104 + 
     105 + // first: let's try the image detector, which has more scheme parsing internal to stereoscope
     106 + source, imageSpec, err := imageDetector(userInput)
     107 + if err == nil && source != image.UnknownSource {
     108 + return ContainerImageScheme, source, imageSpec, nil
     109 + }
     110 + 
     111 + // next: let's try more generic sources (dir, file, etc.)
     112 + location, err := homedir.Expand(userInput)
     113 + if err != nil {
     114 + return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand potential directory path: %w", err)
     115 + }
     116 + 
     117 + fileMeta, err := fs.Stat(location)
     118 + if err != nil {
     119 + return UnknownScheme, source, "", nil
     120 + }
     121 + 
     122 + if fileMeta.IsDir() {
     123 + return DirectoryScheme, source, location, nil
     124 + }
     125 + 
     126 + return FileScheme, source, location, nil
     127 +}
     128 + 
     129 +func parseDefaultImageSource(defaultImageSource string) image.Source {
     130 + switch defaultImageSource {
     131 + case "registry":
     132 + return image.OciRegistrySource
     133 + case "docker":
     134 + return image.DockerDaemonSource
     135 + case "podman":
     136 + return image.PodmanDaemonSource
     137 + default:
     138 + return image.UnknownSource
     139 + }
     140 +}
     141 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/scheme_test.go syft/source/scheme/scheme_test.go
    1  -package source
     1 +package scheme
    2 2   
    3 3  import (
    4 4   "os"
    skipped 29 lines
    34 34   src: image.DockerDaemonSource,
    35 35   ref: "wagoodman/dive:latest",
    36 36   },
    37  - expectedScheme: ImageScheme,
     37 + expectedScheme: ContainerImageScheme,
    38 38   expectedLocation: "wagoodman/dive:latest",
    39 39   },
    40 40   {
    skipped 3 lines
    44 44   src: image.DockerDaemonSource,
    45 45   ref: "wagoodman/dive",
    46 46   },
    47  - expectedScheme: ImageScheme,
     47 + expectedScheme: ContainerImageScheme,
    48 48   expectedLocation: "wagoodman/dive",
    49 49   },
    50 50   {
    skipped 3 lines
    54 54   src: image.OciRegistrySource,
    55 55   ref: "wagoodman/dive:latest",
    56 56   },
    57  - expectedScheme: ImageScheme,
     57 + expectedScheme: ContainerImageScheme,
    58 58   expectedLocation: "wagoodman/dive:latest",
    59 59   },
    60 60   {
    skipped 3 lines
    64 64   src: image.DockerDaemonSource,
    65 65   ref: "wagoodman/dive:latest",
    66 66   },
    67  - expectedScheme: ImageScheme,
     67 + expectedScheme: ContainerImageScheme,
    68 68   expectedLocation: "wagoodman/dive:latest",
    69 69   },
    70 70   {
    skipped 3 lines
    74 74   src: image.DockerDaemonSource,
    75 75   ref: "wagoodman/dive",
    76 76   },
    77  - expectedScheme: ImageScheme,
     77 + expectedScheme: ContainerImageScheme,
    78 78   expectedLocation: "wagoodman/dive",
    79 79   },
    80 80   {
    skipped 3 lines
    84 84   src: image.DockerDaemonSource,
    85 85   ref: "latest",
    86 86   },
    87  - expectedScheme: ImageScheme,
     87 + expectedScheme: ContainerImageScheme,
    88 88   // we expected to be able to handle this case better, however, I don't see a way to do this
    89 89   // the user will need to provide more explicit input (docker:docker:latest)
    90 90   expectedLocation: "latest",
    skipped 5 lines
    96 96   src: image.DockerDaemonSource,
    97 97   ref: "docker:latest",
    98 98   },
    99  - expectedScheme: ImageScheme,
     99 + expectedScheme: ContainerImageScheme,
    100 100   // we expected to be able to handle this case better, however, I don't see a way to do this
    101 101   // the user will need to provide more explicit input (docker:docker:latest)
    102 102   expectedLocation: "docker:latest",
    skipped 5 lines
    108 108   src: image.OciTarballSource,
    109 109   ref: "some/path-to-file",
    110 110   },
    111  - expectedScheme: ImageScheme,
     111 + expectedScheme: ContainerImageScheme,
    112 112   expectedLocation: "some/path-to-file",
    113 113   },
    114 114   {
    skipped 4 lines
    119 119   ref: "some/path-to-dir",
    120 120   },
    121 121   dirs: []string{"some/path-to-dir"},
    122  - expectedScheme: ImageScheme,
     122 + expectedScheme: ContainerImageScheme,
    123 123   expectedLocation: "some/path-to-dir",
    124 124   },
    125 125   {
    skipped 14 lines
    140 140   src: image.DockerDaemonSource,
    141 141   ref: "some/path-to-dir",
    142 142   },
    143  - expectedScheme: ImageScheme,
     143 + expectedScheme: ContainerImageScheme,
    144 144   expectedLocation: "some/path-to-dir",
    145 145   },
    146 146   {
    skipped 3 lines
    150 150   src: image.PodmanDaemonSource,
    151 151   ref: "something:latest",
    152 152   },
    153  - expectedScheme: ImageScheme,
     153 + expectedScheme: ContainerImageScheme,
    154 154   expectedLocation: "something:latest",
    155 155   },
    156 156   {
    skipped 57 lines
    214 214   src: image.OciDirectorySource,
    215 215   ref: "~/some-path",
    216 216   },
    217  - expectedScheme: ImageScheme,
     217 + expectedScheme: ContainerImageScheme,
    218 218   expectedLocation: "~/some-path",
    219 219   },
    220 220   {
    skipped 67 lines
    288 288   }
    289 289   }
    290 290   
    291  - actualScheme, actualSource, actualLocation, err := DetectScheme(fs, imageDetector, test.userInput)
     291 + actualScheme, actualSource, actualLocation, err := detect(fs, imageDetector, test.userInput)
    292 292   if err != nil {
    293 293   t.Fatalf("unexpected err : %+v", err)
    294 294   }
    skipped 15 lines
  • ■ ■ ■ ■ ■ ■
    syft/source/scheme/source.go
     1 +package scheme
     2 + 
     3 +import (
     4 + "fmt"
     5 + 
     6 + "github.com/anchore/stereoscope/pkg/image"
     7 + "github.com/anchore/syft/syft/source"
     8 +)
     9 + 
     10 +// NewSource produces a Source based on userInput like dir: or image:tag
     11 +func NewSource(in Input, registryOptions *image.RegistryOptions, exclusions []string) (source.Source, error) {
     12 + var err error
     13 + var src source.Source
     14 + 
     15 + switch in.Scheme {
     16 + case FileScheme:
     17 + src, err = source.NewFromFile(
     18 + source.FileConfig{
     19 + Path: in.Location,
     20 + Exclude: source.ExcludeConfig{
     21 + Paths: exclusions,
     22 + },
     23 + Name: in.Name,
     24 + },
     25 + )
     26 + case DirectoryScheme:
     27 + src, err = source.NewFromDirectory(
     28 + source.DirectoryConfig{
     29 + Path: in.Location,
     30 + Base: in.Location,
     31 + Exclude: source.ExcludeConfig{},
     32 + Name: in.Name,
     33 + },
     34 + )
     35 + case ContainerImageScheme:
     36 + var platform *image.Platform
     37 + if in.Platform != "" {
     38 + platform, err = image.NewPlatform(in.Platform)
     39 + if err != nil {
     40 + return nil, fmt.Errorf("unable to parse platform: %w", err)
     41 + }
     42 + }
     43 + src, err = source.NewFromImage(
     44 + source.StereoscopeImageConfig{
     45 + Reference: in.Location,
     46 + From: in.ImageSource,
     47 + Platform: platform,
     48 + RegistryOptions: registryOptions,
     49 + Exclude: source.ExcludeConfig{
     50 + Paths: exclusions,
     51 + },
     52 + Name: in.Name,
     53 + },
     54 + )
     55 + default:
     56 + err = fmt.Errorf("unable to process input for scanning: %q", in.UserInput)
     57 + }
     58 + 
     59 + return src, err
     60 +}
     61 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/scheme.go
    1  -package source
    2  - 
    3  -import (
    4  - "fmt"
    5  - "strings"
    6  - 
    7  - "github.com/mitchellh/go-homedir"
    8  - "github.com/spf13/afero"
    9  - 
    10  - "github.com/anchore/stereoscope/pkg/image"
    11  -)
    12  - 
    13  -// Scheme represents the optional prefixed string at the beginning of a user request (e.g. "docker:").
    14  -type Scheme string
    15  - 
    16  -const (
    17  - // UnknownScheme is the default scheme
    18  - UnknownScheme Scheme = "UnknownScheme"
    19  - // DirectoryScheme indicates the source being cataloged is a directory on the root filesystem
    20  - DirectoryScheme Scheme = "DirectoryScheme"
    21  - // ImageScheme indicates the source being cataloged is a container image
    22  - ImageScheme Scheme = "ImageScheme"
    23  - // FileScheme indicates the source being cataloged is a single file
    24  - FileScheme Scheme = "FileScheme"
    25  -)
    26  - 
    27  -var AllSchemes = []Scheme{
    28  - DirectoryScheme,
    29  - ImageScheme,
    30  - FileScheme,
    31  -}
    32  - 
    33  -func DetectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, image.Source, string, error) {
    34  - switch {
    35  - case strings.HasPrefix(userInput, "dir:"):
    36  - dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:"))
    37  - if err != nil {
    38  - return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
    39  - }
    40  - return DirectoryScheme, image.UnknownSource, dirLocation, nil
    41  - 
    42  - case strings.HasPrefix(userInput, "file:"):
    43  - fileLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "file:"))
    44  - if err != nil {
    45  - return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
    46  - }
    47  - return FileScheme, image.UnknownSource, fileLocation, nil
    48  - }
    49  - 
    50  - // try the most specific sources first and move out towards more generic sources.
    51  - 
    52  - // first: let's try the image detector, which has more scheme parsing internal to stereoscope
    53  - source, imageSpec, err := imageDetector(userInput)
    54  - if err == nil && source != image.UnknownSource {
    55  - return ImageScheme, source, imageSpec, nil
    56  - }
    57  - 
    58  - // next: let's try more generic sources (dir, file, etc.)
    59  - location, err := homedir.Expand(userInput)
    60  - if err != nil {
    61  - return UnknownScheme, image.UnknownSource, "", fmt.Errorf("unable to expand potential directory path: %w", err)
    62  - }
    63  - 
    64  - fileMeta, err := fs.Stat(location)
    65  - if err != nil {
    66  - return UnknownScheme, source, "", nil
    67  - }
    68  - 
    69  - if fileMeta.IsDir() {
    70  - return DirectoryScheme, source, location, nil
    71  - }
    72  - 
    73  - return FileScheme, source, location, nil
    74  -}
    75  - 
  • ■ ■ ■ ■ ■ ■
    syft/source/scope.go
    skipped 6 lines
    7 7   
    8 8  const (
    9 9   // UnknownScope is the default scope
    10  - UnknownScope Scope = "UnknownScope"
     10 + UnknownScope Scope = ""
    11 11   // SquashedScope indicates to only catalog content visible from the squashed filesystem representation (what can be seen only within the container at runtime)
    12 12   SquashedScope Scope = "Squashed"
    13  - // AllLayersScope indicates to catalog content on all layers, irregardless if it is visible from the container at runtime.
     13 + // AllLayersScope indicates to catalog content on all layers, regardless if it is visible from the container at runtime.
    14 14   AllLayersScope Scope = "AllLayers"
    15 15  )
    16 16   
    skipped 21 lines
  • ■ ■ ■ ■ ■
    syft/source/source.go
    skipped 5 lines
    6 6  package source
    7 7   
    8 8  import (
    9  - "context"
    10  - "fmt"
    11  - "os"
    12  - "path/filepath"
    13  - "strings"
    14  - "sync"
     9 + "errors"
     10 + "io"
    15 11   
    16  - "github.com/bmatcuk/doublestar/v4"
    17  - "github.com/mholt/archiver/v3"
    18  - digest "github.com/opencontainers/go-digest"
    19  - "github.com/spf13/afero"
    20  - 
    21  - "github.com/anchore/stereoscope"
    22  - "github.com/anchore/stereoscope/pkg/image"
    23  - "github.com/anchore/syft/internal/log"
    24 12   "github.com/anchore/syft/syft/artifact"
    25 13   "github.com/anchore/syft/syft/file"
    26  - "github.com/anchore/syft/syft/internal/fileresolver"
    27 14  )
    28 15   
    29  -// Source is an object that captures the data source to be cataloged, configuration, and a specific resolver used
    30  -// in cataloging (based on the data source and configuration)
    31  -type Source struct {
    32  - id artifact.ID `hash:"ignore"`
    33  - Image *image.Image `hash:"ignore"` // the image object to be cataloged (image only)
    34  - Metadata Metadata
    35  - directoryResolver *fileresolver.Directory `hash:"ignore"`
    36  - path string
    37  - base string
    38  - mutex *sync.Mutex
    39  - Exclusions []string `hash:"ignore"`
     16 +type Source interface {
     17 + artifact.Identifiable
     18 + FileResolver(Scope) (file.Resolver, error)
     19 + Describe() Description
     20 + io.Closer
    40 21  }
    41 22   
    42  -// Input is an object that captures the detected user input regarding source location, scheme, and provider type.
    43  -// It acts as a struct input for some source constructors.
    44  -type Input struct {
    45  - UserInput string
    46  - Scheme Scheme
    47  - ImageSource image.Source
    48  - Location string
    49  - Platform string
    50  - Name string
     23 +type emptySource struct {
     24 + description Description
    51 25  }
    52 26   
    53  -// ParseInput generates a source Input that can be used as an argument to generate a new source
    54  -// from specific providers including a registry.
    55  -func ParseInput(userInput string, platform string) (*Input, error) {
    56  - return ParseInputWithName(userInput, platform, "", "")
     27 +func (e emptySource) ID() artifact.ID {
     28 + return artifact.ID(e.description.ID)
    57 29  }
    58 30   
    59  -// ParseInputWithName generates a source Input that can be used as an argument to generate a new source
    60  -// from specific providers including a registry, with an explicit name.
    61  -func ParseInputWithName(userInput string, platform, name, defaultImageSource string) (*Input, error) {
    62  - fs := afero.NewOsFs()
    63  - scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput)
    64  - if err != nil {
    65  - return nil, err
    66  - }
    67  - 
    68  - if source == image.UnknownSource {
    69  - // only run for these two scheme
    70  - // only check on packages command, attest we automatically try to pull from userInput
    71  - switch scheme {
    72  - case ImageScheme, UnknownScheme:
    73  - scheme = ImageScheme
    74  - location = userInput
    75  - if defaultImageSource != "" {
    76  - source = parseDefaultImageSource(defaultImageSource)
    77  - } else {
    78  - imagePullSource := image.DetermineDefaultImagePullSource(userInput)
    79  - source = imagePullSource
    80  - }
    81  - if location == "" {
    82  - location = userInput
    83  - }
    84  - default:
    85  - }
    86  - }
    87  - 
    88  - if scheme != ImageScheme && platform != "" {
    89  - return nil, fmt.Errorf("cannot specify a platform for a non-image source")
    90  - }
    91  - 
    92  - // collect user input for downstream consumption
    93  - return &Input{
    94  - UserInput: userInput,
    95  - Scheme: scheme,
    96  - ImageSource: source,
    97  - Location: location,
    98  - Platform: platform,
    99  - Name: name,
    100  - }, nil
     31 +func (e emptySource) FileResolver(scope Scope) (file.Resolver, error) {
     32 + return nil, errors.New("no file resolver available for description-only source")
    101 33  }
    102 34   
    103  -func parseDefaultImageSource(defaultImageSource string) image.Source {
    104  - switch defaultImageSource {
    105  - case "registry":
    106  - return image.OciRegistrySource
    107  - case "docker":
    108  - return image.DockerDaemonSource
    109  - case "podman":
    110  - return image.PodmanDaemonSource
    111  - default:
    112  - return image.UnknownSource
    113  - }
    114  -}
    115  - 
    116  -type sourceDetector func(string) (image.Source, string, error)
    117  - 
    118  -func NewFromRegistry(in Input, registryOptions *image.RegistryOptions, exclusions []string) (*Source, func(), error) {
    119  - source, cleanupFn, err := generateImageSource(in, registryOptions)
    120  - if source != nil {
    121  - source.Exclusions = exclusions
    122  - }
    123  - return source, cleanupFn, err
     35 +func (e emptySource) Describe() Description {
     36 + return e.description
    124 37  }
    125 38   
    126  -// New produces a Source based on userInput like dir: or image:tag
    127  -func New(in Input, registryOptions *image.RegistryOptions, exclusions []string) (*Source, func(), error) {
    128  - var err error
    129  - fs := afero.NewOsFs()
    130  - var source *Source
    131  - cleanupFn := func() {}
    132  - 
    133  - switch in.Scheme {
    134  - case FileScheme:
    135  - source, cleanupFn, err = generateFileSource(fs, in)
    136  - case DirectoryScheme:
    137  - source, cleanupFn, err = generateDirectorySource(fs, in)
    138  - case ImageScheme:
    139  - source, cleanupFn, err = generateImageSource(in, registryOptions)
    140  - default:
    141  - err = fmt.Errorf("unable to process input for scanning: %q", in.UserInput)
    142  - }
    143  - 
    144  - if err == nil {
    145  - source.Exclusions = exclusions
    146  - }
    147  - 
    148  - return source, cleanupFn, err
     39 +func (e emptySource) Close() error {
     40 + return nil // no-op
    149 41  }
    150 42   
    151  -func generateImageSource(in Input, registryOptions *image.RegistryOptions) (*Source, func(), error) {
    152  - img, cleanup, err := getImageWithRetryStrategy(in, registryOptions)
    153  - if err != nil || img == nil {
    154  - return nil, cleanup, fmt.Errorf("could not fetch image %q: %w", in.Location, err)
     43 +func FromDescription(d Description) Source {
     44 + return &emptySource{
     45 + description: d,
    155 46   }
    156  - 
    157  - s, err := NewFromImageWithName(img, in.Location, in.Name)
    158  - if err != nil {
    159  - return nil, cleanup, fmt.Errorf("could not populate source with image: %w", err)
    160  - }
    161  - 
    162  - return &s, cleanup, nil
    163  -}
    164  - 
    165  -func parseScheme(userInput string) string {
    166  - parts := strings.SplitN(userInput, ":", 2)
    167  - if len(parts) < 2 {
    168  - return ""
    169  - }
    170  - 
    171  - return parts[0]
    172  -}
    173  - 
    174  -func getImageWithRetryStrategy(in Input, registryOptions *image.RegistryOptions) (*image.Image, func(), error) {
    175  - ctx := context.TODO()
    176  - 
    177  - var opts []stereoscope.Option
    178  - if registryOptions != nil {
    179  - opts = append(opts, stereoscope.WithRegistryOptions(*registryOptions))
    180  - }
    181  - 
    182  - if in.Platform != "" {
    183  - opts = append(opts, stereoscope.WithPlatform(in.Platform))
    184  - }
    185  - 
    186  - img, err := stereoscope.GetImageFromSource(ctx, in.Location, in.ImageSource, opts...)
    187  - cleanup := func() {
    188  - if err := img.Cleanup(); err != nil {
    189  - log.Warnf("unable to cleanup image=%q: %w", in.UserInput, err)
    190  - }
    191  - }
    192  - if err == nil {
    193  - // Success on the first try!
    194  - return img, cleanup, nil
    195  - }
    196  - 
    197  - scheme := parseScheme(in.UserInput)
    198  - if !(scheme == "docker" || scheme == "registry") {
    199  - // Image retrieval failed, and we shouldn't retry it. It's most likely that the
    200  - // user _did_ intend the parsed scheme, but there was a legitimate failure with
    201  - // using the scheme to load the image. Alert the user to this failure, so they
    202  - // can fix the problem.
    203  - return nil, nil, err
    204  - }
    205  - 
    206  - // Maybe the user wanted "docker" or "registry" to refer to an _image name_
    207  - // (e.g. "docker:latest"), not a scheme. We'll retry image retrieval with this
    208  - // alternative interpretation, in an attempt to avoid unnecessary user friction.
    209  - 
    210  - log.Warnf(
    211  - "scheme %q specified, but it coincides with a common image name; re-examining user input %q"+
    212  - " without scheme parsing because image retrieval using scheme parsing was unsuccessful: %v",
    213  - scheme,
    214  - in.UserInput,
    215  - err,
    216  - )
    217  - 
    218  - // We need to determine the image source again, such that this determination
    219  - // doesn't take scheme parsing into account.
    220  - in.ImageSource = image.DetermineDefaultImagePullSource(in.UserInput)
    221  - img, userInputErr := stereoscope.GetImageFromSource(ctx, in.UserInput, in.ImageSource, opts...)
    222  - cleanup = func() {
    223  - if err := img.Cleanup(); err != nil {
    224  - log.Warnf("unable to cleanup image=%q: %w", in.UserInput, err)
    225  - }
    226  - }
    227  - if userInputErr != nil {
    228  - // Image retrieval failed on both tries, we will want to return both errors.
    229  - return nil, nil, fmt.Errorf(
    230  - "scheme %q specified; "+
    231  - "image retrieval using scheme parsing (%s) was unsuccessful: %v; "+
    232  - "image retrieval without scheme parsing (%s) was unsuccessful: %v",
    233  - scheme,
    234  - in.Location,
    235  - err,
    236  - in.UserInput,
    237  - userInputErr,
    238  - )
    239  - }
    240  - 
    241  - return img, cleanup, nil
    242  -}
    243  - 
    244  -func generateDirectorySource(fs afero.Fs, in Input) (*Source, func(), error) {
    245  - fileMeta, err := fs.Stat(in.Location)
    246  - if err != nil {
    247  - return nil, func() {}, fmt.Errorf("unable to stat dir=%q: %w", in.Location, err)
    248  - }
    249  - 
    250  - if !fileMeta.IsDir() {
    251  - return nil, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", in.Location, err)
    252  - }
    253  - 
    254  - s, err := NewFromDirectoryWithName(in.Location, in.Name)
    255  - if err != nil {
    256  - return nil, func() {}, fmt.Errorf("could not populate source from path=%q: %w", in.Location, err)
    257  - }
    258  - 
    259  - return &s, func() {}, nil
    260  -}
    261  - 
    262  -func generateFileSource(fs afero.Fs, in Input) (*Source, func(), error) {
    263  - fileMeta, err := fs.Stat(in.Location)
    264  - if err != nil {
    265  - return nil, func() {}, fmt.Errorf("unable to stat dir=%q: %w", in.Location, err)
    266  - }
    267  - 
    268  - if fileMeta.IsDir() {
    269  - return nil, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", in.Location, err)
    270  - }
    271  - 
    272  - s, cleanupFn := NewFromFileWithName(in.Location, in.Name)
    273  - 
    274  - return &s, cleanupFn, nil
    275  -}
    276  - 
    277  -// NewFromDirectory creates a new source object tailored to catalog a given filesystem directory recursively.
    278  -func NewFromDirectory(path string) (Source, error) {
    279  - return NewFromDirectoryWithName(path, "")
    280  -}
    281  - 
    282  -// NewFromDirectory creates a new source object tailored to catalog a given filesystem directory recursively.
    283  -func NewFromDirectoryRoot(path string) (Source, error) {
    284  - return NewFromDirectoryRootWithName(path, "")
    285  -}
    286  - 
    287  -// NewFromDirectoryWithName creates a new source object tailored to catalog a given filesystem directory recursively, with an explicitly provided name.
    288  -func NewFromDirectoryWithName(path string, name string) (Source, error) {
    289  - s := Source{
    290  - mutex: &sync.Mutex{},
    291  - Metadata: Metadata{
    292  - Name: name,
    293  - Scheme: DirectoryScheme,
    294  - Path: path,
    295  - },
    296  - path: path,
    297  - }
    298  - s.SetID()
    299  - return s, nil
    300  -}
    301  - 
    302  -// NewFromDirectoryRootWithName creates a new source object tailored to catalog a given filesystem directory recursively, with an explicitly provided name.
    303  -func NewFromDirectoryRootWithName(path string, name string) (Source, error) {
    304  - s := Source{
    305  - mutex: &sync.Mutex{},
    306  - Metadata: Metadata{
    307  - Name: name,
    308  - Scheme: DirectoryScheme,
    309  - Path: path,
    310  - Base: path,
    311  - },
    312  - path: path,
    313  - base: path,
    314  - }
    315  - s.SetID()
    316  - return s, nil
    317  -}
    318  - 
    319  -// NewFromFile creates a new source object tailored to catalog a file.
    320  -func NewFromFile(path string) (Source, func()) {
    321  - return NewFromFileWithName(path, "")
    322  -}
    323  - 
    324  -// NewFromFileWithName creates a new source object tailored to catalog a file, with an explicitly provided name.
    325  -func NewFromFileWithName(path string, name string) (Source, func()) {
    326  - analysisPath, cleanupFn := fileAnalysisPath(path)
    327  - 
    328  - s := Source{
    329  - mutex: &sync.Mutex{},
    330  - Metadata: Metadata{
    331  - Name: name,
    332  - Scheme: FileScheme,
    333  - Path: path,
    334  - },
    335  - path: analysisPath,
    336  - }
    337  - 
    338  - s.SetID()
    339  - return s, cleanupFn
    340  -}
    341  - 
    342  -// fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive
    343  -// contents have been made available. A cleanup function is provided for any temp files created (if any).
    344  -func fileAnalysisPath(path string) (string, func()) {
    345  - var analysisPath = path
    346  - var cleanupFn = func() {}
    347  - 
    348  - // if the given file is an archive (as indicated by the file extension and not MIME type) then unarchive it and
    349  - // use the contents as the source. Note: this does NOT recursively unarchive contents, only the given path is
    350  - // unarchived.
    351  - envelopedUnarchiver, err := archiver.ByExtension(path)
    352  - if unarchiver, ok := envelopedUnarchiver.(archiver.Unarchiver); err == nil && ok {
    353  - if tar, ok := unarchiver.(*archiver.Tar); ok {
    354  - // when tar files are extracted, if there are multiple entries at the same
    355  - // location, the last entry wins
    356  - // NOTE: this currently does not display any messages if an overwrite happens
    357  - tar.OverwriteExisting = true
    358  - }
    359  - unarchivedPath, tmpCleanup, err := unarchiveToTmp(path, unarchiver)
    360  - if err != nil {
    361  - log.Warnf("file could not be unarchived: %+v", err)
    362  - } else {
    363  - log.Debugf("source path is an archive")
    364  - analysisPath = unarchivedPath
    365  - }
    366  - if tmpCleanup != nil {
    367  - cleanupFn = tmpCleanup
    368  - }
    369  - }
    370  - 
    371  - return analysisPath, cleanupFn
    372  -}
    373  - 
    374  -// NewFromImage creates a new source object tailored to catalog a given container image, relative to the
    375  -// option given (e.g. all-layers, squashed, etc)
    376  -func NewFromImage(img *image.Image, userImageStr string) (Source, error) {
    377  - return NewFromImageWithName(img, userImageStr, "")
    378  -}
    379  - 
    380  -// NewFromImageWithName creates a new source object tailored to catalog a given container image, relative to the
    381  -// option given (e.g. all-layers, squashed, etc), with an explicit name.
    382  -func NewFromImageWithName(img *image.Image, userImageStr string, name string) (Source, error) {
    383  - if img == nil {
    384  - return Source{}, fmt.Errorf("no image given")
    385  - }
    386  - 
    387  - s := Source{
    388  - Image: img,
    389  - Metadata: Metadata{
    390  - Name: name,
    391  - Scheme: ImageScheme,
    392  - ImageMetadata: NewImageMetadata(img, userImageStr),
    393  - },
    394  - }
    395  - s.SetID()
    396  - return s, nil
    397  -}
    398  - 
    399  -func (s *Source) ID() artifact.ID {
    400  - if s.id == "" {
    401  - s.SetID()
    402  - }
    403  - return s.id
    404  -}
    405  - 
    406  -func (s *Source) SetID() {
    407  - var d string
    408  - switch s.Metadata.Scheme {
    409  - case DirectoryScheme:
    410  - d = digest.FromString(s.Metadata.Path).String()
    411  - case FileScheme:
    412  - // attempt to use the digest of the contents of the file as the ID
    413  - file, err := os.Open(s.Metadata.Path)
    414  - if err != nil {
    415  - d = digest.FromString(s.Metadata.Path).String()
    416  - break
    417  - }
    418  - defer file.Close()
    419  - di, err := digest.FromReader(file)
    420  - if err != nil {
    421  - d = digest.FromString(s.Metadata.Path).String()
    422  - break
    423  - }
    424  - d = di.String()
    425  - case ImageScheme:
    426  - manifestDigest := digest.FromBytes(s.Metadata.ImageMetadata.RawManifest).String()
    427  - if manifestDigest != "" {
    428  - d = manifestDigest
    429  - break
    430  - }
    431  - 
    432  - // calcuate chain ID for image sources where manifestDigest is not available
    433  - // https://github.com/opencontainers/image-spec/blob/main/config.md#layer-chainid
    434  - d = calculateChainID(s.Metadata.ImageMetadata.Layers)
    435  - if d == "" {
    436  - // TODO what happens here if image has no layers?
    437  - // Is this case possible
    438  - d = digest.FromString(s.Metadata.ImageMetadata.UserInput).String()
    439  - }
    440  - default: // for UnknownScheme we hash the struct
    441  - id, _ := artifact.IDByHash(s)
    442  - d = string(id)
    443  - }
    444  - 
    445  - s.id = artifact.ID(strings.TrimPrefix(d, "sha256:"))
    446  - s.Metadata.ID = strings.TrimPrefix(d, "sha256:")
    447  -}
    448  - 
    449  -func calculateChainID(lm []LayerMetadata) string {
    450  - if len(lm) < 1 {
    451  - return ""
    452  - }
    453  - 
    454  - // DiffID(L0) = digest of layer 0
    455  - // https://github.com/anchore/stereoscope/blob/1b1b744a919964f38d14e1416fb3f25221b761ce/pkg/image/layer_metadata.go#L19-L32
    456  - chainID := lm[0].Digest
    457  - id := chain(chainID, lm[1:])
    458  - 
    459  - return id
    460  -}
    461  - 
    462  -func chain(chainID string, layers []LayerMetadata) string {
    463  - if len(layers) < 1 {
    464  - return chainID
    465  - }
    466  - 
    467  - chainID = digest.FromString(layers[0].Digest + " " + chainID).String()
    468  - return chain(chainID, layers[1:])
    469  -}
    470  - 
    471  -func (s *Source) FileResolver(scope Scope) (file.Resolver, error) {
    472  - switch s.Metadata.Scheme {
    473  - case DirectoryScheme, FileScheme:
    474  - s.mutex.Lock()
    475  - defer s.mutex.Unlock()
    476  - if s.directoryResolver == nil {
    477  - exclusionFunctions, err := getDirectoryExclusionFunctions(s.path, s.Exclusions)
    478  - if err != nil {
    479  - return nil, err
    480  - }
    481  - res, err := fileresolver.NewFromDirectory(s.path, s.base, exclusionFunctions...)
    482  - if err != nil {
    483  - return nil, fmt.Errorf("unable to create directory resolver: %w", err)
    484  - }
    485  - s.directoryResolver = res
    486  - }
    487  - return s.directoryResolver, nil
    488  - case ImageScheme:
    489  - var res file.Resolver
    490  - var err error
    491  - switch scope {
    492  - case SquashedScope:
    493  - res, err = fileresolver.NewFromContainerImageSquash(s.Image)
    494  - case AllLayersScope:
    495  - res, err = fileresolver.NewFromContainerImageAllLayers(s.Image)
    496  - default:
    497  - return nil, fmt.Errorf("bad image scope provided: %+v", scope)
    498  - }
    499  - if err != nil {
    500  - return nil, err
    501  - }
    502  - // image tree contains all paths, so we filter out the excluded entries afterwards
    503  - if len(s.Exclusions) > 0 {
    504  - res = fileresolver.NewExcluding(res, getImageExclusionFunction(s.Exclusions))
    505  - }
    506  - return res, nil
    507  - }
    508  - return nil, fmt.Errorf("unable to determine FilePathResolver with current scheme=%q", s.Metadata.Scheme)
    509  -}
    510  - 
    511  -func unarchiveToTmp(path string, unarchiver archiver.Unarchiver) (string, func(), error) {
    512  - tempDir, err := os.MkdirTemp("", "syft-archive-contents-")
    513  - if err != nil {
    514  - return "", func() {}, fmt.Errorf("unable to create tempdir for archive processing: %w", err)
    515  - }
    516  - 
    517  - cleanupFn := func() {
    518  - if err := os.RemoveAll(tempDir); err != nil {
    519  - log.Warnf("unable to cleanup archive tempdir: %+v", err)
    520  - }
    521  - }
    522  - 
    523  - return tempDir, cleanupFn, unarchiver.Unarchive(path, tempDir)
    524  -}
    525  - 
    526  -func getImageExclusionFunction(exclusions []string) func(string) bool {
    527  - if len(exclusions) == 0 {
    528  - return nil
    529  - }
    530  - // add subpath exclusions
    531  - for _, exclusion := range exclusions {
    532  - exclusions = append(exclusions, exclusion+"/**")
    533  - }
    534  - return func(path string) bool {
    535  - for _, exclusion := range exclusions {
    536  - matches, err := doublestar.Match(exclusion, path)
    537  - if err != nil {
    538  - return false
    539  - }
    540  - if matches {
    541  - return true
    542  - }
    543  - }
    544  - return false
    545  - }
    546  -}
    547  - 
    548  -func getDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, error) {
    549  - if len(exclusions) == 0 {
    550  - return nil, nil
    551  - }
    552  - 
    553  - // this is what Directory.indexTree is doing to get the absolute path:
    554  - root, err := filepath.Abs(root)
    555  - if err != nil {
    556  - return nil, err
    557  - }
    558  - 
    559  - // this handles Windows file paths by converting them to C:/something/else format
    560  - root = filepath.ToSlash(root)
    561  - 
    562  - if !strings.HasSuffix(root, "/") {
    563  - root += "/"
    564  - }
    565  - 
    566  - var errors []string
    567  - for idx, exclusion := range exclusions {
    568  - // check exclusions for supported paths, these are all relative to the "scan root"
    569  - if strings.HasPrefix(exclusion, "./") || strings.HasPrefix(exclusion, "*/") || strings.HasPrefix(exclusion, "**/") {
    570  - exclusion = strings.TrimPrefix(exclusion, "./")
    571  - exclusions[idx] = root + exclusion
    572  - } else {
    573  - errors = append(errors, exclusion)
    574  - }
    575  - }
    576  - 
    577  - if errors != nil {
    578  - return nil, fmt.Errorf("invalid exclusion pattern(s): '%s' (must start with one of: './', '*/', or '**/')", strings.Join(errors, "', '"))
    579  - }
    580  - 
    581  - return []fileresolver.PathIndexVisitor{
    582  - func(path string, info os.FileInfo, _ error) error {
    583  - for _, exclusion := range exclusions {
    584  - // this is required to handle Windows filepaths
    585  - path = filepath.ToSlash(path)
    586  - matches, err := doublestar.Match(exclusion, path)
    587  - if err != nil {
    588  - return nil
    589  - }
    590  - if matches {
    591  - if info != nil && info.IsDir() {
    592  - return filepath.SkipDir
    593  - }
    594  - return fileresolver.ErrSkipPath
    595  - }
    596  - }
    597  - return nil
    598  - },
    599  - }, nil
    600 47  }
    601 48   
  • ■ ■ ■ ■ ■ ■
    syft/source/source_test.go
    1  -//go:build !windows
    2  -// +build !windows
    3  - 
    4  -package source
    5  - 
    6  -import (
    7  - "io"
    8  - "io/fs"
    9  - "os"
    10  - "os/exec"
    11  - "path"
    12  - "path/filepath"
    13  - "sort"
    14  - "strings"
    15  - "syscall"
    16  - "testing"
    17  - "time"
    18  - 
    19  - "github.com/google/go-cmp/cmp"
    20  - "github.com/stretchr/testify/assert"
    21  - "github.com/stretchr/testify/require"
    22  - 
    23  - "github.com/anchore/stereoscope/pkg/image"
    24  - "github.com/anchore/stereoscope/pkg/imagetest"
    25  - "github.com/anchore/syft/syft/artifact"
    26  - "github.com/anchore/syft/syft/internal/fileresolver"
    27  -)
    28  - 
    29  -func TestParseInput(t *testing.T) {
    30  - tests := []struct {
    31  - name string
    32  - input string
    33  - platform string
    34  - expected Scheme
    35  - errFn require.ErrorAssertionFunc
    36  - }{
    37  - {
    38  - name: "ParseInput parses a file input",
    39  - input: "test-fixtures/image-simple/file-1.txt",
    40  - expected: FileScheme,
    41  - },
    42  - {
    43  - name: "errors out when using platform for non-image scheme",
    44  - input: "test-fixtures/image-simple/file-1.txt",
    45  - platform: "arm64",
    46  - errFn: require.Error,
    47  - },
    48  - }
    49  - 
    50  - for _, test := range tests {
    51  - t.Run(test.name, func(t *testing.T) {
    52  - if test.errFn == nil {
    53  - test.errFn = require.NoError
    54  - }
    55  - sourceInput, err := ParseInput(test.input, test.platform)
    56  - test.errFn(t, err)
    57  - if test.expected != "" {
    58  - require.NotNil(t, sourceInput)
    59  - assert.Equal(t, sourceInput.Scheme, test.expected)
    60  - }
    61  - })
    62  - }
    63  -}
    64  - 
    65  -func TestNewFromImageFails(t *testing.T) {
    66  - t.Run("no image given", func(t *testing.T) {
    67  - _, err := NewFromImage(nil, "")
    68  - if err == nil {
    69  - t.Errorf("expected an error condition but none was given")
    70  - }
    71  - })
    72  -}
    73  - 
    74  -func TestSetID(t *testing.T) {
    75  - layer := image.NewLayer(nil)
    76  - layer.Metadata = image.LayerMetadata{
    77  - Digest: "sha256:6f4fb385d4e698647bf2a450749dfbb7bc2831ec9a730ef4046c78c08d468e89",
    78  - }
    79  - img := image.Image{
    80  - Layers: []*image.Layer{layer},
    81  - }
    82  - 
    83  - tests := []struct {
    84  - name string
    85  - input *Source
    86  - expected artifact.ID
    87  - }{
    88  - {
    89  - name: "source.SetID sets the ID for FileScheme",
    90  - input: &Source{
    91  - Metadata: Metadata{
    92  - Scheme: FileScheme,
    93  - Path: "test-fixtures/image-simple/file-1.txt",
    94  - },
    95  - },
    96  - expected: artifact.ID("55096713247489add592ce977637be868497132b36d1e294a3831925ec64319a"),
    97  - },
    98  - {
    99  - name: "source.SetID sets the ID for ImageScheme",
    100  - input: &Source{
    101  - Image: &img,
    102  - Metadata: Metadata{
    103  - Scheme: ImageScheme,
    104  - },
    105  - },
    106  - expected: artifact.ID("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
    107  - },
    108  - {
    109  - name: "source.SetID sets the ID for DirectoryScheme",
    110  - input: &Source{
    111  - Image: &img,
    112  - Metadata: Metadata{
    113  - Scheme: DirectoryScheme,
    114  - Path: "test-fixtures/image-simple",
    115  - },
    116  - },
    117  - expected: artifact.ID("91db61e5e0ae097ef764796ce85e442a93f2a03e5313d4c7307e9b413f62e8c4"),
    118  - },
    119  - {
    120  - name: "source.SetID sets the ID for UnknownScheme",
    121  - input: &Source{
    122  - Image: &img,
    123  - Metadata: Metadata{
    124  - Scheme: UnknownScheme,
    125  - Path: "test-fixtures/image-simple",
    126  - },
    127  - },
    128  - expected: artifact.ID("1b0dc351e6577b01"),
    129  - },
    130  - }
    131  - 
    132  - for _, test := range tests {
    133  - t.Run(test.name, func(t *testing.T) {
    134  - test.input.SetID()
    135  - assert.Equal(t, test.expected, test.input.ID())
    136  - })
    137  - }
    138  -}
    139  - 
    140  -func TestNewFromImage(t *testing.T) {
    141  - layer := image.NewLayer(nil)
    142  - img := image.Image{
    143  - Layers: []*image.Layer{layer},
    144  - }
    145  - 
    146  - t.Run("create a new source object from image", func(t *testing.T) {
    147  - _, err := NewFromImage(&img, "")
    148  - if err != nil {
    149  - t.Errorf("unexpected error when creating a new Locations from img: %+v", err)
    150  - }
    151  - })
    152  -}
    153  - 
    154  -func TestNewFromDirectory(t *testing.T) {
    155  - testCases := []struct {
    156  - desc string
    157  - input string
    158  - expString string
    159  - inputPaths []string
    160  - expectedRefs int
    161  - expectedErr bool
    162  - }{
    163  - {
    164  - desc: "no paths exist",
    165  - input: "foobar/",
    166  - inputPaths: []string{"/opt/", "/other"},
    167  - expectedErr: true,
    168  - },
    169  - {
    170  - desc: "path detected",
    171  - input: "test-fixtures",
    172  - inputPaths: []string{"path-detected/.vimrc"},
    173  - expectedRefs: 1,
    174  - },
    175  - {
    176  - desc: "directory ignored",
    177  - input: "test-fixtures",
    178  - inputPaths: []string{"path-detected"},
    179  - expectedRefs: 0,
    180  - },
    181  - {
    182  - desc: "no files-by-path detected",
    183  - input: "test-fixtures",
    184  - inputPaths: []string{"no-path-detected"},
    185  - expectedRefs: 0,
    186  - },
    187  - }
    188  - for _, test := range testCases {
    189  - t.Run(test.desc, func(t *testing.T) {
    190  - src, err := NewFromDirectory(test.input)
    191  - require.NoError(t, err)
    192  - assert.Equal(t, test.input, src.Metadata.Path)
    193  - 
    194  - res, err := src.FileResolver(SquashedScope)
    195  - if test.expectedErr {
    196  - if err == nil {
    197  - t.Fatal("expected an error when making the resolver but got none")
    198  - }
    199  - return
    200  - } else {
    201  - require.NoError(t, err)
    202  - }
    203  - 
    204  - refs, err := res.FilesByPath(test.inputPaths...)
    205  - if err != nil {
    206  - t.Errorf("FilesByPath call produced an error: %+v", err)
    207  - }
    208  - if len(refs) != test.expectedRefs {
    209  - t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expectedRefs)
    210  - 
    211  - }
    212  - 
    213  - })
    214  - }
    215  -}
    216  - 
    217  -func TestNewFromFile(t *testing.T) {
    218  - testCases := []struct {
    219  - desc string
    220  - input string
    221  - expString string
    222  - inputPaths []string
    223  - expRefs int
    224  - }{
    225  - {
    226  - desc: "path detected",
    227  - input: "test-fixtures/path-detected",
    228  - inputPaths: []string{"/.vimrc"},
    229  - expRefs: 1,
    230  - },
    231  - }
    232  - for _, test := range testCases {
    233  - t.Run(test.desc, func(t *testing.T) {
    234  - src, cleanup := NewFromFile(test.input)
    235  - if cleanup != nil {
    236  - t.Cleanup(cleanup)
    237  - }
    238  - 
    239  - assert.Equal(t, test.input, src.Metadata.Path)
    240  - assert.Equal(t, src.Metadata.Path, src.path)
    241  - 
    242  - res, err := src.FileResolver(SquashedScope)
    243  - require.NoError(t, err)
    244  - 
    245  - refs, err := res.FilesByPath(test.inputPaths...)
    246  - require.NoError(t, err)
    247  - assert.Len(t, refs, test.expRefs)
    248  - 
    249  - })
    250  - }
    251  -}
    252  - 
    253  -func TestNewFromFile_WithArchive(t *testing.T) {
    254  - testCases := []struct {
    255  - desc string
    256  - input string
    257  - expString string
    258  - inputPaths []string
    259  - expRefs int
    260  - layer2 bool
    261  - contents string
    262  - }{
    263  - {
    264  - desc: "path detected",
    265  - input: "test-fixtures/path-detected",
    266  - inputPaths: []string{"/.vimrc"},
    267  - expRefs: 1,
    268  - },
    269  - {
    270  - desc: "lest entry for duplicate paths",
    271  - input: "test-fixtures/path-detected",
    272  - inputPaths: []string{"/.vimrc"},
    273  - expRefs: 1,
    274  - layer2: true,
    275  - contents: "Another .vimrc file",
    276  - },
    277  - }
    278  - for _, test := range testCases {
    279  - t.Run(test.desc, func(t *testing.T) {
    280  - archivePath := setupArchiveTest(t, test.input, test.layer2)
    281  - 
    282  - src, cleanup := NewFromFile(archivePath)
    283  - if cleanup != nil {
    284  - t.Cleanup(cleanup)
    285  - }
    286  - 
    287  - assert.Equal(t, archivePath, src.Metadata.Path)
    288  - assert.NotEqual(t, src.Metadata.Path, src.path)
    289  - 
    290  - res, err := src.FileResolver(SquashedScope)
    291  - require.NoError(t, err)
    292  - 
    293  - refs, err := res.FilesByPath(test.inputPaths...)
    294  - require.NoError(t, err)
    295  - assert.Len(t, refs, test.expRefs)
    296  - 
    297  - if test.contents != "" {
    298  - reader, err := res.FileContentsByLocation(refs[0])
    299  - require.NoError(t, err)
    300  - 
    301  - data, err := io.ReadAll(reader)
    302  - require.NoError(t, err)
    303  - 
    304  - assert.Equal(t, test.contents, string(data))
    305  - }
    306  - 
    307  - })
    308  - }
    309  -}
    310  - 
    311  -func TestNewFromDirectoryShared(t *testing.T) {
    312  - testCases := []struct {
    313  - desc string
    314  - input string
    315  - expString string
    316  - notExist string
    317  - inputPaths []string
    318  - expRefs int
    319  - }{
    320  - {
    321  - desc: "path detected",
    322  - input: "test-fixtures",
    323  - notExist: "foobar/",
    324  - inputPaths: []string{"path-detected/.vimrc"},
    325  - expRefs: 1,
    326  - },
    327  - {
    328  - desc: "directory ignored",
    329  - input: "test-fixtures",
    330  - notExist: "foobar/",
    331  - inputPaths: []string{"path-detected"},
    332  - expRefs: 0,
    333  - },
    334  - {
    335  - desc: "no files-by-path detected",
    336  - input: "test-fixtures",
    337  - notExist: "foobar/",
    338  - inputPaths: []string{"no-path-detected"},
    339  - expRefs: 0,
    340  - },
    341  - }
    342  - for _, test := range testCases {
    343  - t.Run(test.desc, func(t *testing.T) {
    344  - src, err := NewFromDirectory(test.input)
    345  - 
    346  - if err != nil {
    347  - t.Errorf("could not create NewDirScope: %+v", err)
    348  - }
    349  - if src.Metadata.Path != test.input {
    350  - t.Errorf("mismatched stringer: '%s' != '%s'", src.Metadata.Path, test.input)
    351  - }
    352  - 
    353  - _, err = src.FileResolver(SquashedScope)
    354  - assert.NoError(t, err)
    355  - 
    356  - src.Metadata.Path = test.notExist
    357  - resolver, err := src.FileResolver(SquashedScope)
    358  - assert.NoError(t, err)
    359  - 
    360  - refs, err := resolver.FilesByPath(test.inputPaths...)
    361  - if err != nil {
    362  - t.Errorf("FilesByPath call produced an error: %+v", err)
    363  - }
    364  - if len(refs) != test.expRefs {
    365  - t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expRefs)
    366  - 
    367  - }
    368  - 
    369  - })
    370  - }
    371  -}
    372  - 
    373  -func TestFilesByPathDoesNotExist(t *testing.T) {
    374  - testCases := []struct {
    375  - desc string
    376  - input string
    377  - path string
    378  - expected string
    379  - }{
    380  - {
    381  - input: "test-fixtures/path-detected",
    382  - desc: "path does not exist",
    383  - path: "foo",
    384  - },
    385  - }
    386  - for _, test := range testCases {
    387  - t.Run(test.desc, func(t *testing.T) {
    388  - src, err := NewFromDirectory(test.input)
    389  - if err != nil {
    390  - t.Errorf("could not create NewDirScope: %+v", err)
    391  - }
    392  - res, err := src.FileResolver(SquashedScope)
    393  - if err != nil {
    394  - t.Errorf("could not get resolver error: %+v", err)
    395  - }
    396  - refs, err := res.FilesByPath(test.path)
    397  - if err != nil {
    398  - t.Errorf("could not get file references from path: %s, %v", test.path, err)
    399  - }
    400  - 
    401  - if len(refs) != 0 {
    402  - t.Errorf("didnt' expect a ref, but got: %d", len(refs))
    403  - }
    404  - 
    405  - })
    406  - }
    407  -}
    408  - 
    409  -func TestFilesByGlob(t *testing.T) {
    410  - testCases := []struct {
    411  - desc string
    412  - input string
    413  - glob string
    414  - expected int
    415  - }{
    416  - {
    417  - input: "test-fixtures",
    418  - desc: "no matches",
    419  - glob: "bar/foo",
    420  - expected: 0,
    421  - },
    422  - {
    423  - input: "test-fixtures/path-detected",
    424  - desc: "a single match",
    425  - glob: "**/*vimrc",
    426  - expected: 1,
    427  - },
    428  - {
    429  - input: "test-fixtures/path-detected",
    430  - desc: "multiple matches",
    431  - glob: "**",
    432  - expected: 2,
    433  - },
    434  - }
    435  - for _, test := range testCases {
    436  - t.Run(test.desc, func(t *testing.T) {
    437  - src, err := NewFromDirectory(test.input)
    438  - if err != nil {
    439  - t.Errorf("could not create NewDirScope: %+v", err)
    440  - }
    441  - res, err := src.FileResolver(SquashedScope)
    442  - if err != nil {
    443  - t.Errorf("could not get resolver error: %+v", err)
    444  - }
    445  - contents, err := res.FilesByGlob(test.glob)
    446  - if err != nil {
    447  - t.Errorf("could not get files by glob: %s+v", err)
    448  - }
    449  - if len(contents) != test.expected {
    450  - t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected)
    451  - }
    452  - 
    453  - })
    454  - }
    455  -}
    456  - 
    457  -func TestDirectoryExclusions(t *testing.T) {
    458  - testCases := []struct {
    459  - desc string
    460  - input string
    461  - glob string
    462  - expected []string
    463  - exclusions []string
    464  - err bool
    465  - }{
    466  - {
    467  - input: "test-fixtures/system_paths",
    468  - desc: "exclude everything",
    469  - glob: "**",
    470  - expected: nil,
    471  - exclusions: []string{"**/*"},
    472  - },
    473  - {
    474  - input: "test-fixtures/image-simple",
    475  - desc: "a single path excluded",
    476  - glob: "**",
    477  - expected: []string{
    478  - "Dockerfile",
    479  - "file-1.txt",
    480  - "file-2.txt",
    481  - },
    482  - exclusions: []string{"**/target/**"},
    483  - },
    484  - {
    485  - input: "test-fixtures/image-simple",
    486  - desc: "exclude explicit directory relative to the root",
    487  - glob: "**",
    488  - expected: []string{
    489  - "Dockerfile",
    490  - "file-1.txt",
    491  - "file-2.txt",
    492  - //"target/really/nested/file-3.txt", // explicitly skipped
    493  - },
    494  - exclusions: []string{"./target"},
    495  - },
    496  - {
    497  - input: "test-fixtures/image-simple",
    498  - desc: "exclude explicit file relative to the root",
    499  - glob: "**",
    500  - expected: []string{
    501  - "Dockerfile",
    502  - //"file-1.txt", // explicitly skipped
    503  - "file-2.txt",
    504  - "target/really/nested/file-3.txt",
    505  - },
    506  - exclusions: []string{"./file-1.txt"},
    507  - },
    508  - {
    509  - input: "test-fixtures/image-simple",
    510  - desc: "exclude wildcard relative to the root",
    511  - glob: "**",
    512  - expected: []string{
    513  - "Dockerfile",
    514  - //"file-1.txt", // explicitly skipped
    515  - //"file-2.txt", // explicitly skipped
    516  - "target/really/nested/file-3.txt",
    517  - },
    518  - exclusions: []string{"./*.txt"},
    519  - },
    520  - {
    521  - input: "test-fixtures/image-simple",
    522  - desc: "exclude files deeper",
    523  - glob: "**",
    524  - expected: []string{
    525  - "Dockerfile",
    526  - "file-1.txt",
    527  - "file-2.txt",
    528  - //"target/really/nested/file-3.txt", // explicitly skipped
    529  - },
    530  - exclusions: []string{"**/really/**"},
    531  - },
    532  - {
    533  - input: "test-fixtures/image-simple",
    534  - desc: "files excluded with extension",
    535  - glob: "**",
    536  - expected: []string{
    537  - "Dockerfile",
    538  - //"file-1.txt", // explicitly skipped
    539  - //"file-2.txt", // explicitly skipped
    540  - //"target/really/nested/file-3.txt", // explicitly skipped
    541  - },
    542  - exclusions: []string{"**/*.txt"},
    543  - },
    544  - {
    545  - input: "test-fixtures/image-simple",
    546  - desc: "keep files with different extensions",
    547  - glob: "**",
    548  - expected: []string{
    549  - "Dockerfile",
    550  - "file-1.txt",
    551  - "file-2.txt",
    552  - "target/really/nested/file-3.txt",
    553  - },
    554  - exclusions: []string{"**/target/**/*.jar"},
    555  - },
    556  - {
    557  - input: "test-fixtures/path-detected",
    558  - desc: "file directly excluded",
    559  - glob: "**",
    560  - expected: []string{
    561  - ".vimrc",
    562  - },
    563  - exclusions: []string{"**/empty"},
    564  - },
    565  - {
    566  - input: "test-fixtures/path-detected",
    567  - desc: "pattern error containing **/",
    568  - glob: "**",
    569  - expected: []string{
    570  - ".vimrc",
    571  - },
    572  - exclusions: []string{"/**/empty"},
    573  - err: true,
    574  - },
    575  - {
    576  - input: "test-fixtures/path-detected",
    577  - desc: "pattern error incorrect start",
    578  - glob: "**",
    579  - expected: []string{
    580  - ".vimrc",
    581  - },
    582  - exclusions: []string{"empty"},
    583  - err: true,
    584  - },
    585  - {
    586  - input: "test-fixtures/path-detected",
    587  - desc: "pattern error starting with /",
    588  - glob: "**",
    589  - expected: []string{
    590  - ".vimrc",
    591  - },
    592  - exclusions: []string{"/empty"},
    593  - err: true,
    594  - },
    595  - }
    596  - registryOpts := &image.RegistryOptions{}
    597  - for _, test := range testCases {
    598  - t.Run(test.desc, func(t *testing.T) {
    599  - sourceInput, err := ParseInput("dir:"+test.input, "")
    600  - require.NoError(t, err)
    601  - src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
    602  - defer fn()
    603  - 
    604  - if test.err {
    605  - _, err = src.FileResolver(SquashedScope)
    606  - if err == nil {
    607  - t.Errorf("expected an error for patterns: %s", strings.Join(test.exclusions, " or "))
    608  - }
    609  - return
    610  - }
    611  - 
    612  - if err != nil {
    613  - t.Errorf("could not create NewDirScope: %+v", err)
    614  - }
    615  - res, err := src.FileResolver(SquashedScope)
    616  - if err != nil {
    617  - t.Errorf("could not get resolver error: %+v", err)
    618  - }
    619  - locations, err := res.FilesByGlob(test.glob)
    620  - if err != nil {
    621  - t.Errorf("could not get files by glob: %s+v", err)
    622  - }
    623  - var actual []string
    624  - for _, l := range locations {
    625  - actual = append(actual, l.RealPath)
    626  - }
    627  - 
    628  - sort.Strings(test.expected)
    629  - sort.Strings(actual)
    630  - 
    631  - assert.Equal(t, test.expected, actual, "diff \n"+cmp.Diff(test.expected, actual))
    632  - })
    633  - }
    634  -}
    635  - 
    636  -func TestImageExclusions(t *testing.T) {
    637  - testCases := []struct {
    638  - desc string
    639  - input string
    640  - glob string
    641  - expected int
    642  - exclusions []string
    643  - }{
    644  - // NOTE: in the Dockerfile, /target is moved to /, which makes /really a top-level dir
    645  - {
    646  - input: "image-simple",
    647  - desc: "a single path excluded",
    648  - glob: "**",
    649  - expected: 2,
    650  - exclusions: []string{"/really/**"},
    651  - },
    652  - {
    653  - input: "image-simple",
    654  - desc: "a directly referenced directory is excluded",
    655  - glob: "**",
    656  - expected: 2,
    657  - exclusions: []string{"/really"},
    658  - },
    659  - {
    660  - input: "image-simple",
    661  - desc: "a partial directory is not excluded",
    662  - glob: "**",
    663  - expected: 3,
    664  - exclusions: []string{"/reall"},
    665  - },
    666  - {
    667  - input: "image-simple",
    668  - desc: "exclude files deeper",
    669  - glob: "**",
    670  - expected: 2,
    671  - exclusions: []string{"**/nested/**"},
    672  - },
    673  - {
    674  - input: "image-simple",
    675  - desc: "files excluded with extension",
    676  - glob: "**",
    677  - expected: 2,
    678  - exclusions: []string{"**/*1.txt"},
    679  - },
    680  - {
    681  - input: "image-simple",
    682  - desc: "keep files with different extensions",
    683  - glob: "**",
    684  - expected: 3,
    685  - exclusions: []string{"**/target/**/*.jar"},
    686  - },
    687  - {
    688  - input: "image-simple",
    689  - desc: "file directly excluded",
    690  - glob: "**",
    691  - expected: 2,
    692  - exclusions: []string{"**/somefile-1.txt"}, // file-1 renamed to somefile-1 in Dockerfile
    693  - },
    694  - }
    695  - registryOpts := &image.RegistryOptions{}
    696  - for _, test := range testCases {
    697  - t.Run(test.desc, func(t *testing.T) {
    698  - archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input)
    699  - sourceInput, err := ParseInput(archiveLocation, "")
    700  - require.NoError(t, err)
    701  - src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
    702  - defer fn()
    703  - 
    704  - if err != nil {
    705  - t.Errorf("could not create NewDirScope: %+v", err)
    706  - }
    707  - res, err := src.FileResolver(SquashedScope)
    708  - if err != nil {
    709  - t.Errorf("could not get resolver error: %+v", err)
    710  - }
    711  - contents, err := res.FilesByGlob(test.glob)
    712  - if err != nil {
    713  - t.Errorf("could not get files by glob: %s+v", err)
    714  - }
    715  - if len(contents) != test.expected {
    716  - t.Errorf("wrong number of files after exclusions (%s): %d != %d", test.glob, len(contents), test.expected)
    717  - }
    718  - })
    719  - }
    720  -}
    721  - 
    722  -type dummyInfo struct {
    723  - isDir bool
    724  -}
    725  - 
    726  -func (d dummyInfo) Name() string {
    727  - //TODO implement me
    728  - panic("implement me")
    729  -}
    730  - 
    731  -func (d dummyInfo) Size() int64 {
    732  - //TODO implement me
    733  - panic("implement me")
    734  -}
    735  - 
    736  -func (d dummyInfo) Mode() fs.FileMode {
    737  - //TODO implement me
    738  - panic("implement me")
    739  -}
    740  - 
    741  -func (d dummyInfo) ModTime() time.Time {
    742  - //TODO implement me
    743  - panic("implement me")
    744  -}
    745  - 
    746  -func (d dummyInfo) IsDir() bool {
    747  - return d.isDir
    748  -}
    749  - 
    750  -func (d dummyInfo) Sys() any {
    751  - //TODO implement me
    752  - panic("implement me")
    753  -}
    754  - 
    755  -func Test_crossPlatformExclusions(t *testing.T) {
    756  - testCases := []struct {
    757  - desc string
    758  - root string
    759  - path string
    760  - finfo os.FileInfo
    761  - exclude string
    762  - walkHint error
    763  - }{
    764  - {
    765  - desc: "directory exclusion",
    766  - root: "/",
    767  - path: "/usr/var/lib",
    768  - exclude: "**/var/lib",
    769  - finfo: dummyInfo{isDir: true},
    770  - walkHint: fs.SkipDir,
    771  - },
    772  - {
    773  - desc: "no file info",
    774  - root: "/",
    775  - path: "/usr/var/lib",
    776  - exclude: "**/var/lib",
    777  - walkHint: fileresolver.ErrSkipPath,
    778  - },
    779  - // linux specific tests...
    780  - {
    781  - desc: "linux doublestar",
    782  - root: "/usr",
    783  - path: "/usr/var/lib/etc.txt",
    784  - exclude: "**/*.txt",
    785  - finfo: dummyInfo{isDir: false},
    786  - walkHint: fileresolver.ErrSkipPath,
    787  - },
    788  - {
    789  - desc: "linux relative",
    790  - root: "/usr/var/lib",
    791  - path: "/usr/var/lib/etc.txt",
    792  - exclude: "./*.txt",
    793  - finfo: dummyInfo{isDir: false},
    794  - 
    795  - walkHint: fileresolver.ErrSkipPath,
    796  - },
    797  - {
    798  - desc: "linux one level",
    799  - root: "/usr",
    800  - path: "/usr/var/lib/etc.txt",
    801  - exclude: "*/*.txt",
    802  - finfo: dummyInfo{isDir: false},
    803  - walkHint: nil,
    804  - },
    805  - // NOTE: since these tests will run in linux and macOS, the windows paths will be
    806  - // considered relative if they do not start with a forward slash and paths with backslashes
    807  - // won't be modified by the filepath.ToSlash call, so these are emulating the result of
    808  - // filepath.ToSlash usage
    809  - 
    810  - // windows specific tests...
    811  - {
    812  - desc: "windows doublestar",
    813  - root: "/C:/User/stuff",
    814  - path: "/C:/User/stuff/thing.txt",
    815  - exclude: "**/*.txt",
    816  - finfo: dummyInfo{isDir: false},
    817  - walkHint: fileresolver.ErrSkipPath,
    818  - },
    819  - {
    820  - desc: "windows relative",
    821  - root: "/C:/User/stuff",
    822  - path: "/C:/User/stuff/thing.txt",
    823  - exclude: "./*.txt",
    824  - finfo: dummyInfo{isDir: false},
    825  - walkHint: fileresolver.ErrSkipPath,
    826  - },
    827  - {
    828  - desc: "windows one level",
    829  - root: "/C:/User/stuff",
    830  - path: "/C:/User/stuff/thing.txt",
    831  - exclude: "*/*.txt",
    832  - finfo: dummyInfo{isDir: false},
    833  - walkHint: nil,
    834  - },
    835  - }
    836  - 
    837  - for _, test := range testCases {
    838  - t.Run(test.desc, func(t *testing.T) {
    839  - fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude})
    840  - require.NoError(t, err)
    841  - 
    842  - for _, f := range fns {
    843  - result := f(test.path, test.finfo, nil)
    844  - require.Equal(t, test.walkHint, result)
    845  - }
    846  - })
    847  - }
    848  -}
    849  - 
    850  -// createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath.
    851  -func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, layer2 bool) {
    852  - t.Helper()
    853  - 
    854  - cwd, err := os.Getwd()
    855  - if err != nil {
    856  - t.Fatalf("unable to get cwd: %+v", err)
    857  - }
    858  - 
    859  - cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
    860  - cmd.Dir = filepath.Join(cwd, "test-fixtures")
    861  - 
    862  - if err := cmd.Start(); err != nil {
    863  - t.Fatalf("unable to start generate zip fixture script: %+v", err)
    864  - }
    865  - 
    866  - if err := cmd.Wait(); err != nil {
    867  - if exiterr, ok := err.(*exec.ExitError); ok {
    868  - // The program has exited with an exit code != 0
    869  - 
    870  - // This works on both Unix and Windows. Although package
    871  - // syscall is generally platform dependent, WaitStatus is
    872  - // defined for both Unix and Windows and in both cases has
    873  - // an ExitStatus() method with the same signature.
    874  - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
    875  - if status.ExitStatus() != 0 {
    876  - t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
    877  - }
    878  - }
    879  - } else {
    880  - t.Fatalf("unable to get generate fixture script result: %+v", err)
    881  - }
    882  - }
    883  - 
    884  - if layer2 {
    885  - cmd = exec.Command("tar", "-rvf", destinationArchivePath, ".")
    886  - cmd.Dir = filepath.Join(cwd, "test-fixtures", path.Base(sourceDirPath+"-2"))
    887  - if err := cmd.Start(); err != nil {
    888  - t.Fatalf("unable to start tar appending fixture script: %+v", err)
    889  - }
    890  - _ = cmd.Wait()
    891  - }
    892  -}
    893  - 
    894  -// setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function,
    895  -// which should be called (typically deferred) by the caller, the path of the created tar archive, and an error,
    896  -// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
    897  -// (even if there's an error), and it should always be called.
    898  -func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string {
    899  - t.Helper()
    900  - 
    901  - archivePrefix, err := os.CreateTemp("", "syft-archive-TEST-")
    902  - require.NoError(t, err)
    903  - 
    904  - t.Cleanup(
    905  - assertNoError(t,
    906  - func() error {
    907  - return os.Remove(archivePrefix.Name())
    908  - },
    909  - ),
    910  - )
    911  - 
    912  - destinationArchiveFilePath := archivePrefix.Name() + ".tar"
    913  - t.Logf("archive path: %s", destinationArchiveFilePath)
    914  - createArchive(t, sourceDirPath, destinationArchiveFilePath, layer2)
    915  - 
    916  - t.Cleanup(
    917  - assertNoError(t,
    918  - func() error {
    919  - return os.Remove(destinationArchiveFilePath)
    920  - },
    921  - ),
    922  - )
    923  - 
    924  - cwd, err := os.Getwd()
    925  - require.NoError(t, err)
    926  - 
    927  - t.Logf("running from: %s", cwd)
    928  - 
    929  - return destinationArchiveFilePath
    930  -}
    931  - 
    932  -func assertNoError(t testing.TB, fn func() error) func() {
    933  - return func() {
    934  - assert.NoError(t, fn())
    935  - }
    936  -}
    937  - 
  • ■ ■ ■ ■ ■ ■
    syft/source/steroscope_image_source.go
     1 +package source
     2 + 
     3 +import (
     4 + "context"
     5 + "fmt"
     6 + 
     7 + "github.com/bmatcuk/doublestar/v4"
     8 + "github.com/opencontainers/go-digest"
     9 + 
     10 + "github.com/anchore/stereoscope"
     11 + "github.com/anchore/stereoscope/pkg/image"
     12 + "github.com/anchore/syft/syft/artifact"
     13 + "github.com/anchore/syft/syft/file"
     14 + "github.com/anchore/syft/syft/internal/fileresolver"
     15 +)
     16 + 
     17 +var _ Source = (*StereoscopeImageSource)(nil)
     18 + 
     19 +type StereoscopeImageConfig struct {
     20 + Reference string
     21 + From image.Source
     22 + Platform *image.Platform
     23 + RegistryOptions *image.RegistryOptions // TODO: takes platform? as string?
     24 + Exclude ExcludeConfig
     25 + Name string // ? can this be done differently?
     26 +}
     27 + 
     28 +type StereoscopeImageSource struct {
     29 + id artifact.ID
     30 + config StereoscopeImageConfig
     31 + image *image.Image
     32 + metadata ImageMetadata
     33 +}
     34 + 
     35 +func NewFromImage(cfg StereoscopeImageConfig) (*StereoscopeImageSource, error) {
     36 + ctx := context.TODO()
     37 + 
     38 + var opts []stereoscope.Option
     39 + if cfg.RegistryOptions != nil {
     40 + opts = append(opts, stereoscope.WithRegistryOptions(*cfg.RegistryOptions))
     41 + }
     42 + 
     43 + if cfg.Platform != nil {
     44 + opts = append(opts, stereoscope.WithPlatform(cfg.Platform.String()))
     45 + }
     46 + 
     47 + img, err := stereoscope.GetImageFromSource(ctx, cfg.Reference, cfg.From, opts...)
     48 + if err != nil {
     49 + return nil, fmt.Errorf("unable to load image: %w", err)
     50 + }
     51 + 
     52 + metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
     53 + 
     54 + return &StereoscopeImageSource{
     55 + id: artifactIDFromStereoscopeImage(metadata),
     56 + config: cfg,
     57 + image: img,
     58 + metadata: metadata,
     59 + }, nil
     60 +}
     61 + 
     62 +func (s StereoscopeImageSource) ID() artifact.ID {
     63 + return s.id
     64 +}
     65 + 
     66 +func (s StereoscopeImageSource) Describe() Description {
     67 + return Description{
     68 + ID: string(s.id),
     69 + Name: s.config.Reference,
     70 + Metadata: s.metadata,
     71 + }
     72 +}
     73 + 
     74 +func (s StereoscopeImageSource) FileResolver(scope Scope) (file.Resolver, error) {
     75 + var res file.Resolver
     76 + var err error
     77 + 
     78 + switch scope {
     79 + case SquashedScope:
     80 + res, err = fileresolver.NewFromContainerImageSquash(s.image)
     81 + case AllLayersScope:
     82 + res, err = fileresolver.NewFromContainerImageAllLayers(s.image)
     83 + default:
     84 + return nil, fmt.Errorf("bad image scope provided: %+v", scope)
     85 + }
     86 + 
     87 + if err != nil {
     88 + return nil, err
     89 + }
     90 + 
     91 + // image tree contains all paths, so we filter out the excluded entries afterward
     92 + if len(s.config.Exclude.Paths) > 0 {
     93 + res = fileresolver.NewExcludingDecorator(res, getImageExclusionFunction(s.config.Exclude.Paths))
     94 + }
     95 + 
     96 + return res, nil
     97 +}
     98 + 
     99 +func (s StereoscopeImageSource) Close() error {
     100 + if s.image == nil {
     101 + return nil
     102 + }
     103 + return s.image.Cleanup()
     104 +}
     105 + 
     106 +func imageMetadataFromStereoscopeImage(img *image.Image, reference string) ImageMetadata {
     107 + 
     108 + tags := make([]string, len(img.Metadata.Tags))
     109 + for idx, tag := range img.Metadata.Tags {
     110 + tags[idx] = tag.String()
     111 + }
     112 + 
     113 + layers := make([]LayerMetadata, len(img.Layers))
     114 + for _, l := range img.Layers {
     115 + layers = append(layers,
     116 + LayerMetadata{
     117 + MediaType: string(l.Metadata.MediaType),
     118 + Digest: l.Metadata.Digest,
     119 + Size: l.Metadata.Size,
     120 + },
     121 + )
     122 + }
     123 + 
     124 + return ImageMetadata{
     125 + ID: img.Metadata.ID,
     126 + UserInput: reference,
     127 + ManifestDigest: img.Metadata.ManifestDigest,
     128 + Size: img.Metadata.Size,
     129 + MediaType: string(img.Metadata.MediaType),
     130 + Tags: tags,
     131 + Layers: layers,
     132 + RawConfig: img.Metadata.RawConfig,
     133 + RawManifest: img.Metadata.RawManifest,
     134 + RepoDigests: img.Metadata.RepoDigests,
     135 + Architecture: img.Metadata.Architecture,
     136 + Variant: img.Metadata.Variant,
     137 + OS: img.Metadata.OS,
     138 + }
     139 +}
     140 + 
     141 +func artifactIDFromStereoscopeImage(metadata ImageMetadata) artifact.ID {
     142 + var input string
     143 + 
     144 + if len(metadata.RawManifest) > 0 {
     145 + input = digest.FromBytes(metadata.RawManifest).String()
     146 + } else {
     147 + // calculate chain ID for image sources where manifestDigest is not available
     148 + // https://github.com/opencontainers/image-spec/blob/main/config.md#layer-chainid
     149 + input = calculateChainID(metadata.Layers)
     150 + if input == "" {
     151 + // TODO what happens here if image has no layers?
     152 + // is this case possible?
     153 + input = digest.FromString(metadata.UserInput).String()
     154 + }
     155 + 
     156 + }
     157 + 
     158 + return artifactIDFromDigest(input)
     159 +}
     160 + 
     161 +func calculateChainID(lm []LayerMetadata) string {
     162 + if len(lm) < 1 {
     163 + return ""
     164 + }
     165 + 
     166 + // DiffID(L0) = digest of layer 0
     167 + // https://github.com/anchore/stereoscope/blob/1b1b744a919964f38d14e1416fb3f25221b761ce/pkg/image/layer_metadata.go#L19-L32
     168 + chainID := lm[0].Digest
     169 + id := chain(chainID, lm[1:])
     170 + 
     171 + return id
     172 +}
     173 + 
     174 +func chain(chainID string, layers []LayerMetadata) string {
     175 + if len(layers) < 1 {
     176 + return chainID
     177 + }
     178 + 
     179 + chainID = digest.FromString(layers[0].Digest + " " + chainID).String()
     180 + return chain(chainID, layers[1:])
     181 +}
     182 + 
     183 +func getImageExclusionFunction(exclusions []string) func(string) bool {
     184 + if len(exclusions) == 0 {
     185 + return nil
     186 + }
     187 + // add subpath exclusions
     188 + for _, exclusion := range exclusions {
     189 + exclusions = append(exclusions, exclusion+"/**")
     190 + }
     191 + return func(path string) bool {
     192 + for _, exclusion := range exclusions {
     193 + matches, err := doublestar.Match(exclusion, path)
     194 + if err != nil {
     195 + return false
     196 + }
     197 + if matches {
     198 + return true
     199 + }
     200 + }
     201 + return false
     202 + }
     203 +}
     204 + 
  • ■ ■ ■ ■ ■ ■
    syft/source/steroscope_image_source_test.go
     1 +package source
     2 + 
     3 +import (
     4 + "crypto/sha256"
     5 + "fmt"
     6 + "strings"
     7 + "testing"
     8 + 
     9 + "github.com/stretchr/testify/assert"
     10 + "github.com/stretchr/testify/require"
     11 + 
     12 + "github.com/anchore/stereoscope/pkg/image"
     13 + "github.com/anchore/stereoscope/pkg/imagetest"
     14 + "github.com/anchore/syft/syft/artifact"
     15 +)
     16 + 
     17 +//func TestNewFromImage(t *testing.T) {
     18 +// layer := image.NewLayer(nil)
     19 +// img := image.Image{
     20 +// Layers: []*image.Layer{layer},
     21 +// }
     22 +//
     23 +// t.Run("create a new source object from image", func(t *testing.T) {
     24 +// _, err := NewFromImage(StereoscopeImageConfig{
     25 +// Reference: "",
     26 +// From: 0,
     27 +// Platform: nil,
     28 +// RegistryOptions: nil,
     29 +// Exclude: ExcludeConfig{},
     30 +// Name: "",
     31 +// })
     32 +// if err != nil {
     33 +// t.Errorf("unexpected error when creating a new Locations from img: %+v", err)
     34 +// }
     35 +// })
     36 +//}
     37 + 
     38 +func Test_StereoscopeImage_Exclusions(t *testing.T) {
     39 + testCases := []struct {
     40 + desc string
     41 + input string
     42 + glob string
     43 + expected int
     44 + exclusions []string
     45 + }{
     46 + // NOTE: in the Dockerfile, /target is moved to /, which makes /really a top-level dir
     47 + {
     48 + input: "image-simple",
     49 + desc: "a single path excluded",
     50 + glob: "**",
     51 + expected: 2,
     52 + exclusions: []string{"/really/**"},
     53 + },
     54 + {
     55 + input: "image-simple",
     56 + desc: "a directly referenced directory is excluded",
     57 + glob: "**",
     58 + expected: 2,
     59 + exclusions: []string{"/really"},
     60 + },
     61 + {
     62 + input: "image-simple",
     63 + desc: "a partial directory is not excluded",
     64 + glob: "**",
     65 + expected: 3,
     66 + exclusions: []string{"/reall"},
     67 + },
     68 + {
     69 + input: "image-simple",
     70 + desc: "exclude files deeper",
     71 + glob: "**",
     72 + expected: 2,
     73 + exclusions: []string{"**/nested/**"},
     74 + },
     75 + {
     76 + input: "image-simple",
     77 + desc: "files excluded with extension",
     78 + glob: "**",
     79 + expected: 2,
     80 + exclusions: []string{"**/*1.txt"},
     81 + },
     82 + {
     83 + input: "image-simple",
     84 + desc: "keep files with different extensions",
     85 + glob: "**",
     86 + expected: 3,
     87 + exclusions: []string{"**/target/**/*.jar"},
     88 + },
     89 + {
     90 + input: "image-simple",
     91 + desc: "file directly excluded",
     92 + glob: "**",
     93 + expected: 2,
     94 + exclusions: []string{"**/somefile-1.txt"}, // file-1 renamed to somefile-1 in Dockerfile
     95 + },
     96 + }
     97 + 
     98 + for _, test := range testCases {
     99 + t.Run(test.desc, func(t *testing.T) {
     100 + src, err := NewFromImage(StereoscopeImageConfig{
     101 + Reference: imagetest.PrepareFixtureImage(t, "docker-archive", test.input),
     102 + From: image.DockerTarballSource,
     103 + Exclude: ExcludeConfig{
     104 + Paths: test.exclusions,
     105 + },
     106 + })
     107 + 
     108 + require.NoError(t, err)
     109 + t.Cleanup(func() {
     110 + require.NoError(t, src.Close())
     111 + })
     112 + 
     113 + res, err := src.FileResolver(SquashedScope)
     114 + require.NoError(t, err)
     115 + 
     116 + contents, err := res.FilesByGlob(test.glob)
     117 + require.NoError(t, err)
     118 + 
     119 + assert.Len(t, test.expected, len(contents))
     120 + })
     121 + }
     122 +}
     123 + 
     124 +func Test_StereoscopeImageSource_ID(t *testing.T) {
     125 + tests := []struct {
     126 + name string
     127 + metadata ImageMetadata
     128 + want artifact.ID
     129 + }{
     130 + {
     131 + name: "use raw manifest over chain ID or user input",
     132 + metadata: ImageMetadata{
     133 + UserInput: "user-input",
     134 + Layers: []LayerMetadata{
     135 + {
     136 + Digest: "a",
     137 + },
     138 + {
     139 + Digest: "b",
     140 + },
     141 + {
     142 + Digest: "c",
     143 + },
     144 + },
     145 + RawManifest: []byte("raw-manifest"),
     146 + },
     147 + want: func() artifact.ID {
     148 + hasher := sha256.New()
     149 + hasher.Write([]byte("raw-manifest"))
     150 + return artifact.ID(fmt.Sprintf("%x", hasher.Sum(nil)))
     151 + }(),
     152 + },
     153 + {
     154 + name: "use chain ID over user input",
     155 + metadata: ImageMetadata{
     156 + //UserInput: "user-input",
     157 + Layers: []LayerMetadata{
     158 + {
     159 + Digest: "a",
     160 + },
     161 + {
     162 + Digest: "b",
     163 + },
     164 + {
     165 + Digest: "c",
     166 + },
     167 + },
     168 + },
     169 + want: func() artifact.ID {
     170 + metadata := []LayerMetadata{
     171 + {
     172 + Digest: "a",
     173 + },
     174 + {
     175 + Digest: "b",
     176 + },
     177 + {
     178 + Digest: "c",
     179 + },
     180 + }
     181 + return artifact.ID(strings.TrimPrefix(calculateChainID(metadata), "sha256:"))
     182 + }(),
     183 + },
     184 + {
     185 + name: "use user input last",
     186 + metadata: ImageMetadata{
     187 + UserInput: "user-input",
     188 + },
     189 + want: func() artifact.ID {
     190 + hasher := sha256.New()
     191 + hasher.Write([]byte("user-input"))
     192 + return artifact.ID(fmt.Sprintf("%x", hasher.Sum(nil)))
     193 + }(),
     194 + },
     195 + }
     196 + for _, tt := range tests {
     197 + t.Run(tt.name, func(t *testing.T) {
     198 + assert.Equal(t, tt.want, artifactIDFromStereoscopeImage(tt.metadata))
     199 + })
     200 + }
     201 +}
     202 + 
Please wait...
Page is in error, reload to recover