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