feat: Refactor CycloneDX parser into CycloneDX Graph Parser

refactor: CDX graph parser to improve readability

fix: Set dependency graph present only when BOM contains at least 1 dependency relation

chore: Add a root note while graph rendering (reporter)

chore: Remove old cyclonedx files

test: Add maven cyclonedx sbom test case
This commit is contained in:
abhisek 2024-02-02 00:02:48 +05:30
parent 774323c28f
commit b662145492
No known key found for this signature in database
GPG Key ID: CB92A4990C02A88F
14 changed files with 40727 additions and 44583 deletions

View File

@ -65,6 +65,10 @@ func (pm *PackageManifest) AddPackage(pkg *Package) {
pm.m.Lock()
defer pm.m.Unlock()
if pkg.Manifest == nil {
pkg.Manifest = pm
}
pm.Packages = append(pm.Packages, pkg)
pm.DependencyGraph.AddNode(pkg)
}

View File

@ -1,57 +0,0 @@
package cyclonedx
import (
"bufio"
"os"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/google/osv-scanner/pkg/lockfile"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/parser/custom/packagefile"
)
func Parse(pathToLockfile string) ([]lockfile.PackageDetails, error) {
details := []lockfile.PackageDetails{}
bom := cdx.NewBOM()
logger.Infof("Starting SBOM decoding...")
file, err := os.Open(pathToLockfile)
if err != nil {
logger.Debugf("Error in Decoding the SBOM file %v", err)
return nil, err
}
defer file.Close()
sbom_content := bufio.NewReader(file)
decoder := cdx.NewBOMDecoder(sbom_content, cdx.BOMFileFormatJSON)
if err = decoder.Decode(bom); err != nil {
logger.Debugf("Error in Decoding the SBOM file %v", err)
return nil, err
}
// Components is a pointer array and it can be empty
components := utils.SafelyGetValue(bom.Components)
for _, comp := range components {
if d, err := convertSbomComponent2LPD(&comp); err != nil {
logger.Debugf("Failed converting sbom to lockfile component: %v", err)
} else {
details = append(details, *d)
}
}
logger.Debugf("Found number of packages %d", len(details))
return details, nil
}
func convertSbomComponent2LPD(comp *cdx.Component) (*lockfile.PackageDetails, error) {
pd, err := packagefile.ParsePackageFromPurl(comp.PackageURL)
if err != nil {
return nil, err
}
pd.CycloneDxRef = comp
return pd.Convert2LockfilePackageDetails(), nil
}

View File

@ -1,91 +0,0 @@
package cyclonedx
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/google/osv-scanner/pkg/lockfile"
"github.com/stretchr/testify/assert"
)
func TestParseCyclonedxSBOM(t *testing.T) {
// Create a sample SBOM JSON file
tempFile, _ := ioutil.TempFile("", "sbom_*.json")
defer os.Remove(tempFile.Name())
sbomContent := `{
"Components": [
{
"group": "",
"name": "requests",
"version": "1.0",
"purl": "pkg:pypi/requests@2.26.0"
},
{
"group": "testgroup",
"name": "lodash",
"version": "2.0",
"purl": "pkg:npm/testgroup/lodash@4.17.21"
}
]
}`
err := ioutil.WriteFile(tempFile.Name(), []byte(sbomContent), 0644)
assert.Nil(t, err)
packages, err := Parse(tempFile.Name())
assert.Nil(t, err)
assert.Len(t, packages, 2)
assert.Equal(t, "requests", packages[0].Name)
assert.Equal(t, "testgroup/lodash", packages[1].Name)
}
func TestConvertSbomComponent2LPD(t *testing.T) {
component := cdx.Component{
Group: "",
Name: "requests",
Version: "2.26.0",
PackageURL: "pkg:pypi/requests@2.26.0",
}
pd, err := convertSbomComponent2LPD(&component)
assert.Nil(t, err)
assert.Equal(t, "requests", pd.Name)
assert.Equal(t, "2.26.0", pd.Version)
assert.Equal(t, lockfile.PipEcosystem, pd.Ecosystem)
}
func TestParseCyclonedxSBOM_WithEmptyComponents(t *testing.T) {
// Create a sample SBOM JSON file
tempFile, _ := ioutil.TempFile("", "sbom_*.json")
defer os.Remove(tempFile.Name())
sbomContent := `{
}`
err := ioutil.WriteFile(tempFile.Name(), []byte(sbomContent), 0644)
assert.Nil(t, err)
packages, err := Parse(tempFile.Name())
assert.Nil(t, err)
assert.Len(t, packages, 0)
}
func TestParseCyclonedxSBOM_WithMultipleFiles(t *testing.T) {
fixtureDir := "fixtures/cydxsbom"
filesToTest := []string{"bom-dpc-int1.json", "bom-du.json", "bom-npm1.json"}
for _, filename := range filesToTest {
t.Run(filename, func(t *testing.T) {
filePath := filepath.Join(fixtureDir, filename)
packages, err := Parse(filePath)
assert.Nil(t, err)
assert.NotNil(t, packages)
assert.NotEmpty(t, packages)
})
}
}

View File

@ -1,472 +0,0 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:8161abe9-1e8b-4456-ba4f-4d847267b76a",
"version": 1,
"metadata": {
"timestamp": "2023-08-11T06:45:11.368Z",
"tools": [
{
"vendor": "cyclonedx",
"name": "cdxgen",
"version": "8.0.4"
}
],
"authors": [
{
"name": "Prabhu Subramanian",
"email": "prabhu@appthreat.com"
}
],
"component": {
"group": "",
"name": "tmp",
"version": "",
"type": "application"
}
},
"components": [
{
"publisher": "",
"group": "",
"name": "google-cloud-pubsub",
"version": "2.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/google-cloud-pubsub@2.0",
"type": "library",
"bom-ref": "pkg:pypi/google-cloud-pubsub@2.0"
},
{
"publisher": "",
"group": "",
"name": "knowledge-graph",
"version": "3.12.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/knowledge-graph@3.12.0",
"type": "library",
"bom-ref": "pkg:pypi/knowledge-graph@3.12.0"
},
{
"publisher": "",
"group": "",
"name": "deepc_exceptions",
"version": "0.4.3",
"description": "",
"licenses": [],
"purl": "pkg:pypi/deepc-exceptions@0.4.3",
"type": "library",
"bom-ref": "pkg:pypi/deepc-exceptions@0.4.3"
},
{
"publisher": "",
"group": "",
"name": "deepc-models",
"version": "3.48.6",
"description": "",
"licenses": [],
"purl": "pkg:pypi/deepc-models@3.48.6",
"type": "library",
"bom-ref": "pkg:pypi/deepc-models@3.48.6"
},
{
"publisher": "",
"group": "",
"name": "deepc-utils",
"version": "6.54.32",
"description": "",
"licenses": [],
"purl": "pkg:pypi/deepc-utils@6.54.32",
"type": "library",
"bom-ref": "pkg:pypi/deepc-utils@6.54.32"
},
{
"publisher": "",
"group": "",
"name": "deepc-convertor",
"version": "0.62.2",
"description": "",
"licenses": [],
"purl": "pkg:pypi/deepc-convertor@0.62.2",
"type": "library",
"bom-ref": "pkg:pypi/deepc-convertor@0.62.2"
},
{
"publisher": "",
"group": "",
"name": "deepc_dorks",
"version": "0.4",
"description": "",
"licenses": [],
"purl": "pkg:pypi/deepc-dorks@0.4",
"type": "library",
"bom-ref": "pkg:pypi/deepc-dorks@0.4"
},
{
"publisher": "",
"group": "",
"name": "deepc_social",
"version": "0.2",
"description": "",
"licenses": [],
"purl": "pkg:pypi/deepc-social@0.2",
"type": "library",
"bom-ref": "pkg:pypi/deepc-social@0.2"
},
{
"publisher": "",
"group": "",
"name": "di_client",
"version": "1.4.5",
"description": "",
"licenses": [],
"purl": "pkg:pypi/di-client@1.4.5",
"type": "library",
"bom-ref": "pkg:pypi/di-client@1.4.5"
},
{
"publisher": "",
"group": "",
"name": "exploration-events",
"version": "3.4",
"description": "",
"licenses": [],
"purl": "pkg:pypi/exploration-events@3.4",
"type": "library",
"bom-ref": "pkg:pypi/exploration-events@3.4"
},
{
"publisher": "",
"group": "",
"name": "nessus-client",
"version": "0.13.12",
"description": "",
"licenses": [],
"purl": "pkg:pypi/nessus-client@0.13.12",
"type": "library",
"bom-ref": "pkg:pypi/nessus-client@0.13.12"
},
{
"publisher": "",
"group": "",
"name": "vulners",
"version": "1.5.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/vulners@1.5.0",
"type": "library",
"bom-ref": "pkg:pypi/vulners@1.5.0"
},
{
"publisher": "",
"group": "",
"name": "ipwhois",
"version": "1.1.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/ipwhois@1.1.0",
"type": "library",
"bom-ref": "pkg:pypi/ipwhois@1.1.0"
},
{
"publisher": "",
"group": "",
"name": "gpapi",
"version": "0.4.4",
"description": "",
"licenses": [],
"purl": "pkg:pypi/gpapi@0.4.4",
"type": "library",
"bom-ref": "pkg:pypi/gpapi@0.4.4"
},
{
"publisher": "",
"group": "",
"name": "PyGithub",
"version": "1.54.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/pygithub@1.54.1",
"type": "library",
"bom-ref": "pkg:pypi/pygithub@1.54.1"
},
{
"publisher": "",
"group": "",
"name": "python-whois",
"version": "0.7.3",
"description": "",
"licenses": [],
"purl": "pkg:pypi/python-whois@0.7.3",
"type": "library",
"bom-ref": "pkg:pypi/python-whois@0.7.3"
},
{
"publisher": "",
"group": "",
"name": "sh",
"version": "1.14.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/sh@1.14.1",
"type": "library",
"bom-ref": "pkg:pypi/sh@1.14.1"
},
{
"publisher": "",
"group": "",
"name": "OTXv2",
"version": "1.5.10",
"description": "",
"licenses": [],
"purl": "pkg:pypi/otxv2@1.5.10",
"type": "library",
"bom-ref": "pkg:pypi/otxv2@1.5.10"
},
{
"publisher": "",
"group": "",
"name": "certstream",
"version": "1.11",
"description": "",
"licenses": [],
"purl": "pkg:pypi/certstream@1.11",
"type": "library",
"bom-ref": "pkg:pypi/certstream@1.11"
},
{
"publisher": "",
"group": "",
"name": "colorama",
"version": "0.4.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/colorama@0.4.1",
"type": "library",
"bom-ref": "pkg:pypi/colorama@0.4.1"
},
{
"publisher": "",
"group": "",
"name": "ipaddress",
"version": "1.0.22",
"description": "",
"licenses": [],
"purl": "pkg:pypi/ipaddress@1.0.22",
"type": "library",
"bom-ref": "pkg:pypi/ipaddress@1.0.22"
},
{
"publisher": "",
"group": "",
"name": "packaging",
"version": "19.2",
"description": "",
"licenses": [],
"purl": "pkg:pypi/packaging@19.2",
"type": "library",
"bom-ref": "pkg:pypi/packaging@19.2"
},
{
"publisher": "",
"group": "",
"name": "prettytable",
"version": "0.7.2",
"description": "",
"licenses": [],
"purl": "pkg:pypi/prettytable@0.7.2",
"type": "library",
"bom-ref": "pkg:pypi/prettytable@0.7.2"
},
{
"publisher": "",
"group": "",
"name": "pyfiglet",
"version": "0.8.post1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/pyfiglet@0.8.post1",
"type": "library",
"bom-ref": "pkg:pypi/pyfiglet@0.8.post1"
},
{
"publisher": "",
"group": "",
"name": "requests",
"version": "2.22.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/requests@2.22.0",
"type": "library",
"bom-ref": "pkg:pypi/requests@2.22.0"
},
{
"publisher": "",
"group": "",
"name": "termcolor",
"version": "1.1.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/termcolor@1.1.0",
"type": "library",
"bom-ref": "pkg:pypi/termcolor@1.1.0"
},
{
"publisher": "",
"group": "",
"name": "beautifulsoup4",
"version": "4.8.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/beautifulsoup4@4.8.1",
"type": "library",
"bom-ref": "pkg:pypi/beautifulsoup4@4.8.1"
},
{
"publisher": "",
"group": "",
"name": "fcwhispers",
"version": "2.1.7",
"description": "",
"licenses": [],
"purl": "pkg:pypi/fcwhispers@2.1.7",
"type": "library",
"bom-ref": "pkg:pypi/fcwhispers@2.1.7"
},
{
"publisher": "",
"group": "",
"name": "gvm-tools",
"version": "21.6.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/gvm-tools@21.6.0",
"type": "library",
"bom-ref": "pkg:pypi/gvm-tools@21.6.0"
},
{
"publisher": "",
"group": "",
"name": "cloud_ip_info",
"version": "1.3.3",
"description": "",
"licenses": [],
"purl": "pkg:pypi/cloud-ip-info@1.3.3",
"type": "library",
"bom-ref": "pkg:pypi/cloud-ip-info@1.3.3"
},
{
"publisher": "",
"group": "",
"name": "Jinja2",
"version": "3.0.3",
"description": "",
"licenses": [],
"purl": "pkg:pypi/jinja2@3.0.3",
"type": "library",
"bom-ref": "pkg:pypi/jinja2@3.0.3"
},
{
"publisher": "",
"group": "",
"name": "configobj",
"version": "5.0.6",
"description": "",
"licenses": [],
"purl": "pkg:pypi/configobj@5.0.6",
"type": "library",
"bom-ref": "pkg:pypi/configobj@5.0.6"
},
{
"publisher": "",
"group": "",
"name": "cloud_recon",
"version": "0.2.7",
"description": "",
"licenses": [],
"purl": "pkg:pypi/cloud-recon@0.2.7",
"type": "library",
"bom-ref": "pkg:pypi/cloud-recon@0.2.7"
},
{
"publisher": "",
"group": "",
"name": "credovergeneric",
"version": "1.6.7",
"description": "",
"licenses": [],
"purl": "pkg:pypi/credovergeneric@1.6.7",
"type": "library",
"bom-ref": "pkg:pypi/credovergeneric@1.6.7"
},
{
"publisher": "",
"group": "",
"name": "pycryptodome",
"version": "3.12.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/pycryptodome@3.12.0",
"type": "library",
"bom-ref": "pkg:pypi/pycryptodome@3.12.0"
},
{
"publisher": "",
"group": "",
"name": "azure-mgmt-resource",
"version": "20.0.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/azure-mgmt-resource@20.0.0",
"type": "library",
"bom-ref": "pkg:pypi/azure-mgmt-resource@20.0.0"
},
{
"publisher": "",
"group": "",
"name": "fc-cloud-storage-client",
"version": "0.0.14",
"description": "",
"licenses": [],
"purl": "pkg:pypi/fc-cloud-storage-client@0.0.14",
"type": "library",
"bom-ref": "pkg:pypi/fc-cloud-storage-client@0.0.14"
},
{
"publisher": "",
"group": "",
"name": "azure-identity",
"version": "1.7.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/azure-identity@1.7.1",
"type": "library",
"bom-ref": "pkg:pypi/azure-identity@1.7.1"
},
{
"publisher": "",
"group": "",
"name": "dnsdb",
"version": "0.2.5",
"description": "",
"licenses": [],
"purl": "pkg:pypi/dnsdb@0.2.5",
"type": "library",
"bom-ref": "pkg:pypi/dnsdb@0.2.5"
},
{
"publisher": "",
"group": "",
"name": "fc_kb_auth_proxy_client",
"version": "0.0.3",
"description": "",
"licenses": [],
"purl": "pkg:pypi/fc-kb-auth-proxy-client@0.0.3",
"type": "library",
"bom-ref": "pkg:pypi/fc-kb-auth-proxy-client@0.0.3"
}
],
"services": [],
"dependencies": []
}

View File

@ -1,373 +0,0 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:52d87f2e-93ce-4fd6-96d4-071f97ce61a6",
"version": 1,
"metadata": {
"timestamp": "2023-08-11T04:54:21.340Z",
"tools": [
{
"vendor": "cyclonedx",
"name": "cdxgen",
"version": "8.0.4"
}
],
"authors": [
{
"name": "Prabhu Subramanian",
"email": "prabhu@appthreat.com"
}
],
"component": {
"group": "",
"name": "app",
"version": "",
"type": "application"
}
},
"components": [
{
"publisher": "",
"group": "",
"name": "food-exceptions",
"version": "0.4.4",
"description": "",
"licenses": [],
"purl": "pkg:pypi/food-exceptions@0.4.4",
"type": "library",
"bom-ref": "pkg:pypi/food-exceptions@0.4.4"
},
{
"publisher": "",
"group": "",
"name": "food-models",
"version": "3.3.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/food-models@3.3.1",
"type": "library",
"bom-ref": "pkg:pypi/food-models@3.3.1"
},
{
"publisher": "",
"group": "",
"name": "dateutils",
"version": "0.6.6",
"description": "",
"licenses": [],
"purl": "pkg:pypi/dateutils@0.6.6",
"type": "library",
"bom-ref": "pkg:pypi/dateutils@0.6.6"
},
{
"publisher": "",
"group": "",
"name": "publicsuffixlist",
"version": "0.6.2",
"description": "",
"licenses": [],
"purl": "pkg:pypi/publicsuffixlist@0.6.2",
"type": "library",
"bom-ref": "pkg:pypi/publicsuffixlist@0.6.2"
},
{
"publisher": "",
"group": "",
"name": "dnspython",
"version": "1.15.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/dnspython@1.15.0",
"type": "library",
"bom-ref": "pkg:pypi/dnspython@1.15.0"
},
{
"publisher": "",
"group": "",
"name": "netaddr",
"version": "0.7.18",
"description": "",
"licenses": [],
"purl": "pkg:pypi/netaddr@0.7.18",
"type": "library",
"bom-ref": "pkg:pypi/netaddr@0.7.18"
},
{
"publisher": "",
"group": "",
"name": "validators",
"version": "0.12.2",
"description": "",
"licenses": [],
"purl": "pkg:pypi/validators@0.12.2",
"type": "library",
"bom-ref": "pkg:pypi/validators@0.12.2"
},
{
"publisher": "",
"group": "",
"name": "fqdn",
"version": "1.1.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/fqdn@1.1.0",
"type": "library",
"bom-ref": "pkg:pypi/fqdn@1.1.0"
},
{
"publisher": "",
"group": "",
"name": "tld",
"version": "0.9.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/tld@0.9.1",
"type": "library",
"bom-ref": "pkg:pypi/tld@0.9.1"
},
{
"publisher": "",
"group": "",
"name": "cchardet",
"version": "2.1.4",
"description": "",
"licenses": [],
"purl": "pkg:pypi/cchardet@2.1.4",
"type": "library",
"bom-ref": "pkg:pypi/cchardet@2.1.4"
},
{
"publisher": "",
"group": "",
"name": "urllib3",
"version": "1.22",
"description": "",
"licenses": [],
"purl": "pkg:pypi/urllib3@1.22",
"type": "library",
"bom-ref": "pkg:pypi/urllib3@1.22"
},
{
"publisher": "",
"group": "",
"name": "tldextract",
"version": "2.2.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/tldextract@2.2.0",
"type": "library",
"bom-ref": "pkg:pypi/tldextract@2.2.0"
},
{
"publisher": "",
"group": "",
"name": "iptools",
"version": "0.7.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/iptools@0.7.0",
"type": "library",
"bom-ref": "pkg:pypi/iptools@0.7.0"
},
{
"publisher": "",
"group": "",
"name": "parsedatetime",
"version": "2.4",
"description": "",
"licenses": [],
"purl": "pkg:pypi/parsedatetime@2.4",
"type": "library",
"bom-ref": "pkg:pypi/parsedatetime@2.4"
},
{
"publisher": "",
"group": "",
"name": "beautifulsoup4",
"version": "4.7.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/beautifulsoup4@4.7.1",
"type": "library",
"bom-ref": "pkg:pypi/beautifulsoup4@4.7.1"
},
{
"publisher": "",
"group": "",
"name": "filetype",
"version": "1.0.5",
"description": "",
"licenses": [],
"purl": "pkg:pypi/filetype@1.0.5",
"type": "library",
"bom-ref": "pkg:pypi/filetype@1.0.5"
},
{
"publisher": "",
"group": "",
"name": "pyunpack",
"version": "0.1.2",
"description": "",
"licenses": [],
"purl": "pkg:pypi/pyunpack@0.1.2",
"type": "library",
"bom-ref": "pkg:pypi/pyunpack@0.1.2"
},
{
"publisher": "",
"group": "",
"name": "patool",
"version": "1.12",
"description": "",
"licenses": [],
"purl": "pkg:pypi/patool@1.12",
"type": "library",
"bom-ref": "pkg:pypi/patool@1.12"
},
{
"publisher": "",
"group": "",
"name": "wordninja",
"version": "2.0.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/wordninja@2.0.0",
"type": "library",
"bom-ref": "pkg:pypi/wordninja@2.0.0"
},
{
"publisher": "",
"group": "",
"name": "iocextract",
"version": "1.13.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/iocextract@1.13.1",
"type": "library",
"bom-ref": "pkg:pypi/iocextract@1.13.1"
},
{
"publisher": "",
"group": "",
"name": "pyparsing",
"version": "3.0.8",
"description": "",
"licenses": [],
"purl": "pkg:pypi/pyparsing@3.0.8",
"type": "library",
"bom-ref": "pkg:pypi/pyparsing@3.0.8"
},
{
"publisher": "",
"group": "",
"name": "titlecase",
"version": "0.12.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/titlecase@0.12.0",
"type": "library",
"bom-ref": "pkg:pypi/titlecase@0.12.0"
},
{
"publisher": "",
"group": "",
"name": "furl",
"version": "2.1.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/furl@2.1.0",
"type": "library",
"bom-ref": "pkg:pypi/furl@2.1.0"
},
{
"publisher": "",
"group": "",
"name": "pathlib2",
"version": "2.3.3",
"description": "",
"licenses": [],
"purl": "pkg:pypi/pathlib2@2.3.3",
"type": "library",
"bom-ref": "pkg:pypi/pathlib2@2.3.3"
},
{
"publisher": "",
"group": "",
"name": "lxml",
"version": "4.5.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/lxml@4.5.0",
"type": "library",
"bom-ref": "pkg:pypi/lxml@4.5.0"
},
{
"publisher": "",
"group": "",
"name": "fuzzywuzzy",
"version": "0.18.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/fuzzywuzzy@0.18.0",
"type": "library",
"bom-ref": "pkg:pypi/fuzzywuzzy@0.18.0"
},
{
"publisher": "",
"group": "",
"name": "PySocks",
"version": "1.7.0",
"description": "",
"licenses": [],
"purl": "pkg:pypi/pysocks@1.7.0",
"type": "library",
"bom-ref": "pkg:pypi/pysocks@1.7.0"
},
{
"publisher": "",
"group": "",
"name": "truffleHogRegexes",
"version": "0.0.7",
"description": "",
"licenses": [],
"purl": "pkg:pypi/trufflehogregexes@0.0.7",
"type": "library",
"bom-ref": "pkg:pypi/trufflehogregexes@0.0.7"
},
{
"publisher": "",
"group": "",
"name": "soupsieve",
"version": "1.9.1",
"description": "",
"licenses": [],
"purl": "pkg:pypi/soupsieve@1.9.1",
"type": "library",
"bom-ref": "pkg:pypi/soupsieve@1.9.1"
},
{
"publisher": "",
"group": "actions",
"name": "checkout",
"version": "v2",
"description": "",
"licenses": [],
"purl": "pkg:github/actions/checkout@v2",
"type": "application",
"bom-ref": "pkg:github/actions/checkout@v2"
},
{
"publisher": "",
"group": "actions",
"name": "setup-python",
"version": "v2",
"description": "",
"licenses": [],
"purl": "pkg:github/actions/setup-python@v2",
"type": "application",
"bom-ref": "pkg:github/actions/setup-python@v2"
}
],
"services": [],
"dependencies": []
}

File diff suppressed because it is too large Load Diff

136
pkg/parser/cyclonedx.go Normal file
View File

@ -0,0 +1,136 @@
package parser
import (
"bufio"
"fmt"
"os"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/common/purl"
"github.com/safedep/vet/pkg/models"
)
func parseSbomCycloneDxAsGraph(path string, config *ParserConfig) (*models.PackageManifest, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
bom := cdx.NewBOM()
bomReader := bufio.NewReader(file)
format := cdx.BOMFileFormatJSON
if len(path) > 4 && path[len(path)-4:] == ".xml" {
format = cdx.BOMFileFormatXML
}
decoder := cdx.NewBOMDecoder(bomReader, format)
if err = decoder.Decode(bom); err != nil {
return nil, err
}
// Fail fast if the BOM does not have the main (app) component
if bom.Metadata == nil || bom.Metadata.Component == nil {
return nil, fmt.Errorf("Invalid CycloneDX SBOM: Metadata or Component is nil")
}
// Maintain a cache of BOM / packageUrl ref to package mapping for re-use while adding
// dependency relations
bomRefMap := make(map[string]*models.Package)
// Lets start by adding the main component
ref, pkg, err := cdxExtractPackageFromComponent(*bom.Metadata.Component)
if err != nil {
return nil, fmt.Errorf("failed to extract main package from component: %v", err)
}
bomRefMap[ref] = pkg
manifest := models.NewPackageManifest(path, models.EcosystemCyDxSBOM)
components := utils.SafelyGetValue(bom.Components)
// Iterate over all components in the BOM and add the package in dependency graph
// This just adds the nodes in the graph without any relations
for _, component := range components {
ref, pkg, err := cdxExtractPackageFromComponent(component)
if err != nil {
logger.Errorf("Failed to extract package from component %v: %v",
component, err)
continue
}
bomRefMap[ref] = pkg
manifest.AddPackage(pkg)
}
// Iterate over the dependency relations and add the edges in the graph
depedencyRelations := utils.SafelyGetValue(bom.Dependencies)
for _, dependencyRelation := range depedencyRelations {
// We must have seen the package while enumerating components without which
// we cannot add a dependency relation
pkg, ok := bomRefMap[dependencyRelation.Ref]
if !ok {
logger.Errorf("Dependency ref: %s not found in bomRefMap", dependencyRelation.Ref)
continue
}
// We lookup the package in the bomRefMap and add the dependency relation
// We fail if we cannot find the package because as per CycloneDX spec it seems
// every known component must be defined.
dependencies := utils.SafelyGetValue(dependencyRelation.Dependencies)
for _, dependency := range dependencies {
dependsOnPkg, ok := bomRefMap[dependency]
if !ok {
logger.Errorf("%s depends on %s which is not found in bomRefMap",
dependencyRelation.Ref, dependency)
continue
}
if cdxIsMainComponent(bom, dependencyRelation.Ref) {
manifest.DependencyGraph.AddRootNode(dependsOnPkg)
} else {
manifest.DependencyGraph.AddDependency(pkg, dependsOnPkg)
}
}
}
logger.Infof("Resolved %d packages as graph from BOM: %s",
len(manifest.GetPackages()), path)
// We consider that a dependency graph is constructed from BOM
// only when we find at least 1 dependency relation.
if len(depedencyRelations) > 0 {
manifest.DependencyGraph.SetPresent(true)
}
return manifest, nil
}
func cdxIsMainComponent(bom *cdx.BOM, ref string) bool {
return bom.Metadata != nil && bom.Metadata.Component != nil &&
(bom.Metadata.Component.PackageURL == ref || bom.Metadata.Component.BOMRef == ref)
}
func cdxExtractPackageFromComponent(component cdx.Component) (string, *models.Package, error) {
pUrl := component.PackageURL
if pUrl == "" {
pUrl = component.BOMRef
}
if pUrl == "" {
return "", nil, fmt.Errorf("Invalid CycloneDX SBOM: PackageURL or BOMRef is nil")
}
parsedPurl, err := purl.ParsePackageUrl(pUrl)
if err != nil {
return "", nil, err
}
return pUrl, &models.Package{
PackageDetails: parsedPurl.GetPackageDetails(),
}, nil
}

View File

@ -0,0 +1,149 @@
package parser
import (
"os"
"slices"
"testing"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/google/osv-scanner/pkg/lockfile"
"github.com/safedep/vet/pkg/common/purl"
"github.com/safedep/vet/pkg/models"
"github.com/stretchr/testify/assert"
)
func TestParseCyclonedxSBOM(t *testing.T) {
tempFile, _ := os.CreateTemp("", "sbom_*.json")
defer os.Remove(tempFile.Name())
defer tempFile.Close()
sbomContent := `{
"bomFormat": "CycloneDX",
"metadata": {
"component": {
"name": "mybigapp",
"version": "2.26.0",
"type": "application",
"purl": "pkg:pypi/mybigapp@2.26.0"
}
},
"components": [
{
"group": "",
"name": "requests",
"version": "1.0",
"purl": "pkg:pypi/requests@2.26.0"
},
{
"group": "testgroup",
"name": "lodash",
"version": "2.0",
"purl": "pkg:npm/testgroup/lodash@4.17.21"
}
]
}`
err := os.WriteFile(tempFile.Name(), []byte(sbomContent), 0644)
assert.Nil(t, err)
manifest, err := parseSbomCycloneDxAsGraph(tempFile.Name(), &ParserConfig{})
assert.Nil(t, err)
packages := manifest.GetPackages()
assert.Equal(t, manifest.GetDisplayPath(), tempFile.Name())
assert.Len(t, packages, 2)
b := slices.ContainsFunc(packages, func(pkg *models.Package) bool {
return pkg.GetName() == "requests" &&
pkg.GetVersion() == "2.26.0"
})
assert.True(t, b)
b = slices.ContainsFunc(packages, func(pkg *models.Package) bool {
return pkg.GetName() == "testgroup/lodash" &&
pkg.GetVersion() == "4.17.21"
})
assert.True(t, b)
}
func TestConvertSbomComponentToPackage(t *testing.T) {
component := cdx.Component{
Group: "",
Name: "requests",
Version: "2.26.0",
PackageURL: "pkg:pypi/requests@2.26.0",
}
ref, pd, err := cdxExtractPackageFromComponent(component)
assert.Nil(t, err)
assert.Equal(t, component.PackageURL, ref)
assert.Equal(t, "requests", pd.Name)
assert.Equal(t, "2.26.0", pd.Version)
assert.Equal(t, lockfile.PipEcosystem, pd.Ecosystem)
}
func TestParseCyclonedxSBOMWithEmptyComponents(t *testing.T) {
tempFile, _ := os.CreateTemp("", "sbom_*.json")
defer os.Remove(tempFile.Name())
defer tempFile.Close()
sbomContent := `{}`
err := os.WriteFile(tempFile.Name(), []byte(sbomContent), 0644)
assert.Nil(t, err)
_, err = parseSbomCycloneDxAsGraph(tempFile.Name(), &ParserConfig{})
assert.NotNil(t, err)
}
func TestParseCyclonedxSBOMWithGradleSBOM(t *testing.T) {
manifest, err := parseSbomCycloneDxAsGraph("./fixtures/bom-maven.json", &ParserConfig{})
assert.Nil(t, err)
assert.NotNil(t, manifest)
assert.NotEmpty(t, manifest.GetPackages())
dg := manifest.DependencyGraph
assert.NotEmpty(t, dg.GetNodes())
assert.NotEmpty(t, dg.GetPackages())
pkg, err := purl.ParsePackageUrl("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.0?type=jar")
assert.Nil(t, err)
nodes := dg.GetDependencies(&models.Package{PackageDetails: pkg.GetPackageDetails()})
assert.Equal(t, 2, len(nodes))
assert.Equal(t, "com.fasterxml.jackson.core:jackson-annotations", nodes[0].GetName())
assert.Equal(t, "com.fasterxml.jackson.core:jackson-core", nodes[1].GetName())
}
func TestParseCyclonedxSBOMWithMavenSBOM(t *testing.T) {
manifest, err := parseSbomCycloneDxAsGraph("./fixtures/bom-dropwizard-cdx-example.json", &ParserConfig{})
assert.Nil(t, err)
assert.NotNil(t, manifest)
packages := manifest.GetPackages()
assert.Equal(t, 167, len(packages))
pkg, err := purl.ParsePackageUrl("pkg:maven/io.dropwizard/dropwizard-client@1.3.15?type=jar")
assert.Nil(t, err)
nodes := manifest.DependencyGraph.GetDependencies(&models.Package{PackageDetails: pkg.GetPackageDetails()})
assert.Equal(t, 5, len(nodes))
nodes = manifest.DependencyGraph.PathToRoot(&models.Package{PackageDetails: pkg.GetPackageDetails()})
assert.Equal(t, 1, len(nodes))
}
func TestParseCyclonedxSBOMWithNpmSBOM(t *testing.T) {
manifest, err := parseSbomCycloneDxAsGraph("./fixtures/bom-juiceshop-cdx-example.json", &ParserConfig{})
assert.Nil(t, err)
assert.NotNil(t, manifest)
assert.Equal(t, 840, len(manifest.GetPackages()))
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,6 @@ import (
"github.com/safedep/vet/pkg/models"
"github.com/safedep/vet/pkg/parser/custom/py"
cdx "github.com/safedep/vet/pkg/parser/custom/sbom/cyclonedx"
"github.com/safedep/vet/pkg/parser/custom/sbom/spdx"
)
@ -42,10 +41,9 @@ var supportedEcosystems map[string]bool = map[string]bool{
// TODO: Migrate these to graph parser
var customExperimentalParsers map[string]lockfile.PackageDetailsParser = map[string]lockfile.PackageDetailsParser{
customParserTypePyWheel: parsePythonWheelDist,
customParserCycloneDXSBOM: cdx.Parse,
customParserSpdxSBOM: spdx.Parse,
customParserTypeSetupPy: py.ParseSetuppy,
customParserTypePyWheel: parsePythonWheelDist,
customParserSpdxSBOM: spdx.Parse,
customParserTypeSetupPy: py.ParseSetuppy,
}
type Parser interface {
@ -73,7 +71,8 @@ type dependencyGraphParser func(lockfilePath string, config *ParserConfig) (*mod
// Maintain a map of lockfileAs to dependencyGraphParser
var dependencyGraphParsers map[string]dependencyGraphParser = map[string]dependencyGraphParser{
"package-lock.json": parseNpmPackageLockAsGraph,
"package-lock.json": parseNpmPackageLockAsGraph,
customParserCycloneDXSBOM: parseSbomCycloneDxAsGraph,
}
func List(experimental bool) []string {

View File

@ -9,7 +9,7 @@ import (
func TestListParser(t *testing.T) {
parsers := List(false)
assert.Equal(t, 12, len(parsers))
assert.Equal(t, 13, len(parsers))
}
func TestInvalidEcosystemMapping(t *testing.T) {

View File

@ -80,6 +80,9 @@ func (r *DotGraphReporter) dotRenderDependencyGraph(dg *models.DependencyGraph[*
sb.WriteString(" rankdir=LR;\n")
sb.WriteString(" node [shape=box];\n")
// Add a dummy root node
sb.WriteString(" \"root\";\n")
// Generate the node names
for _, node := range dg.GetNodes() {
sb.WriteString(" ")
@ -89,6 +92,14 @@ func (r *DotGraphReporter) dotRenderDependencyGraph(dg *models.DependencyGraph[*
// Add the relations
for _, node := range dg.GetNodes() {
if node.Root {
sb.WriteString(" ")
sb.WriteString("\"root\"")
sb.WriteString(" -> ")
sb.WriteString("\"" + r.nodeNameForPackage(node.Data) + "\"")
sb.WriteString(";\n")
}
for _, edge := range node.Children {
sb.WriteString(" ")
sb.WriteString("\"" + r.nodeNameForPackage(node.Data) + "\"")