From c29c6ba3cfe2401162e7fa61b140487436399504 Mon Sep 17 00:00:00 2001 From: Joel Rudsberg Date: Wed, 30 Oct 2024 14:03:18 +0100 Subject: [PATCH] Add support for dependencies and purl for Native Image SBOMs Signed-off-by: Joel Rudsberg --- .../java/graalvm_native_image_cataloger.go | 142 ++++------ .../graalvm_native_image_cataloger_test.go | 255 +++++++++++++----- .../test-fixtures/graalvm-sbom/micronaut.json | 56 +++- 3 files changed, 291 insertions(+), 162 deletions(-) diff --git a/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go b/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go index add09a45899..96a63955cec 100644 --- a/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go +++ b/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go @@ -8,7 +8,6 @@ import ( "debug/macho" "debug/pe" "encoding/binary" - "encoding/json" "errors" "fmt" "io" @@ -18,34 +17,14 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/mimetype" "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/format/cyclonedxjson" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" ) -type nativeImageCycloneDX struct { - BomFormat string `json:"bomFormat"` - SpecVersion string `json:"specVersion"` - Version int `json:"version"` - Components []nativeImageComponent `json:"components"` -} - -type nativeImageComponent struct { - Type string `json:"type"` - Group string `json:"group"` - Name string `json:"name"` - Version string `json:"version"` - Properties []nativeImageCPE `json:"properties"` -} - -type nativeImageCPE struct { - Name string `json:"name"` - Value string `json:"value"` -} - type nativeImage interface { - fetchPkgs() ([]pkg.Package, error) + fetchPkgs() ([]pkg.Package, []artifact.Relationship, error) } type nativeImageElf struct { @@ -113,40 +92,12 @@ func (c *nativeImageCataloger) Name() string { return nativeImageCatalogerName } -// getPackage returns the package given within a NativeImageComponent. -func getPackage(component nativeImageComponent) pkg.Package { - var cpes []cpe.CPE - for _, property := range component.Properties { - c, err := cpe.New(property.Value, cpe.DeclaredSource) - if err != nil { - log.Debugf("unable to parse Attributes: %v", err) - continue - } - cpes = append(cpes, c) - } - return pkg.Package{ - Name: component.Name, - Version: component.Version, - Language: pkg.Java, - Type: pkg.GraalVMNativeImagePkg, - FoundBy: nativeImageCatalogerName, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: component.Group, - }, - }, - CPEs: cpes, - } -} - // decompressSbom returns the packages given within a native image executable's SBOM. -func decompressSbom(dataBuf []byte, sbomStart uint64, lengthStart uint64) ([]pkg.Package, error) { - var pkgs []pkg.Package - +func decompressSbom(dataBuf []byte, sbomStart uint64, lengthStart uint64) ([]pkg.Package, []artifact.Relationship, error) { lengthEnd := lengthStart + 8 bufLen := len(dataBuf) if lengthEnd > uint64(bufLen) { - return nil, errors.New("the 'sbom_length' symbol overflows the binary") + return nil, nil, errors.New("the 'sbom_length' symbol overflows the binary") } length := dataBuf[lengthStart:lengthEnd] @@ -154,39 +105,31 @@ func decompressSbom(dataBuf []byte, sbomStart uint64, lengthStart uint64) ([]pkg var storedLength uint64 err := binary.Read(p, binary.LittleEndian, &storedLength) if err != nil { - return nil, fmt.Errorf("could not read from binary file: %w", err) + return nil, nil, fmt.Errorf("could not read from binary file: %w", err) } log.WithFields("len", storedLength).Trace("found java native-image SBOM") sbomEnd := sbomStart + storedLength if sbomEnd > uint64(bufLen) { - return nil, errors.New("the sbom symbol overflows the binary") + return nil, nil, errors.New("the sbom symbol overflows the binary") } sbomCompressed := dataBuf[sbomStart:sbomEnd] p = bytes.NewBuffer(sbomCompressed) gzreader, err := gzip.NewReader(p) if err != nil { - return nil, fmt.Errorf("could not decompress the java native-image SBOM: %w", err) - } - - output, err := io.ReadAll(gzreader) - if err != nil { - return nil, fmt.Errorf("could not read the java native-image SBOM: %w", err) + return nil, nil, fmt.Errorf("could not decompress the java native-image SBOM: %w", err) } - var sbomContent nativeImageCycloneDX - err = json.Unmarshal(output, &sbomContent) + sbom, _, _, err := cyclonedxjson.NewFormatDecoder().Decode(gzreader) if err != nil { - return nil, fmt.Errorf("could not unmarshal the java native-image SBOM: %w", err) + return nil, nil, fmt.Errorf("could not unmarshal the java native-image SBOM: %w", err) } - - for _, component := range sbomContent.Components { - p := getPackage(component) + var pkgs []pkg.Package + for p := range sbom.Artifacts.Packages.Enumerate() { pkgs = append(pkgs, p) } - - return pkgs, nil + return pkgs, sbom.Relationships, nil } // fileError logs an error message when an executable cannot be read. @@ -294,7 +237,7 @@ func newPE(filename string, r io.ReaderAt) (nativeImage, error) { } // fetchPkgs obtains the packages given in the binary. -func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) { +func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, relationships []artifact.Relationship, retErr error) { defer func() { if r := recover(); r != nil { // this can happen in cases where a malformed binary is passed in can be initially parsed, but not @@ -310,10 +253,10 @@ func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) { si, err := bi.Symbols() if err != nil { - return nil, fmt.Errorf("no symbols found in binary: %w", err) + return nil, nil, fmt.Errorf("no symbols found in binary: %w", err) } if si == nil { - return nil, errors.New(nativeImageMissingSymbolsError) + return nil, nil, errors.New(nativeImageMissingSymbolsError) } for _, s := range si { switch s.Name { @@ -326,16 +269,16 @@ func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) { } } if sbom.Value == 0 || sbomLength.Value == 0 || svmVersion.Value == 0 { - return nil, errors.New(nativeImageMissingSymbolsError) + return nil, nil, errors.New(nativeImageMissingSymbolsError) } dataSection := bi.Section(".data") if dataSection == nil { - return nil, fmt.Errorf("no .data section found in binary: %w", err) + return nil, nil, fmt.Errorf("no .data section found in binary: %w", err) } dataSectionBase := dataSection.SectionHeader.Addr data, err := dataSection.Data() if err != nil { - return nil, fmt.Errorf("cannot read the .data section: %w", err) + return nil, nil, fmt.Errorf("cannot read the .data section: %w", err) } sbomLocation := sbom.Value - dataSectionBase lengthLocation := sbomLength.Value - dataSectionBase @@ -344,7 +287,7 @@ func (ni nativeImageElf) fetchPkgs() (pkgs []pkg.Package, retErr error) { } // fetchPkgs obtains the packages from a Native Image given as a Mach O file. -func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, retErr error) { +func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, relationships []artifact.Relationship, retErr error) { defer func() { if r := recover(); r != nil { // this can happen in cases where a malformed binary is passed in can be initially parsed, but not @@ -359,7 +302,7 @@ func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, retErr error) { bi := ni.file if bi.Symtab == nil { - return nil, errors.New(nativeImageMissingSymbolsError) + return nil, nil, errors.New(nativeImageMissingSymbolsError) } for _, s := range bi.Symtab.Syms { switch s.Name { @@ -372,17 +315,17 @@ func (ni nativeImageMachO) fetchPkgs() (pkgs []pkg.Package, retErr error) { } } if sbom.Value == 0 || sbomLength.Value == 0 || svmVersion.Value == 0 { - return nil, errors.New(nativeImageMissingSymbolsError) + return nil, nil, errors.New(nativeImageMissingSymbolsError) } dataSegment := bi.Segment("__DATA") if dataSegment == nil { - return nil, nil + return nil, nil, nil } dataBuf, err := dataSegment.Data() if err != nil { log.Tracef("cannot obtain buffer from data segment") - return nil, nil + return nil, nil, nil } sbomLocation := sbom.Value - dataSegment.Addr lengthLocation := sbomLength.Value - dataSegment.Addr @@ -494,7 +437,7 @@ func (ni nativeImagePE) fetchSbomSymbols(content *exportContentPE) { } // fetchPkgs obtains the packages from a Native Image given as a PE file. -func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, retErr error) { +func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, relationships []artifact.Relationship, retErr error) { defer func() { if r := recover(); r != nil { // this can happen in cases where a malformed binary is passed in can be initially parsed, but not @@ -506,32 +449,32 @@ func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, retErr error) { content, err := ni.fetchExportContent() if err != nil { log.Debugf("could not fetch the content of the export directory entry: %v", err) - return nil, err + return nil, nil, err } ni.fetchSbomSymbols(content) if content.addressOfSbom == uint32(0) || content.addressOfSbomLength == uint32(0) || content.addressOfSvmVersion == uint32(0) { - return nil, errors.New(nativeImageMissingSymbolsError) + return nil, nil, errors.New(nativeImageMissingSymbolsError) } functionsBase := content.addressOfFunctions - ni.exportSymbols.VirtualAddress sbomOffset := content.addressOfSbom sbomAddress, err := ni.fetchExportFunctionPointer(functionsBase, sbomOffset) if err != nil { - return nil, fmt.Errorf("could not fetch SBOM pointer from exported functions: %w", err) + return nil, nil, fmt.Errorf("could not fetch SBOM pointer from exported functions: %w", err) } sbomLengthOffset := content.addressOfSbomLength sbomLengthAddress, err := ni.fetchExportFunctionPointer(functionsBase, sbomLengthOffset) if err != nil { - return nil, fmt.Errorf("could not fetch SBOM length pointer from exported functions: %w", err) + return nil, nil, fmt.Errorf("could not fetch SBOM length pointer from exported functions: %w", err) } bi := ni.file dataSection := bi.Section(".data") if dataSection == nil { - return nil, nil + return nil, nil, nil } dataBuf, err := dataSection.Data() if err != nil { log.Tracef("cannot obtain buffer from the java native-image .data section") - return nil, nil + return nil, nil, nil } sbomLocation := sbomAddress - dataSection.VirtualAddress lengthLocation := sbomLengthAddress - dataSection.VirtualAddress @@ -540,8 +483,9 @@ func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, retErr error) { } // fetchPkgs provides the packages available in a UnionReader. -func fetchPkgs(reader unionreader.UnionReader, filename string) []pkg.Package { +func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package, []artifact.Relationship) { var pkgs []pkg.Package + var relationships []artifact.Relationship imageFormats := []func(string, io.ReaderAt) (nativeImage, error){newElf, newMachO, newPE} // NOTE: multiple readers are returned to cover universal binaries, which are files @@ -549,7 +493,7 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) []pkg.Package { readers, err := unionreader.GetReaders(reader) if err != nil { log.Debugf("failed to open the java native-image binary: %v", err) - return nil + return nil, nil } for _, r := range readers { for _, makeNativeImage := range imageFormats { @@ -560,47 +504,51 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) []pkg.Package { if ni == nil { continue } - newPkgs, err := ni.fetchPkgs() + newPkgs, newRelationships, err := ni.fetchPkgs() if err != nil { log.Tracef("unable to extract SBOM from possible java native-image %s: %v", filename, err) continue } pkgs = append(pkgs, newPkgs...) + relationships = append(relationships, newRelationships...) } } - return pkgs + return pkgs, relationships } // Catalog attempts to find any native image executables reachable from a resolver. func (c *nativeImageCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package + var relationships []artifact.Relationship fileMatches, err := resolver.FilesByMIMEType(mimetype.ExecutableMIMETypeSet.List()...) if err != nil { return pkgs, nil, fmt.Errorf("failed to find binaries by mime types: %w", err) } for _, location := range fileMatches { - newPkgs, err := processLocation(location, resolver) + newPkgs, newRelationships, err := processLocation(location, resolver) if err != nil { return nil, nil, err } pkgs = append(pkgs, newPkgs...) + relationships = append(relationships, newRelationships...) } - return pkgs, nil, nil + return pkgs, relationships, nil } -func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Package, error) { +func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { readerCloser, err := resolver.FileContentsByLocation(location) if err != nil { log.Debugf("error opening file: %v", err) - return nil, nil + return nil, nil, nil } defer internal.CloseAndLogError(readerCloser, location.RealPath) reader, err := unionreader.GetUnionReader(readerCloser) if err != nil { - return nil, err + return nil, nil, err } - return fetchPkgs(reader, location.RealPath), nil + pkgs, relationships := fetchPkgs(reader, location.RealPath) + return pkgs, relationships, nil } diff --git a/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go b/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go index 766760c65ad..fb2ec7d917c 100644 --- a/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go +++ b/syft/pkg/cataloger/java/graalvm_native_image_cataloger_test.go @@ -5,6 +5,7 @@ import ( "bytes" "compress/gzip" "encoding/binary" + "fmt" "io" "os" "path" @@ -12,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/pkg" @@ -40,10 +42,11 @@ func TestParseNativeImage(t *testing.T) { assert.NoError(t, err) parsed := false readers, err := unionreader.GetReaders(reader) + assert.NoError(t, err) for _, r := range readers { ni, err := test.newFn(test.fixture, r) assert.NoError(t, err) - _, err = ni.fetchPkgs() + _, _, err = ni.fetchPkgs() if err == nil { t.Fatalf("should have failed to extract SBOM.") } @@ -60,77 +63,205 @@ func TestParseNativeImage(t *testing.T) { } func TestParseNativeImageSbom(t *testing.T) { + const ( + nettyPurl = "pkg:maven/io.netty/netty-codec-http2@4.1.104.Final" + micronautPurl = "pkg:maven/io.micronaut/core@4.2.3" + mainAppPurl = "pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT" + ) + + mainAppPkg := makePackage("main-test-app", "1.0-SNAPSHOT", mainAppPurl, []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "app", + Product: "main-test-app", + Version: "1.0-SNAPSHOT", + }, + Source: "declared", + }, + }) + + nettyPkg := makePackage("netty-codec-http2", "4.1.73.Final", nettyPurl, []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "codec", + Product: "codec", + Version: "4.1.73.Final", + }, + Source: "declared", + }, + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "codec", + Product: "netty-codec-http2", + Version: "4.1.73.Final", + }, + Source: "declared", + }, + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "codec", + Product: "netty_codec_http2", + Version: "4.1.73.Final", + }, + Source: "declared", + }, + }) + + micronautPkg := makePackage("core", "4.2.3", micronautPurl, []cpe.CPE{ + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "core", + Product: "core", + Version: "4.2.3", + }, + Source: "declared", + }, + { + Attributes: cpe.Attributes{ + Part: "a", + Vendor: "micronaut", + Product: "core", + Version: "4.2.3", + }, + Source: "declared", + }, + }) + + basicPkg := makePackage("basic-lib", "1.0", "", nil) + tests := []struct { - fixture string - expected []pkg.Package + fixture string + expectedPackages []pkg.Package + expectedRelations []artifact.Relationship }{ { - fixture: "test-fixtures/graalvm-sbom/micronaut.json", - expected: []pkg.Package{ + fixture: "test-fixtures/graalvm-sbom/micronaut.json", + expectedPackages: []pkg.Package{nettyPkg, micronautPkg, basicPkg, mainAppPkg}, + expectedRelations: []artifact.Relationship{ { - Name: "netty-codec-http2", - Version: "4.1.73.Final", - Language: pkg.Java, - Type: pkg.GraalVMNativeImagePkg, - FoundBy: nativeImageCatalogerName, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "io.netty", - }, - }, - CPEs: []cpe.CPE{ - { - Attributes: cpe.Attributes{ - Part: "a", - Vendor: "codec", - Product: "codec", - Version: "4.1.73.Final", - }, - Source: "declared", - }, - { - Attributes: cpe.Attributes{ - Part: "a", - Vendor: "codec", - Product: "netty-codec-http2", - Version: "4.1.73.Final", - }, - Source: "declared", - }, - { - Attributes: cpe.Attributes{ - Part: "a", - Vendor: "codec", - Product: "netty_codec_http2", - Version: "4.1.73.Final", - }, - Source: "declared", - }, - }, + From: nettyPkg, + To: micronautPkg, + Type: artifact.DependencyOfRelationship, }, }, }, } + for _, test := range tests { t.Run(path.Base(test.fixture), func(t *testing.T) { - // Create a buffer to resemble a compressed SBOM in a native image. - sbom, err := os.ReadFile(test.fixture) - assert.NoError(t, err) - var b bytes.Buffer - writebytes := bufio.NewWriter(&b) - z := gzip.NewWriter(writebytes) - _, err = z.Write(sbom) - assert.NoError(t, err) - _ = z.Close() - _ = writebytes.Flush() - compressedsbom := b.Bytes() - sbomlength := uint64(len(compressedsbom)) - _ = binary.Write(writebytes, binary.LittleEndian, sbomlength) - _ = writebytes.Flush() - compressedsbom = b.Bytes() - actual, err := decompressSbom(compressedsbom, 0, sbomlength) - assert.NoError(t, err) - assert.Equal(t, test.expected, actual) + compressed, length := createCompressedSbom(t, test.fixture) + actualPkgs, actualRels, err := decompressSbom(compressed, 0, length) + if err != nil { + t.Fatal(err) + } + + verifyPackages(t, test.expectedPackages, actualPkgs) + verifyRelationships(t, test.expectedRelations, actualRels) }) } } + +// makePackage makes a package using the data that we know must be in the parsed package +func makePackage(name, version, purl string, cpes []cpe.CPE) pkg.Package { + p := pkg.Package{ + Name: name, + Version: version, + CPEs: cpes, + PURL: purl, + } + return p +} + +// createCompressedSbom creates a compressed a buffer to resemble a compressed SBOM in a native image. +func createCompressedSbom(t *testing.T, filename string) ([]byte, uint64) { + sbom, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + w := bufio.NewWriter(&b) + z := gzip.NewWriter(w) + + if _, err := z.Write(sbom); err != nil { + t.Fatal(err) + } + z.Close() + w.Flush() + + compressedSbom := b.Bytes() + sbomLength := uint64(len(compressedSbom)) + + if err := binary.Write(w, binary.LittleEndian, sbomLength); err != nil { + t.Fatal(err) + } + w.Flush() + + return b.Bytes(), sbomLength +} + +func verifyPackages(t *testing.T, expected, actual []pkg.Package) { + expectedPkgMap := buildPackageMap(expected) + actualPkgMap := buildPackageMap(actual) + + for name, expectedPkg := range expectedPkgMap { + actualPkg, exists := actualPkgMap[name] + if !exists { + t.Errorf("Expected package %s not found in actual packages", name) + continue + } + verifyPackageFields(t, expectedPkg, actualPkg) + } +} + +func buildPackageMap(packages []pkg.Package) map[string]pkg.Package { + pkgMap := make(map[string]pkg.Package) + for _, p := range packages { + pkgMap[p.Name] = p + } + return pkgMap +} + +func verifyPackageFields(t *testing.T, expected, actual pkg.Package) { + assert.Equal(t, expected.Name, actual.Name) + assert.Equal(t, expected.Version, actual.Version) + assert.Equal(t, expected.FoundBy, actual.FoundBy) + assert.Equal(t, expected.PURL, actual.PURL) + assert.ElementsMatch(t, expected.CPEs, actual.CPEs) +} + +func verifyRelationships(t *testing.T, expected, actual []artifact.Relationship) { + expectedRelMap := buildRelationshipMap(expected) + actualRelMap := buildRelationshipMap(actual) + + for key, expectedRels := range expectedRelMap { + actualRels, exists := actualRelMap[key] + if !exists { + t.Errorf("Expected relationship %s not found in actual relationships", key) + continue + } + verifyRelationshipFields(t, expectedRels, actualRels) + } +} + +func buildRelationshipMap(relationships []artifact.Relationship) map[string][]artifact.Relationship { + relMap := make(map[string][]artifact.Relationship) + for _, rel := range relationships { + // we cannot control the id, so use the names instead + key := fmt.Sprintf("%s->%s", rel.From.(pkg.Package).Name, rel.To.(pkg.Package).Name) + relMap[key] = append(relMap[key], rel) + } + return relMap +} + +func verifyRelationshipFields(t *testing.T, expected, actual []artifact.Relationship) { + assert.Equal(t, len(expected), len(actual)) + for i := range expected { + assert.Equal(t, expected[i].Type, actual[i].Type) + } +} diff --git a/syft/pkg/cataloger/java/test-fixtures/graalvm-sbom/micronaut.json b/syft/pkg/cataloger/java/test-fixtures/graalvm-sbom/micronaut.json index 06c7881ab24..ad9c0579050 100644 --- a/syft/pkg/cataloger/java/test-fixtures/graalvm-sbom/micronaut.json +++ b/syft/pkg/cataloger/java/test-fixtures/graalvm-sbom/micronaut.json @@ -1,13 +1,32 @@ { "bomFormat": "CycloneDX", - "specVersion": "1.4", + "specVersion": "1.6", "version": 1, + "serialNumber": "urn:uuid:43538af4-f715-3d85-9629-336fdd3790ad", + "metadata": { + "component": { + "type": "library", + "group": "com.test", + "name": "main-test-app", + "version": "1.0-SNAPSHOT", + "purl": "pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT", + "bom-ref": "pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT", + "properties": [ + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:app:main-test-app:1.0-SNAPSHOT:*:*:*:*:*:*:*" + } + ] + } + }, "components": [ { "type": "library", "group": "io.netty", "name": "netty-codec-http2", "version": "4.1.73.Final", + "purl": "pkg:maven/io.netty/netty-codec-http2@4.1.104.Final", + "bom-ref": "pkg:maven/io.netty/netty-codec-http2@4.1.104.Final", "properties": [ { "name": "syft:cpe23", @@ -22,7 +41,38 @@ "value": "cpe:2.3:a:codec:netty_codec_http2:4.1.73.Final:*:*:*:*:*:*:*" } ] + }, + { + "type": "library", + "group": "io.micronaut", + "name": "core", + "version": "4.2.3", + "purl": "pkg:maven/io.micronaut/core@4.2.3", + "bom-ref": "pkg:maven/io.micronaut/core@4.2.3", + "properties": [ + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:core:core:4.2.3:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:micronaut:core:4.2.3:*:*:*:*:*:*:*" + } + ] + }, + { + "type": "library", + "group": "org.example", + "name": "basic-lib", + "version": "1.0" } ], - "serialNumber": "urn:uuid:43538af4-f715-3d85-9629-336fdd3790ad" -} + "dependencies": [ + { + "ref": "pkg:maven/io.micronaut/core@4.2.3", + "dependsOn": [ + "pkg:maven/io.netty/netty-codec-http2@4.1.104.Final" + ] + } + ] +} \ No newline at end of file