github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/syftjson/to_syft_model_test.go (about)

     1  package syftjson
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	stereoFile "github.com/anchore/stereoscope/pkg/file"
    11  	"github.com/anchore/syft/syft/artifact"
    12  	"github.com/anchore/syft/syft/file"
    13  	"github.com/anchore/syft/syft/formats/syftjson/model"
    14  	"github.com/anchore/syft/syft/internal/sourcemetadata"
    15  	"github.com/anchore/syft/syft/pkg"
    16  	"github.com/anchore/syft/syft/sbom"
    17  	"github.com/anchore/syft/syft/source"
    18  )
    19  
    20  func Test_toSyftSourceData(t *testing.T) {
    21  	tracker := sourcemetadata.NewCompletionTester(t)
    22  
    23  	tests := []struct {
    24  		name     string
    25  		src      model.Source
    26  		expected *source.Description
    27  	}{
    28  		{
    29  			name: "directory",
    30  			src: model.Source{
    31  				ID:      "the-id",
    32  				Name:    "some-name",
    33  				Version: "some-version",
    34  				Type:    "directory",
    35  				Metadata: source.DirectorySourceMetadata{
    36  					Path: "some/path",
    37  					Base: "some/base",
    38  				},
    39  			},
    40  			expected: &source.Description{
    41  				ID:      "the-id",
    42  				Name:    "some-name",
    43  				Version: "some-version",
    44  				Metadata: source.DirectorySourceMetadata{
    45  					Path: "some/path",
    46  					Base: "some/base",
    47  				},
    48  			},
    49  		},
    50  		{
    51  			name: "file",
    52  			src: model.Source{
    53  				ID:      "the-id",
    54  				Name:    "some-name",
    55  				Version: "some-version",
    56  				Type:    "file",
    57  				Metadata: source.FileSourceMetadata{
    58  					Path:     "some/path",
    59  					Digests:  []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
    60  					MIMEType: "text/plain",
    61  				},
    62  			},
    63  			expected: &source.Description{
    64  				ID:      "the-id",
    65  				Name:    "some-name",
    66  				Version: "some-version",
    67  				Metadata: source.FileSourceMetadata{
    68  					Path:     "some/path",
    69  					Digests:  []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
    70  					MIMEType: "text/plain",
    71  				},
    72  			},
    73  		},
    74  		{
    75  			name: "image",
    76  			src: model.Source{
    77  				ID:      "the-id",
    78  				Name:    "some-name",
    79  				Version: "some-version",
    80  				Type:    "image",
    81  				Metadata: source.StereoscopeImageSourceMetadata{
    82  					UserInput:      "user-input",
    83  					ID:             "id...",
    84  					ManifestDigest: "digest...",
    85  					MediaType:      "type...",
    86  				},
    87  			},
    88  			expected: &source.Description{
    89  				ID:      "the-id",
    90  				Name:    "some-name",
    91  				Version: "some-version",
    92  				Metadata: source.StereoscopeImageSourceMetadata{
    93  					UserInput:      "user-input",
    94  					ID:             "id...",
    95  					ManifestDigest: "digest...",
    96  					MediaType:      "type...",
    97  				},
    98  			},
    99  		},
   100  		// below are regression tests for when the name/version are not provided
   101  		// historically we've hoisted up the name/version from the metadata, now it is a simple pass-through
   102  		{
   103  			name: "directory - no name/version",
   104  			src: model.Source{
   105  				ID:   "the-id",
   106  				Type: "directory",
   107  				Metadata: source.DirectorySourceMetadata{
   108  					Path: "some/path",
   109  					Base: "some/base",
   110  				},
   111  			},
   112  			expected: &source.Description{
   113  				ID: "the-id",
   114  				Metadata: source.DirectorySourceMetadata{
   115  					Path: "some/path",
   116  					Base: "some/base",
   117  				},
   118  			},
   119  		},
   120  		{
   121  			name: "file - no name/version",
   122  			src: model.Source{
   123  				ID:   "the-id",
   124  				Type: "file",
   125  				Metadata: source.FileSourceMetadata{
   126  					Path:     "some/path",
   127  					Digests:  []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
   128  					MIMEType: "text/plain",
   129  				},
   130  			},
   131  			expected: &source.Description{
   132  				ID: "the-id",
   133  				Metadata: source.FileSourceMetadata{
   134  					Path:     "some/path",
   135  					Digests:  []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
   136  					MIMEType: "text/plain",
   137  				},
   138  			},
   139  		},
   140  		{
   141  			name: "image - no name/version",
   142  			src: model.Source{
   143  				ID:   "the-id",
   144  				Type: "image",
   145  				Metadata: source.StereoscopeImageSourceMetadata{
   146  					UserInput:      "user-input",
   147  					ID:             "id...",
   148  					ManifestDigest: "digest...",
   149  					MediaType:      "type...",
   150  				},
   151  			},
   152  			expected: &source.Description{
   153  				ID: "the-id",
   154  				Metadata: source.StereoscopeImageSourceMetadata{
   155  					UserInput:      "user-input",
   156  					ID:             "id...",
   157  					ManifestDigest: "digest...",
   158  					MediaType:      "type...",
   159  				},
   160  			},
   161  		},
   162  	}
   163  	for _, test := range tests {
   164  		t.Run(test.name, func(t *testing.T) {
   165  			// assert the model transformation is correct
   166  			actual := toSyftSourceData(test.src)
   167  			assert.Equal(t, test.expected, actual)
   168  
   169  			tracker.Tested(t, test.expected.Metadata)
   170  		})
   171  	}
   172  }
   173  
   174  func Test_idsHaveChanged(t *testing.T) {
   175  	s, err := toSyftModel(model.Document{
   176  		Source: model.Source{
   177  			Type:     "file",
   178  			Metadata: source.FileSourceMetadata{Path: "some/path"},
   179  		},
   180  		Artifacts: []model.Package{
   181  			{
   182  				PackageBasicData: model.PackageBasicData{
   183  					ID:   "1",
   184  					Name: "pkg-1",
   185  				},
   186  			},
   187  			{
   188  				PackageBasicData: model.PackageBasicData{
   189  					ID:   "2",
   190  					Name: "pkg-2",
   191  				},
   192  			},
   193  		},
   194  		ArtifactRelationships: []model.Relationship{
   195  			{
   196  				Parent: "1",
   197  				Child:  "2",
   198  				Type:   string(artifact.ContainsRelationship),
   199  			},
   200  		},
   201  	})
   202  
   203  	require.NoError(t, err)
   204  	require.Len(t, s.Relationships, 1)
   205  
   206  	r := s.Relationships[0]
   207  
   208  	from := s.Artifacts.Packages.Package(r.From.ID())
   209  	require.NotNil(t, from)
   210  	assert.Equal(t, "pkg-1", from.Name)
   211  
   212  	to := s.Artifacts.Packages.Package(r.To.ID())
   213  	require.NotNil(t, to)
   214  	assert.Equal(t, "pkg-2", to.Name)
   215  }
   216  
   217  func Test_toSyftFiles(t *testing.T) {
   218  	coord := file.Coordinates{
   219  		RealPath:     "/somerwhere/place",
   220  		FileSystemID: "abc",
   221  	}
   222  
   223  	tests := []struct {
   224  		name  string
   225  		files []model.File
   226  		want  sbom.Artifacts
   227  	}{
   228  		{
   229  			name:  "empty",
   230  			files: []model.File{},
   231  			want: sbom.Artifacts{
   232  				FileMetadata: map[file.Coordinates]file.Metadata{},
   233  				FileDigests:  map[file.Coordinates][]file.Digest{},
   234  			},
   235  		},
   236  		{
   237  			name: "no metadata",
   238  			files: []model.File{
   239  				{
   240  					ID:       string(coord.ID()),
   241  					Location: coord,
   242  					Metadata: nil,
   243  					Digests: []file.Digest{
   244  						{
   245  							Algorithm: "sha256",
   246  							Value:     "123",
   247  						},
   248  					},
   249  				},
   250  			},
   251  			want: sbom.Artifacts{
   252  				FileMetadata: map[file.Coordinates]file.Metadata{},
   253  				FileDigests: map[file.Coordinates][]file.Digest{
   254  					coord: {
   255  						{
   256  							Algorithm: "sha256",
   257  							Value:     "123",
   258  						},
   259  					},
   260  				},
   261  			},
   262  		},
   263  		{
   264  			name: "single file",
   265  			files: []model.File{
   266  				{
   267  					ID:       string(coord.ID()),
   268  					Location: coord,
   269  					Metadata: &model.FileMetadataEntry{
   270  						Mode:            777,
   271  						Type:            "RegularFile",
   272  						LinkDestination: "",
   273  						UserID:          42,
   274  						GroupID:         32,
   275  						MIMEType:        "text/plain",
   276  						Size:            92,
   277  					},
   278  					Digests: []file.Digest{
   279  						{
   280  							Algorithm: "sha256",
   281  							Value:     "123",
   282  						},
   283  					},
   284  				},
   285  			},
   286  			want: sbom.Artifacts{
   287  				FileMetadata: map[file.Coordinates]file.Metadata{
   288  					coord: {
   289  						FileInfo: stereoFile.ManualInfo{
   290  							NameValue: "place",
   291  							SizeValue: 92,
   292  							ModeValue: 511, // 777 octal = 511 decimal
   293  						},
   294  						Path:            coord.RealPath,
   295  						LinkDestination: "",
   296  						UserID:          42,
   297  						GroupID:         32,
   298  						Type:            stereoFile.TypeRegular,
   299  						MIMEType:        "text/plain",
   300  					},
   301  				},
   302  				FileDigests: map[file.Coordinates][]file.Digest{
   303  					coord: {
   304  						{
   305  							Algorithm: "sha256",
   306  							Value:     "123",
   307  						},
   308  					},
   309  				},
   310  			},
   311  		},
   312  	}
   313  	for _, tt := range tests {
   314  		t.Run(tt.name, func(t *testing.T) {
   315  			tt.want.FileContents = make(map[file.Coordinates]string)
   316  			tt.want.FileLicenses = make(map[file.Coordinates][]file.License)
   317  			assert.Equal(t, tt.want, toSyftFiles(tt.files))
   318  		})
   319  	}
   320  }
   321  
   322  func Test_toSyfRelationship(t *testing.T) {
   323  	packageWithId := func(id string) *pkg.Package {
   324  		p := &pkg.Package{}
   325  		p.OverrideID(artifact.ID(id))
   326  		return p
   327  	}
   328  	childPackage := packageWithId("some-child-id")
   329  	parentPackage := packageWithId("some-parent-id")
   330  	tests := []struct {
   331  		name          string
   332  		idMap         map[string]interface{}
   333  		idAliases     map[string]string
   334  		relationships model.Relationship
   335  		want          *artifact.Relationship
   336  		wantError     error
   337  	}{
   338  		{
   339  			name: "one relationship no warnings",
   340  			idMap: map[string]interface{}{
   341  				"some-child-id":  childPackage,
   342  				"some-parent-id": parentPackage,
   343  			},
   344  			idAliases: map[string]string{},
   345  			relationships: model.Relationship{
   346  				Parent: "some-parent-id",
   347  				Child:  "some-child-id",
   348  				Type:   string(artifact.ContainsRelationship),
   349  			},
   350  			want: &artifact.Relationship{
   351  				To:   childPackage,
   352  				From: parentPackage,
   353  				Type: artifact.ContainsRelationship,
   354  			},
   355  		},
   356  		{
   357  			name: "relationship unknown type one warning",
   358  			idMap: map[string]interface{}{
   359  				"some-child-id":  childPackage,
   360  				"some-parent-id": parentPackage,
   361  			},
   362  			idAliases: map[string]string{},
   363  			relationships: model.Relationship{
   364  				Parent: "some-parent-id",
   365  				Child:  "some-child-id",
   366  				Type:   "some-unknown-relationship-type",
   367  			},
   368  			wantError: errors.New(
   369  				"unknown relationship type: some-unknown-relationship-type",
   370  			),
   371  		},
   372  		{
   373  			name: "relationship missing child ID one warning",
   374  			idMap: map[string]interface{}{
   375  				"some-parent-id": parentPackage,
   376  			},
   377  			idAliases: map[string]string{},
   378  			relationships: model.Relationship{
   379  				Parent: "some-parent-id",
   380  				Child:  "some-child-id",
   381  				Type:   string(artifact.ContainsRelationship),
   382  			},
   383  			wantError: errors.New(
   384  				"relationship mapping to key some-child-id is not a valid artifact.Identifiable type: <nil>",
   385  			),
   386  		},
   387  		{
   388  			name: "relationship missing parent ID one warning",
   389  			idMap: map[string]interface{}{
   390  				"some-child-id": childPackage,
   391  			},
   392  			idAliases: map[string]string{},
   393  			relationships: model.Relationship{
   394  				Parent: "some-parent-id",
   395  				Child:  "some-child-id",
   396  				Type:   string(artifact.ContainsRelationship),
   397  			},
   398  			wantError: errors.New("relationship mapping from key some-parent-id is not a valid artifact.Identifiable type: <nil>"),
   399  		},
   400  	}
   401  
   402  	for _, tt := range tests {
   403  		t.Run(tt.name, func(t *testing.T) {
   404  			got, gotErr := toSyftRelationship(tt.idMap, tt.relationships, tt.idAliases)
   405  			assert.Equal(t, tt.want, got)
   406  			assert.Equal(t, tt.wantError, gotErr)
   407  		})
   408  	}
   409  }
   410  
   411  func Test_deduplicateErrors(t *testing.T) {
   412  	tests := []struct {
   413  		name   string
   414  		errors []error
   415  		want   []string
   416  	}{
   417  		{
   418  			name: "no errors, nil slice",
   419  		},
   420  		{
   421  			name: "deduplicates errors",
   422  			errors: []error{
   423  				errors.New("some error"),
   424  				errors.New("some error"),
   425  			},
   426  			want: []string{
   427  				`"some error" occurred 2 time(s)`,
   428  			},
   429  		},
   430  	}
   431  	for _, tt := range tests {
   432  		t.Run(tt.name, func(t *testing.T) {
   433  			got := deduplicateErrors(tt.errors)
   434  			assert.Equal(t, tt.want, got)
   435  		})
   436  	}
   437  }