github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/syftjson/to_syft_model_test.go (about)

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