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 }