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 }