From fbe0258e991adf90b7d41bfc756cb44ac1098d7e Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 19 Jan 2026 11:49:34 +0200 Subject: [PATCH 1/5] Static-SCA: Fix direct component in Sbom calculation for impacted component and add ID to simplejson.ComponentRow --- policy/enforcer/policyenforcer.go | 2 +- utils/formats/cdxutils/cyclonedxutils.go | 22 ++++++++- utils/formats/simplejsonapi.go | 1 + utils/results/common.go | 47 ++++++++++++++++--- utils/results/conversion/convertor.go | 8 +--- .../cyclonedxparser/cyclonedxparser.go | 2 +- .../conversion/sarifparser/sarifparser.go | 24 ++++------ .../simplejsonparser/simplejsonparser.go | 20 +++++--- .../conversion/summaryparser/summaryparser.go | 2 +- .../conversion/tableparser/tableparser.go | 4 +- 10 files changed, 90 insertions(+), 42 deletions(-) diff --git a/policy/enforcer/policyenforcer.go b/policy/enforcer/policyenforcer.go index e9eabd00c..b63ad3a53 100644 --- a/policy/enforcer/policyenforcer.go +++ b/policy/enforcer/policyenforcer.go @@ -241,8 +241,8 @@ func locateBomComponentInfo(cmdResults *results.SecurityCommandResults, impacted if target.ScaResults.Sbom.Dependencies != nil { dependencies = *target.ScaResults.Sbom.Dependencies } - directComponents = results.GetDirectDependenciesAsComponentRows(component, *target.ScaResults.Sbom.Components, dependencies) impactPaths = results.BuildImpactPath(component, *target.ScaResults.Sbom.Components, dependencies...) + directComponents = results.ExtractComponentDirectComponentsInBOM(target.ScaResults.Sbom, component, impactPaths) break } } diff --git a/utils/formats/cdxutils/cyclonedxutils.go b/utils/formats/cdxutils/cyclonedxutils.go index 3700410fd..d9aa94fa9 100644 --- a/utils/formats/cdxutils/cyclonedxutils.go +++ b/utils/formats/cdxutils/cyclonedxutils.go @@ -32,11 +32,14 @@ const ( // Indicates that the component is a root component in the BOM RootRelation ComponentRelation = "root" // Indicates that the component is a direct dependency of another component - DirectRelation ComponentRelation = "direct_dependency" + DirectRelation ComponentRelation = "direct" // Indicates that the component is a transitive dependency of another component - TransitiveRelation ComponentRelation = "transitive_dependency" + TransitiveRelation ComponentRelation = "transitive" // Undefined relation UnknownRelation ComponentRelation = "" + + // JFrog specific properties + JfrogRelationProperty = "jfrog:dependency:type" ) type ComponentRelation string @@ -96,6 +99,17 @@ func SearchDependencyEntry(dependencies *[]cyclonedx.Dependency, ref string) *cy return nil } +func GetJfrogRelationProperty(component *cyclonedx.Component) ComponentRelation { + if component == nil || component.Properties == nil || len(*component.Properties) == 0 { + return UnknownRelation + } + property := GetProperty(component.Properties, JfrogRelationProperty) + if property == nil || property.Value == "" { + return UnknownRelation + } + return ComponentRelation(property.Value) +} + func GetComponentRelation(bom *cyclonedx.BOM, componentRef string, skipDefaultRoot bool) ComponentRelation { if bom == nil || bom.Components == nil { return UnknownRelation @@ -105,6 +119,10 @@ func GetComponentRelation(bom *cyclonedx.BOM, componentRef string, skipDefaultRo // The component is not found in the BOM components or not library, return UnknownRelation return UnknownRelation } + // Check if the component has a JFrog specific relation property + if relation := GetJfrogRelationProperty(component); relation != UnknownRelation { + return relation + } dependencies := []cyclonedx.Dependency{} if bom.Dependencies != nil { dependencies = *bom.Dependencies diff --git a/utils/formats/simplejsonapi.go b/utils/formats/simplejsonapi.go index 18481f404..b9fb7a2bf 100644 --- a/utils/formats/simplejsonapi.go +++ b/utils/formats/simplejsonapi.go @@ -138,6 +138,7 @@ func (l Location) ToString() string { } type ComponentRow struct { + Id string `json:"id,omitempty"` Name string `json:"name"` Version string `json:"version"` Location *Location `json:"location,omitempty"` diff --git a/utils/results/common.go b/utils/results/common.go index 5cd34e72d..6eb626d4d 100644 --- a/utils/results/common.go +++ b/utils/results/common.go @@ -257,7 +257,12 @@ func getDirectComponentsAndImpactPaths(target string, impactPaths [][]services.I componentId := impactPath[impactPathIndex].ComponentId if _, exist := componentsMap[componentId]; !exist { compName, compVersion, _ := techutils.SplitComponentIdRaw(componentId) - componentsMap[componentId] = formats.ComponentRow{Name: compName, Version: compVersion, Location: getComponentLocation(impactPath[impactPathIndex].FullPath, target)} + componentsMap[componentId] = formats.ComponentRow{ + Id: componentId, + Name: compName, + Version: compVersion, + Location: getComponentLocation(impactPath[impactPathIndex].FullPath, target), + } } // Convert the impact path @@ -265,6 +270,7 @@ func getDirectComponentsAndImpactPaths(target string, impactPaths [][]services.I for _, pathNode := range impactPath { nodeCompName, nodeCompVersion, _ := techutils.SplitComponentIdRaw(pathNode.ComponentId) compImpactPathRows = append(compImpactPathRows, formats.ComponentRow{ + Id: pathNode.ComponentId, Name: nodeCompName, Version: nodeCompVersion, Location: getComponentLocation(pathNode.FullPath), @@ -286,8 +292,10 @@ func BuildImpactPath(affectedComponent cyclonedx.Component, components []cyclone impactedPath := buildImpactPathForComponent(parent, componentAppearances, components, dependencies...) // Add the affected component at the end of the impact path impactedPath = append(impactedPath, formats.ComponentRow{ - Name: affectedComponent.Name, - Version: affectedComponent.Version, + Id: affectedComponent.BOMRef, + Name: affectedComponent.Name, + Version: affectedComponent.Version, + Location: CdxEvidenceToLocation(affectedComponent), }) // Add the impact path to the list of impact paths impactPathsRows = append(impactPathsRows, impactedPath) @@ -300,8 +308,10 @@ func buildImpactPathForComponent(component cyclonedx.Component, componentAppeara // Build the impact path for the component impactPath = []formats.ComponentRow{ { - Name: component.Name, - Version: component.Version, + Id: component.BOMRef, + Name: component.Name, + Version: component.Version, + Location: CdxEvidenceToLocation(component), }, } // Add the parent components to the impact path @@ -1369,9 +1379,34 @@ func CdxToFixedVersions(affectedVersions *[]cyclonedx.AffectedVersions) (fixedVe return } -func GetDirectDependenciesAsComponentRows(component cyclonedx.Component, components []cyclonedx.Component, dependencies []cyclonedx.Dependency) (directComponents []formats.ComponentRow) { +func ExtractComponentDirectComponentsInBOM(bom *cyclonedx.BOM, component cyclonedx.Component, impactPaths [][]formats.ComponentRow) (directComponents []formats.ComponentRow) { + if relation := cdxutils.GetComponentRelation(bom, component.BOMRef, true); relation == cdxutils.RootRelation || relation == cdxutils.DirectRelation { + // The component is a root or direct dependency, no parents to extract, return the component itself + directComponents = append(directComponents, formats.ComponentRow{ + Id: component.BOMRef, + Name: component.Name, + Version: component.Version, + Location: CdxEvidenceToLocation(component), + }) + return + } + // The component is a transitive dependency, go over path from start until we find the first direct dependency relation + for _, path := range impactPaths { + for _, pathComponent := range path { + if relation := cdxutils.GetComponentRelation(bom, pathComponent.Id, true); relation == cdxutils.RootRelation || relation == cdxutils.DirectRelation { + // Found the first direct dependency in the path, add it to the direct components and stop processing this path + directComponents = append(directComponents, pathComponent) + break + } + } + } + return +} + +func GetParentsAsComponentRows(component cyclonedx.Component, components []cyclonedx.Component, dependencies []cyclonedx.Dependency) (directComponents []formats.ComponentRow) { for _, parent := range cdxutils.SearchParents(component.BOMRef, components, dependencies...) { directComponents = append(directComponents, formats.ComponentRow{ + Id: parent.BOMRef, Name: parent.Name, Version: parent.Version, Location: CdxEvidenceToLocation(parent), diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go index 3c9d6ff7f..493e9498a 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -66,7 +66,7 @@ type ResultsStreamFormatParser[T interface{}] interface { DeprecatedParseLicenses(scaResponse services.ScanResponse) error // Parse SCA content to the current scan target ParseSbom(sbom *cyclonedx.BOM) error - ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) error + ParseSbomLicenses(sbom *cyclonedx.BOM) error ParseCVEs(enrichedSbom *cyclonedx.BOM, applicableScan ...[]*sarif.Run) error // Parse JAS content to the current scan target ParseSecrets(secrets ...[]*sarif.Run) error @@ -169,11 +169,7 @@ func parseScaResults[T interface{}](params ResultConvertParams, parser ResultsSt } // Must be called last for cyclonedxparser to be able to attach the licenses to all the components if params.IncludeLicenses && targetScansResults.ScaResults.Sbom.Components != nil { - dependencies := []cyclonedx.Dependency{} - if targetScansResults.ScaResults.Sbom.Dependencies != nil { - dependencies = append(dependencies, *targetScansResults.ScaResults.Sbom.Dependencies...) - } - if err = parser.ParseSbomLicenses(*targetScansResults.ScaResults.Sbom.Components, dependencies...); err != nil { + if err = parser.ParseSbomLicenses(targetScansResults.ScaResults.Sbom); err != nil { return } } diff --git a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go index 6e2d2d069..b74a2ffb4 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -124,7 +124,7 @@ func (cdc *CmdResultsCycloneDxConverter) ParseSbom(sbom *cyclonedx.BOM) (err err return } -func (cdc *CmdResultsCycloneDxConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (cdc *CmdResultsCycloneDxConverter) ParseSbomLicenses(_ *cyclonedx.BOM) (err error) { // In CycloneDX, licenses are part of the components and dependencies, so we don't need to parse them separately. return nil } diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index e577f4123..fb7811d63 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -324,7 +324,7 @@ func (sc *CmdResultsSarifConverter) ParseSbom(_ *cyclonedx.BOM) (err error) { return } -func (sc *CmdResultsSarifConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (sc *CmdResultsSarifConverter) ParseSbomLicenses(_ *cyclonedx.BOM) (err error) { // Not supported in Sarif format return } @@ -349,15 +349,16 @@ func (sc *CmdResultsSarifConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, appli func addCdxScaVulnerability(cmdType utils.CommandType, enrichedSbom *cyclonedx.BOM, sarifResults *[]*sarif.Result, rules *map[string]*sarif.ReportingDescriptor) results.ParseBomScaVulnerabilityFunc { return func(vulnerability cyclonedx.Vulnerability, component cyclonedx.Component, fixedVersion *[]cyclonedx.AffectedVersions, applicability *formats.Applicability, severity severityutils.Severity) (e error) { // Prepare the required fields - directDependencies := getDirectDependenciesForSarif(component, enrichedSbom) - applicabilityStatus, maxCveScore, cves, fixedVersions, markdownDescription, e := prepareCdxInfoForSarif(vulnerability, severity, applicability, directDependencies, fixedVersion) - if e != nil { - return - } dependencies := []cyclonedx.Dependency{} if enrichedSbom.Dependencies != nil { dependencies = append(dependencies, *enrichedSbom.Dependencies...) } + impactPaths := results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...) + directDependencies := results.ExtractComponentDirectComponentsInBOM(enrichedSbom, component, impactPaths) + applicabilityStatus, maxCveScore, cves, fixedVersions, markdownDescription, e := prepareCdxInfoForSarif(vulnerability, severity, applicability, directDependencies, fixedVersion) + if e != nil { + return + } compName, compVersion, _ := techutils.SplitPackageURL(component.PackageURL) createAndAddScaIssue(scaParseParams{ CmdType: cmdType, @@ -374,21 +375,12 @@ func addCdxScaVulnerability(cmdType utils.CommandType, enrichedSbom *cyclonedx.B AddFixedVersionProperty: true, FixedVersions: fixedVersions, DirectComponents: directDependencies, - ImpactPaths: results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...), + ImpactPaths: impactPaths, }, sarifResults, rules) return } } -func getDirectDependenciesForSarif(component cyclonedx.Component, enrichedSbom *cyclonedx.BOM) (directDependencies []formats.ComponentRow) { - // Extract the direct dependencies - dependencies := []cyclonedx.Dependency{} - if enrichedSbom.Dependencies != nil { - dependencies = append(dependencies, *enrichedSbom.Dependencies...) - } - return results.GetDirectDependenciesAsComponentRows(component, *enrichedSbom.Components, dependencies) -} - func prepareCdxInfoForSarif(vulnerability cyclonedx.Vulnerability, severity severityutils.Severity, applicability *formats.Applicability, directDependencies []formats.ComponentRow, fixedVersion *[]cyclonedx.AffectedVersions) (applicabilityStatus jasutils.ApplicabilityStatus, maxCveScore string, cves []formats.CveRow, fixedVersions []string, markdownDescription string, err error) { // Extract the applicability status applicabilityStatus = jasutils.NotScanned diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index 1b5e673e4..36dcde021 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -86,15 +86,19 @@ func (sjc *CmdResultsSimpleJsonConverter) DeprecatedParseScaVulnerabilities(desc return } -func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(sbom *cyclonedx.BOM) (err error) { if sjc.current == nil { return results.ErrResetConvertor } - if len(components) == 0 { + if sbom == nil || sbom.Components == nil || len(*sbom.Components) == 0 { return } + dependencies := []cyclonedx.Dependency{} + if sbom.Dependencies != nil { + dependencies = append(dependencies, *sbom.Dependencies...) + } // Iterate through the components and collect licenses - for _, component := range components { + for _, component := range *sbom.Components { if component.Licenses == nil || len(*component.Licenses) == 0 { // No licenses found for this component, continue to the next one continue @@ -109,6 +113,7 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(components []cyclone if name == "" { name = license.License.ID } + impactPaths := results.BuildImpactPath(component, *sbom.Components, dependencies...) sjc.current.Licenses = append(sjc.current.Licenses, formats.LicenseRow{ LicenseKey: license.License.ID, LicenseName: name, @@ -116,9 +121,9 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSbomLicenses(components []cyclone ImpactedDependencyName: strings.ReplaceAll(compName, "/", ":"), ImpactedDependencyVersion: compVersion, ImpactedDependencyType: techutils.ConvertXrayPackageType(techutils.CdxPackageTypeToXrayPackageType(compType)), - Components: results.GetDirectDependenciesAsComponentRows(component, components, dependencies), + Components: results.ExtractComponentDirectComponentsInBOM(sbom, component, impactPaths), }, - ImpactPaths: results.BuildImpactPath(component, components, dependencies...), + ImpactPaths: impactPaths, }) } } @@ -135,6 +140,7 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, if enrichedSbom.Dependencies != nil { dependencies = append(dependencies, *enrichedSbom.Dependencies...) } + impactPaths := results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...) // Convert the CycloneDX vulnerability to a simple JSON vulnerability row sjc.current.Vulnerabilities = append(sjc.current.Vulnerabilities, sjc.createVulnerabilityOrViolationRowFromCdx( vulnerability.ID, @@ -143,8 +149,8 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, applicability, vulnerability, component, - results.GetDirectDependenciesAsComponentRows(component, *enrichedSbom.Components, dependencies), - results.BuildImpactPath(component, *enrichedSbom.Components, dependencies...), + results.ExtractComponentDirectComponentsInBOM(enrichedSbom, component, impactPaths), + impactPaths, fixedVersions, // TODO: implement JfrogResearchInformation conversion nil, diff --git a/utils/results/conversion/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go index 0c7a547be..0b2fcbe9c 100644 --- a/utils/results/conversion/summaryparser/summaryparser.go +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -176,7 +176,7 @@ func (sc *CmdResultsSummaryConverter) ParseSbom(_ *cyclonedx.BOM) (err error) { return } -func (sc *CmdResultsSummaryConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { +func (sc *CmdResultsSummaryConverter) ParseSbomLicenses(_ *cyclonedx.BOM) (err error) { // Not supported in the summary return } diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go index 0785c2d10..1ff074132 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -68,8 +68,8 @@ func (tc *CmdResultsTableConverter) DeprecatedParseLicenses(scaResponse services return tc.simpleJsonConvertor.DeprecatedParseLicenses(scaResponse) } -func (tc *CmdResultsTableConverter) ParseSbomLicenses(components []cyclonedx.Component, dependencies ...cyclonedx.Dependency) (err error) { - return tc.simpleJsonConvertor.ParseSbomLicenses(components, dependencies...) +func (tc *CmdResultsTableConverter) ParseSbomLicenses(sbom *cyclonedx.BOM) (err error) { + return tc.simpleJsonConvertor.ParseSbomLicenses(sbom) } func (tc *CmdResultsTableConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, applicableScan ...[]*sarif.Run) (err error) { From f793225790a632deaf6c17f95c8b1ee6693e0e0b Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 19 Jan 2026 13:40:19 +0200 Subject: [PATCH 2/5] fix unit tests --- policy/local/localconvertor_test.go | 6 +++--- utils/results/common_test.go | 14 ++++++------- .../simplejsonparser/simplejsonparser_test.go | 21 ++++++++++++------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/policy/local/localconvertor_test.go b/policy/local/localconvertor_test.go index a4abe30e1..fdb2890c9 100644 --- a/policy/local/localconvertor_test.go +++ b/policy/local/localconvertor_test.go @@ -214,10 +214,10 @@ func createScaTestViolation(id, component string, vioType violationutils.Violati Type: cyclonedx.ComponentTypeLibrary, Name: component, }, - DirectComponents: []formats.ComponentRow{{Name: component}}, + DirectComponents: []formats.ComponentRow{{Id: component, Name: component}}, ImpactPaths: [][]formats.ComponentRow{{ - {Name: "root"}, - {Name: component}, + {Id: "root", Name: "root"}, + {Id: component, Name: component}, }}, } } diff --git a/utils/results/common_test.go b/utils/results/common_test.go index 983b393ca..dd5575756 100644 --- a/utils/results/common_test.go +++ b/utils/results/common_test.go @@ -606,25 +606,25 @@ func TestGetDirectComponents(t *testing.T) { { name: "one direct component", impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack:1.2.3"}}}, - expectedDirectComponentRows: []formats.ComponentRow{{Name: "jfrog:pack", Version: "1.2.3"}}, - expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack", Version: "1.2.3"}}}, + expectedDirectComponentRows: []formats.ComponentRow{{Id: "gav://jfrog:pack:1.2.3", Name: "jfrog:pack", Version: "1.2.3"}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Id: "gav://jfrog:pack:1.2.3", Name: "jfrog:pack", Version: "1.2.3"}}}, }, { name: "one direct component with target", target: filepath.Join("root", "dir", "file"), impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack2:1.2.3"}}}, - expectedDirectComponentRows: []formats.ComponentRow{{Name: "jfrog:pack2", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}}, - expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack2", Version: "1.2.3"}}}, + expectedDirectComponentRows: []formats.ComponentRow{{Id: "gav://jfrog:pack2:1.2.3", Name: "jfrog:pack2", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Id: "gav://jfrog:pack1:1.2.3", Name: "jfrog:pack1", Version: "1.2.3"}, {Id: "gav://jfrog:pack2:1.2.3", Name: "jfrog:pack2", Version: "1.2.3"}}}, }, { name: "multiple direct components", target: filepath.Join("root", "dir", "file"), impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack21:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}, {services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack22:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}}, expectedDirectComponentRows: []formats.ComponentRow{ - {Name: "jfrog:pack21", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, - {Name: "jfrog:pack22", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, + {Id: "gav://jfrog:pack21:1.2.3", Name: "jfrog:pack21", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, + {Id: "gav://jfrog:pack22:1.2.3", Name: "jfrog:pack22", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, }, - expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack21", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}, {{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack22", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Id: "gav://jfrog:pack1:1.2.3", Name: "jfrog:pack1", Version: "1.2.3"}, {Id: "gav://jfrog:pack21:1.2.3", Name: "jfrog:pack21", Version: "1.2.3"}, {Id: "gav://jfrog:pack3:1.2.3", Name: "jfrog:pack3", Version: "1.2.3"}}, {{Id: "gav://jfrog:pack1:1.2.3", Name: "jfrog:pack1", Version: "1.2.3"}, {Id: "gav://jfrog:pack22:1.2.3", Name: "jfrog:pack22", Version: "1.2.3"}, {Id: "gav://jfrog:pack3:1.2.3", Name: "jfrog:pack3", Version: "1.2.3"}}}, }, } diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go index 6da577b98..d792a701e 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go @@ -239,11 +239,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-A", // Direct Components: []formats.ComponentRow{{ + Id: "component-A", Name: "component-A", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-A", Name: "component-A"}}}, }, { Summary: "summary-1", @@ -254,11 +255,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, { Summary: "summary-2", @@ -269,11 +271,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, }, }, @@ -309,11 +312,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-A", // Direct Components: []formats.ComponentRow{{ + Id: "component-A", Name: "component-A", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-A", Name: "component-A"}}}, }, { Summary: "summary-1", @@ -331,11 +335,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, { Summary: "summary-2", @@ -360,11 +365,12 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "descriptor.json"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, }, }, @@ -414,11 +420,12 @@ func TestPrepareSimpleJsonLicenses(t *testing.T) { ImpactedDependencyName: "component-B", // Direct Components: []formats.ComponentRow{{ + Id: "component-B", Name: "component-B", Location: &formats.Location{File: "target"}, }}, }, - ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + ImpactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "root"}, {Id: "component-B", Name: "component-B"}}}, }, }, }, From 1db7b87135d6a28c460a3c8023e2e7e52807d9b8 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 19 Jan 2026 14:21:26 +0200 Subject: [PATCH 3/5] add tests for ExtractComponentDirectComponentsInBOM and more --- utils/formats/cdxutils/cyclonedxutils.go | 14 ++ utils/formats/cdxutils/cyclonedxutils_test.go | 21 ++ utils/results/common.go | 2 +- utils/results/common_test.go | 212 ++++++++++++++++++ 4 files changed, 248 insertions(+), 1 deletion(-) diff --git a/utils/formats/cdxutils/cyclonedxutils.go b/utils/formats/cdxutils/cyclonedxutils.go index d9aa94fa9..5ddd1cc61 100644 --- a/utils/formats/cdxutils/cyclonedxutils.go +++ b/utils/formats/cdxutils/cyclonedxutils.go @@ -196,6 +196,20 @@ func GetRootDependenciesEntries(bom *cyclonedx.BOM, skipDefaultRoot bool) (roots if bom == nil || bom.Components == nil || len(*bom.Components) == 0 { return } + // First, let collect all Jfrog defined root components if exists + for _, comp := range *bom.Components { + if GetJfrogRelationProperty(&comp) == RootRelation { + if compDepEntry := SearchDependencyEntry(bom.Dependencies, comp.BOMRef); compDepEntry != nil { + roots = append(roots, *compDepEntry) + } else { + roots = append(roots, cyclonedx.Dependency{Ref: comp.BOMRef}) + } + } + } + if len(roots) > 0 { + // Jfrog defined roots found, return them + return + } // Create a Set to track all references that are listed in `dependsOn` refs := datastructures.MakeSet[string]() dependedRefs := datastructures.MakeSet[string]() diff --git a/utils/formats/cdxutils/cyclonedxutils_test.go b/utils/formats/cdxutils/cyclonedxutils_test.go index 44e8628f2..8b484753d 100644 --- a/utils/formats/cdxutils/cyclonedxutils_test.go +++ b/utils/formats/cdxutils/cyclonedxutils_test.go @@ -809,6 +809,27 @@ func TestGetRootDependenciesEntries(t *testing.T) { // file1 is not included because it's not a library type }, }, + { + name: "Multiple roots with jfrog:dependency:type property", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root1", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 1", Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(RootRelation)}}}, + {BOMRef: "root2", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 2", Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(RootRelation)}}}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(DirectRelation)}}}, + {BOMRef: "trans1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root1", Dependencies: &[]string{"root2"}}, + {Ref: "root2", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"trans1"}}, + }, + }, + skipRoot: true, + expected: []cyclonedx.Dependency{ + {Ref: "root1"}, + {Ref: "root2"}, + }, + }, } for _, tt := range tests { diff --git a/utils/results/common.go b/utils/results/common.go index 6eb626d4d..130336150 100644 --- a/utils/results/common.go +++ b/utils/results/common.go @@ -1393,7 +1393,7 @@ func ExtractComponentDirectComponentsInBOM(bom *cyclonedx.BOM, component cyclone // The component is a transitive dependency, go over path from start until we find the first direct dependency relation for _, path := range impactPaths { for _, pathComponent := range path { - if relation := cdxutils.GetComponentRelation(bom, pathComponent.Id, true); relation == cdxutils.RootRelation || relation == cdxutils.DirectRelation { + if relation := cdxutils.GetComponentRelation(bom, pathComponent.Id, true); relation == cdxutils.DirectRelation { // Found the first direct dependency in the path, add it to the direct components and stop processing this path directComponents = append(directComponents, pathComponent) break diff --git a/utils/results/common_test.go b/utils/results/common_test.go index dd5575756..691fdb38f 100644 --- a/utils/results/common_test.go +++ b/utils/results/common_test.go @@ -637,6 +637,218 @@ func TestGetDirectComponents(t *testing.T) { } } +func TestExtractComponentDirectComponentsInBOM(t *testing.T) { + tests := []struct { + name string + bom *cyclonedx.BOM + component cyclonedx.Component + impactPaths [][]formats.ComponentRow + expectedDirects []formats.ComponentRow + }{ + { + name: "Component is root dependency - returns component itself", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "comp1", Type: cyclonedx.ComponentTypeLibrary, Name: "Component 1", Version: "1.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"comp1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "root", Name: "Root Component", Version: "1.0.0"}, + impactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "Root Component", Version: "1.0.0"}}}, + expectedDirects: []formats.ComponentRow{ + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + }, + }, + { + name: "Component is direct dependency - returns component itself", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "direct1", Name: "Direct 1", Version: "2.0.0"}, + impactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "Root Component", Version: "1.0.0"}, {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}}}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + }, + }, + { + name: "Component is transitive - returns first direct from impact path", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"transitive1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + impactPaths: [][]formats.ComponentRow{{ + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + }}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + }, + }, + { + name: "Component is transitive - path starts with direct (no root in path)", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"transitive1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + impactPaths: [][]formats.ComponentRow{{ + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + }}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + }, + }, + { + name: "Deep transitive - returns first direct in path", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root1", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 1", Version: "1.0.0", Properties: &[]cyclonedx.Property{{Name: "jfrog:dependency:type", Value: "root"}}}, + {BOMRef: "root2", Type: cyclonedx.ComponentTypeLibrary, Name: "Root 2", Version: "1.0.0", Properties: &[]cyclonedx.Property{{Name: "jfrog:dependency:type", Value: "root"}}}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + {BOMRef: "trans1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + {BOMRef: "trans2", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 2", Version: "4.0.0"}, + {BOMRef: "deepTrans", Type: cyclonedx.ComponentTypeLibrary, Name: "Deep Transitive", Version: "5.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root1", Dependencies: &[]string{"root2"}}, + {Ref: "root2", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"trans1"}}, + {Ref: "trans1", Dependencies: &[]string{"trans2"}}, + {Ref: "trans2", Dependencies: &[]string{"deepTrans"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "deepTrans", Name: "Deep Transitive", Version: "5.0.0"}, + impactPaths: [][]formats.ComponentRow{{ + {Id: "root1", Name: "Root 1", Version: "1.0.0"}, + {Id: "root2", Name: "Root 2", Version: "1.0.0"}, + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + {Id: "trans1", Name: "Transitive 1", Version: "3.0.0"}, + {Id: "trans2", Name: "Transitive 2", Version: "4.0.0"}, + {Id: "deepTrans", Name: "Deep Transitive", Version: "5.0.0"}, + }}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, + }, + }, + { + name: "Component is transitive with multiple impact paths - returns first direct from each path", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "directA", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct A", Version: "2.0.0"}, + {BOMRef: "directB", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct B", Version: "2.1.0"}, + {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"directA", "directB"}}, + {Ref: "directA", Dependencies: &[]string{"transitive1"}}, + {Ref: "directB", Dependencies: &[]string{"transitive1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + impactPaths: [][]formats.ComponentRow{ + { + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + {Id: "directA", Name: "Direct A", Version: "2.0.0"}, + {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + }, + { + {Id: "root", Name: "Root Component", Version: "1.0.0"}, + {Id: "directB", Name: "Direct B", Version: "2.1.0"}, + {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + }, + }, + expectedDirects: []formats.ComponentRow{ + {Id: "directA", Name: "Direct A", Version: "2.0.0"}, + {Id: "directB", Name: "Direct B", Version: "2.1.0"}, + }, + }, + { + name: "Component with evidence location - location preserved in result", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + { + BOMRef: "direct1", + Type: cyclonedx.ComponentTypeLibrary, + Name: "Direct 1", + Version: "2.0.0", + Evidence: &cyclonedx.Evidence{ + Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}}, + }, + }, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + }, + }, + component: cyclonedx.Component{ + BOMRef: "direct1", + Name: "Direct 1", + Version: "2.0.0", + Evidence: &cyclonedx.Evidence{ + Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}}, + }, + }, + impactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "Root Component", Version: "1.0.0"}, {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}}}, + expectedDirects: []formats.ComponentRow{ + {Id: "direct1", Name: "Direct 1", Version: "2.0.0", Location: &formats.Location{File: "package.json"}}, + }, + }, + { + name: "Component not in impact paths - return empty", + bom: &cyclonedx.BOM{ + Components: &[]cyclonedx.Component{ + {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, + {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, + {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + }, + Dependencies: &[]cyclonedx.Dependency{ + {Ref: "root", Dependencies: &[]string{"direct1"}}, + {Ref: "direct1", Dependencies: &[]string{"transitive1"}}, + }, + }, + component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, + impactPaths: [][]formats.ComponentRow{}, + expectedDirects: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actualDirects := ExtractComponentDirectComponentsInBOM(test.bom, test.component, test.impactPaths) + assert.ElementsMatch(t, test.expectedDirects, actualDirects) + }) + } +} + func TestGetFinalApplicabilityStatus(t *testing.T) { testCases := []struct { name string From 961c5a0b0b4ea5cb4205ad5ea5fee09ef5a38737 Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 19 Jan 2026 16:06:33 +0200 Subject: [PATCH 4/5] fix tests --- utils/formats/cdxutils/cyclonedxutils_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/formats/cdxutils/cyclonedxutils_test.go b/utils/formats/cdxutils/cyclonedxutils_test.go index 8b484753d..2cb09c585 100644 --- a/utils/formats/cdxutils/cyclonedxutils_test.go +++ b/utils/formats/cdxutils/cyclonedxutils_test.go @@ -826,8 +826,8 @@ func TestGetRootDependenciesEntries(t *testing.T) { }, skipRoot: true, expected: []cyclonedx.Dependency{ - {Ref: "root1"}, - {Ref: "root2"}, + {Ref: "root1", Dependencies: &[]string{"root2"}}, + {Ref: "root2", Dependencies: &[]string{"direct1"}}, }, }, } From 2cfc69bfe6197a51a71c926fd4bb570ce804d766 Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 20 Jan 2026 13:31:48 +0200 Subject: [PATCH 5/5] CR changes --- cli/docs/flags.go | 2 +- utils/formats/cdxutils/cyclonedxutils_test.go | 70 +++++++++++++++++++ utils/results/common.go | 12 ---- utils/results/common_test.go | 31 ++------ 4 files changed, 77 insertions(+), 38 deletions(-) diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 49da9c4a5..b16c2f744 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -215,7 +215,7 @@ var commandFlags = map[string][]string{ StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, AddSastRules, }, CurationAudit: { - CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, UseIncludedBuilds, SolutionPath, DockerImageName,IncludeCachedPackages, + CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, UseIncludedBuilds, SolutionPath, DockerImageName, IncludeCachedPackages, }, GitCountContributors: { InputFile, ScmType, ScmApiUrl, Token, Owner, RepoName, Months, DetailedSummary, InsecureTls, diff --git a/utils/formats/cdxutils/cyclonedxutils_test.go b/utils/formats/cdxutils/cyclonedxutils_test.go index 2cb09c585..7caa58c0c 100644 --- a/utils/formats/cdxutils/cyclonedxutils_test.go +++ b/utils/formats/cdxutils/cyclonedxutils_test.go @@ -65,6 +65,76 @@ func TestSearchDependencyEntry(t *testing.T) { } } +func TestGetJfrogRelationProperty(t *testing.T) { + tests := []struct { + name string + component *cyclonedx.Component + expected ComponentRelation + }{ + { + name: "Component with nil properties", + component: &cyclonedx.Component{BOMRef: "comp1", Properties: nil}, + expected: UnknownRelation, + }, + { + name: "Component with empty properties", + component: &cyclonedx.Component{BOMRef: "comp1", Properties: &[]cyclonedx.Property{}}, + expected: UnknownRelation, + }, + { + name: "Component without jfrog:dependency:type property", + component: &cyclonedx.Component{ + BOMRef: "comp1", + Properties: &[]cyclonedx.Property{{Name: "other:property", Value: "value"}}, + }, + expected: UnknownRelation, + }, + { + name: "Component with empty jfrog:dependency:type value", + component: &cyclonedx.Component{ + BOMRef: "comp1", + Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: ""}}, + }, + expected: UnknownRelation, + }, + { + name: "Component with root relation", + component: &cyclonedx.Component{ + BOMRef: "root", + Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(RootRelation)}}, + }, + expected: RootRelation, + }, + { + name: "Component with direct relation", + component: &cyclonedx.Component{ + BOMRef: "comp1", + Properties: &[]cyclonedx.Property{ + {Name: "some:other:property", Value: "value1"}, + {Name: JfrogRelationProperty, Value: string(DirectRelation)}, + {Name: "another:property", Value: "value2"}, + }, + }, + expected: DirectRelation, + }, + { + name: "Component with transitive relation", + component: &cyclonedx.Component{ + BOMRef: "trans1", + Properties: &[]cyclonedx.Property{{Name: JfrogRelationProperty, Value: string(TransitiveRelation)}}, + }, + expected: TransitiveRelation, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetJfrogRelationProperty(tt.component) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestGetComponentRelation(t *testing.T) { tests := []struct { name string diff --git a/utils/results/common.go b/utils/results/common.go index 130336150..3a0dac740 100644 --- a/utils/results/common.go +++ b/utils/results/common.go @@ -1403,18 +1403,6 @@ func ExtractComponentDirectComponentsInBOM(bom *cyclonedx.BOM, component cyclone return } -func GetParentsAsComponentRows(component cyclonedx.Component, components []cyclonedx.Component, dependencies []cyclonedx.Dependency) (directComponents []formats.ComponentRow) { - for _, parent := range cdxutils.SearchParents(component.BOMRef, components, dependencies...) { - directComponents = append(directComponents, formats.ComponentRow{ - Id: parent.BOMRef, - Name: parent.Name, - Version: parent.Version, - Location: CdxEvidenceToLocation(parent), - }) - } - return -} - func CdxEvidenceToLocation(component cyclonedx.Component) (location *formats.Location) { if component.Evidence == nil || component.Evidence.Occurrences == nil || len(*component.Evidence.Occurrences) == 0 { return nil diff --git a/utils/results/common_test.go b/utils/results/common_test.go index 691fdb38f..f15c745a4 100644 --- a/utils/results/common_test.go +++ b/utils/results/common_test.go @@ -702,29 +702,6 @@ func TestExtractComponentDirectComponentsInBOM(t *testing.T) { {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, }, }, - { - name: "Component is transitive - path starts with direct (no root in path)", - bom: &cyclonedx.BOM{ - Components: &[]cyclonedx.Component{ - {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, - {BOMRef: "direct1", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct 1", Version: "2.0.0"}, - {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, - }, - Dependencies: &[]cyclonedx.Dependency{ - {Ref: "root", Dependencies: &[]string{"direct1"}}, - {Ref: "direct1", Dependencies: &[]string{"transitive1"}}, - }, - }, - component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, - impactPaths: [][]formats.ComponentRow{{ - {Id: "root", Name: "Root Component", Version: "1.0.0"}, - {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, - {Id: "transitive1", Name: "Transitive 1", Version: "3.0.0"}, - }}, - expectedDirects: []formats.ComponentRow{ - {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}, - }, - }, { name: "Deep transitive - returns first direct in path", bom: &cyclonedx.BOM{ @@ -764,12 +741,16 @@ func TestExtractComponentDirectComponentsInBOM(t *testing.T) { {BOMRef: "root", Type: cyclonedx.ComponentTypeLibrary, Name: "Root Component", Version: "1.0.0"}, {BOMRef: "directA", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct A", Version: "2.0.0"}, {BOMRef: "directB", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct B", Version: "2.1.0"}, + {BOMRef: "directC", Type: cyclonedx.ComponentTypeLibrary, Name: "Direct C", Version: "2.2.0"}, {BOMRef: "transitive1", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 1", Version: "3.0.0"}, + {BOMRef: "transitive2", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 2", Version: "3.1.0"}, + {BOMRef: "transitive3", Type: cyclonedx.ComponentTypeLibrary, Name: "Transitive 3", Version: "3.2.0"}, }, Dependencies: &[]cyclonedx.Dependency{ - {Ref: "root", Dependencies: &[]string{"directA", "directB"}}, - {Ref: "directA", Dependencies: &[]string{"transitive1"}}, + {Ref: "root", Dependencies: &[]string{"directA", "directB", "directC"}}, + {Ref: "directA", Dependencies: &[]string{"transitive1", "transitive3"}}, {Ref: "directB", Dependencies: &[]string{"transitive1"}}, + {Ref: "directC", Dependencies: &[]string{"transitive2"}}, }, }, component: cyclonedx.Component{BOMRef: "transitive1", Name: "Transitive 1", Version: "3.0.0"},