Skip to content

Commit 5fe56e3

Browse files
authored
Fix locations (evidences) for all components (jfrog#659)
1 parent 2a440fb commit 5fe56e3

File tree

12 files changed

+247
-87
lines changed

12 files changed

+247
-87
lines changed

sca/bom/buildinfo/technologies/pnpm/pnpm_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestBuildDependencyTreeLimitedDepth(t *testing.T) {
4343
name: "With transitive dependencies",
4444
treeDepth: "1",
4545
expectedUniqueDeps: []string{
46-
"npm://axios:1.13.4",
46+
"npm://axios:1.13.5",
4747
"npm://balaganjs:1.0.0",
4848
"npm://yargs:13.3.0",
4949
"npm://zen-website:1.0.0",
@@ -53,7 +53,7 @@ func TestBuildDependencyTreeLimitedDepth(t *testing.T) {
5353
Nodes: []*xrayUtils.GraphNode{
5454
{
5555
Id: "npm://balaganjs:1.0.0",
56-
Nodes: []*xrayUtils.GraphNode{{Id: "npm://axios:1.13.4"}, {Id: "npm://yargs:13.3.0"}},
56+
Nodes: []*xrayUtils.GraphNode{{Id: "npm://axios:1.13.5"}, {Id: "npm://yargs:13.3.0"}},
5757
},
5858
},
5959
},

tests/utils/test_utils.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,26 +189,35 @@ func convertScaSimpleJsonPathsForOS(potentialComponents *[]formats.ComponentRow,
189189
if potentialComponents != nil {
190190
components := *potentialComponents
191191
for i := range components {
192-
if components[i].Location != nil {
193-
components[i].Location.File = filepath.FromSlash(components[i].Location.File)
192+
if components[i].PreferredLocation != nil {
193+
components[i].PreferredLocation.File = filepath.FromSlash(components[i].PreferredLocation.File)
194+
}
195+
for j := range components[i].Evidences {
196+
components[i].Evidences[j].File = filepath.FromSlash(components[i].Evidences[j].File)
194197
}
195198
}
196199
}
197200
if potentialImpactPaths != nil {
198201
impactPaths := *potentialImpactPaths
199202
for i := range impactPaths {
200203
for j := range impactPaths[i] {
201-
if impactPaths[i][j].Location != nil {
202-
impactPaths[i][j].Location.File = filepath.FromSlash(impactPaths[i][j].Location.File)
204+
if impactPaths[i][j].PreferredLocation != nil {
205+
impactPaths[i][j].PreferredLocation.File = filepath.FromSlash(impactPaths[i][j].PreferredLocation.File)
206+
}
207+
for k := range impactPaths[i][j].Evidences {
208+
impactPaths[i][j].Evidences[k].File = filepath.FromSlash(impactPaths[i][j].Evidences[k].File)
203209
}
204210
}
205211
}
206212
}
207213
if potentialImpactedDependencyDetails != nil {
208214
impactedDependencyDetails := *potentialImpactedDependencyDetails
209215
for i := range impactedDependencyDetails.Components {
210-
if impactedDependencyDetails.Components[i].Location != nil {
211-
impactedDependencyDetails.Components[i].Location.File = filepath.FromSlash(impactedDependencyDetails.Components[i].Location.File)
216+
if impactedDependencyDetails.Components[i].PreferredLocation != nil {
217+
impactedDependencyDetails.Components[i].PreferredLocation.File = filepath.FromSlash(impactedDependencyDetails.Components[i].PreferredLocation.File)
218+
}
219+
for j := range impactedDependencyDetails.Components[i].Evidences {
220+
impactedDependencyDetails.Components[i].Evidences[j].File = filepath.FromSlash(impactedDependencyDetails.Components[i].Evidences[j].File)
212221
}
213222
}
214223
}

tests/validations/test_validate_simple_json.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,14 @@ func validateComponentRows(t *testing.T, issueId string, exactMatch bool, expect
226226

227227
func validateComponentRow(t *testing.T, issueId string, exactMatch bool, expected, actual formats.ComponentRow) {
228228
ValidateContent(t, exactMatch,
229-
PointerValidation[formats.Location]{Expected: expected.Location, Actual: actual.Location, Msg: fmt.Sprintf("IssueId %s: Component %s:%s Location mismatch", issueId, expected.Name, expected.Version)},
229+
PointerValidation[formats.Location]{Expected: expected.PreferredLocation, Actual: actual.PreferredLocation, Msg: fmt.Sprintf("IssueId %s: Component %s:%s Location mismatch", issueId, expected.Name, expected.Version)},
230230
)
231-
if expected.Location != nil {
232-
ValidateContent(t, exactMatch, StringValidation{Expected: expected.Location.File, Actual: actual.Location.File, Msg: fmt.Sprintf("IssueId %s: Component %s:%s Location.File mismatch", issueId, expected.Name, expected.Version)})
231+
if expected.PreferredLocation != nil {
232+
ValidateContent(t, exactMatch, StringValidation{Expected: expected.PreferredLocation.File, Actual: actual.PreferredLocation.File, Msg: fmt.Sprintf("IssueId %s: Component %s:%s Location.File mismatch", issueId, expected.Name, expected.Version)})
233233
}
234+
ValidateContent(t, exactMatch,
235+
ListValidation[formats.Location]{Expected: expected.Evidences, Actual: actual.Evidences, Msg: fmt.Sprintf("IssueId %s: Component %s:%s Evidences mismatch", issueId, expected.Name, expected.Version)},
236+
)
234237
}
235238

236239
func getComponent(name, version string, content []formats.ComponentRow) *formats.ComponentRow {

utils/formats/simplejsonapi.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,13 @@ func (l Location) ToString() string {
138138
}
139139

140140
type ComponentRow struct {
141-
Id string `json:"id,omitempty"`
142-
Name string `json:"name"`
143-
Version string `json:"version"`
144-
Location *Location `json:"location,omitempty"`
141+
Id string `json:"id,omitempty"`
142+
Name string `json:"name"`
143+
Version string `json:"version"`
144+
// Shown the preferred location of the component from the evidences.
145+
PreferredLocation *Location `json:"location,omitempty"`
146+
// Evidences is a list of locations that the component was found in.
147+
Evidences []Location `json:"evidences,omitempty"`
145148
}
146149

147150
type CveRow struct {

utils/results/common.go

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,10 @@ func getDirectComponentsAndImpactPaths(target string, impactPaths [][]services.I
258258
if _, exist := componentsMap[componentId]; !exist {
259259
compName, compVersion, _ := techutils.SplitComponentIdRaw(componentId)
260260
componentsMap[componentId] = formats.ComponentRow{
261-
Id: componentId,
262-
Name: compName,
263-
Version: compVersion,
264-
Location: getComponentLocation(impactPath[impactPathIndex].FullPath, target),
261+
Id: componentId,
262+
Name: compName,
263+
Version: compVersion,
264+
PreferredLocation: getComponentLocation(impactPath[impactPathIndex].FullPath, target),
265265
}
266266
}
267267

@@ -270,10 +270,10 @@ func getDirectComponentsAndImpactPaths(target string, impactPaths [][]services.I
270270
for _, pathNode := range impactPath {
271271
nodeCompName, nodeCompVersion, _ := techutils.SplitComponentIdRaw(pathNode.ComponentId)
272272
compImpactPathRows = append(compImpactPathRows, formats.ComponentRow{
273-
Id: pathNode.ComponentId,
274-
Name: nodeCompName,
275-
Version: nodeCompVersion,
276-
Location: getComponentLocation(pathNode.FullPath),
273+
Id: pathNode.ComponentId,
274+
Name: nodeCompName,
275+
Version: nodeCompVersion,
276+
PreferredLocation: getComponentLocation(pathNode.FullPath),
277277
})
278278
}
279279
impactPathsRows = append(impactPathsRows, compImpactPathRows)
@@ -292,10 +292,10 @@ func BuildImpactPath(affectedComponent cyclonedx.Component, components []cyclone
292292
impactedPath := buildImpactPathForComponent(parent, componentAppearances, components, dependencies...)
293293
// Add the affected component at the end of the impact path
294294
impactedPath = append(impactedPath, formats.ComponentRow{
295-
Id: affectedComponent.BOMRef,
296-
Name: affectedComponent.Name,
297-
Version: affectedComponent.Version,
298-
Location: CdxEvidenceToLocation(affectedComponent),
295+
Id: affectedComponent.BOMRef,
296+
Name: affectedComponent.Name,
297+
Version: affectedComponent.Version,
298+
Evidences: CdxEvidencesToLocations(affectedComponent),
299299
})
300300
// Add the impact path to the list of impact paths
301301
impactPathsRows = append(impactPathsRows, impactedPath)
@@ -308,10 +308,10 @@ func buildImpactPathForComponent(component cyclonedx.Component, componentAppeara
308308
// Build the impact path for the component
309309
impactPath = []formats.ComponentRow{
310310
{
311-
Id: component.BOMRef,
312-
Name: component.Name,
313-
Version: component.Version,
314-
Location: CdxEvidenceToLocation(component),
311+
Id: component.BOMRef,
312+
Name: component.Name,
313+
Version: component.Version,
314+
Evidences: CdxEvidencesToLocations(component),
315315
},
316316
}
317317
// Add the parent components to the impact path
@@ -1383,10 +1383,10 @@ func ExtractComponentDirectComponentsInBOM(bom *cyclonedx.BOM, component cyclone
13831383
if relation := cdxutils.GetComponentRelation(bom, component.BOMRef, true); relation == cdxutils.RootRelation || relation == cdxutils.DirectRelation {
13841384
// The component is a root or direct dependency, no parents to extract, return the component itself
13851385
directComponents = append(directComponents, formats.ComponentRow{
1386-
Id: component.BOMRef,
1387-
Name: component.Name,
1388-
Version: component.Version,
1389-
Location: CdxEvidenceToLocation(component),
1386+
Id: component.BOMRef,
1387+
Name: component.Name,
1388+
Version: component.Version,
1389+
Evidences: CdxEvidencesToLocations(component),
13901390
})
13911391
return
13921392
}
@@ -1403,19 +1403,50 @@ func ExtractComponentDirectComponentsInBOM(bom *cyclonedx.BOM, component cyclone
14031403
return
14041404
}
14051405

1406-
func CdxEvidenceToLocation(component cyclonedx.Component) (location *formats.Location) {
1406+
func CdxEvidencesToPreferredLocation(component cyclonedx.Component) (location *formats.Location) {
14071407
if component.Evidence == nil || component.Evidence.Occurrences == nil || len(*component.Evidence.Occurrences) == 0 {
14081408
return nil
14091409
}
1410+
if len(*component.Evidence.Occurrences) == 1 {
1411+
return &formats.Location{
1412+
File: (*component.Evidence.Occurrences)[0].Location,
1413+
}
1414+
}
1415+
// We need to pick the preferred location from the evidences (we prefer descriptors over lock files)
1416+
for _, occurrence := range *component.Evidence.Occurrences {
1417+
if techutils.IsTechnologyDescriptor(occurrence.Location) != techutils.NoTech {
1418+
return &formats.Location{
1419+
File: occurrence.Location,
1420+
}
1421+
}
1422+
}
14101423
// We take the first location as the main location
1411-
if len(*component.Evidence.Occurrences) > 1 {
1412-
log.Debug(fmt.Sprintf("Multiple locations found for component %s evidence, using the first one as location", component.Name))
1424+
log.Debug(fmt.Sprintf("Multiple locations found for component %s evidence, using the first one as location", component.Name))
1425+
return &formats.Location{
1426+
File: (*component.Evidence.Occurrences)[0].Location,
1427+
}
1428+
}
1429+
1430+
func CdxEvidencesToLocations(component cyclonedx.Component) (evidences []formats.Location) {
1431+
if component.Evidence == nil || component.Evidence.Occurrences == nil || len(*component.Evidence.Occurrences) == 0 {
1432+
return nil
1433+
}
1434+
for _, occurrence := range *component.Evidence.Occurrences {
1435+
evidences = append(evidences, formats.Location{File: occurrence.Location})
1436+
}
1437+
return
1438+
}
1439+
1440+
func GetBestLocation(component formats.ComponentRow) string {
1441+
if component.PreferredLocation != nil {
1442+
return strings.TrimSpace(component.PreferredLocation.File)
14131443
}
1414-
loc := (*component.Evidence.Occurrences)[0]
1415-
location = &formats.Location{
1416-
File: loc.Location,
1444+
for _, evidence := range component.Evidences {
1445+
if strings.TrimSpace(evidence.File) != "" {
1446+
return strings.TrimSpace(evidence.File)
1447+
}
14171448
}
1418-
return location
1449+
return ""
14191450
}
14201451

14211452
func CdxVulnToCveRows(vulnerability cyclonedx.Vulnerability, applicability *formats.Applicability) (cveRows []formats.CveRow) {

utils/results/common_test.go

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -613,16 +613,16 @@ func TestGetDirectComponents(t *testing.T) {
613613
name: "one direct component with target",
614614
target: filepath.Join("root", "dir", "file"),
615615
impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack2:1.2.3"}}},
616-
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")}}},
616+
expectedDirectComponentRows: []formats.ComponentRow{{Id: "gav://jfrog:pack2:1.2.3", Name: "jfrog:pack2", Version: "1.2.3", PreferredLocation: &formats.Location{File: filepath.Join("root", "dir", "file")}}},
617617
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"}}},
618618
},
619619
{
620620
name: "multiple direct components",
621621
target: filepath.Join("root", "dir", "file"),
622622
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"}}},
623623
expectedDirectComponentRows: []formats.ComponentRow{
624-
{Id: "gav://jfrog:pack21:1.2.3", Name: "jfrog:pack21", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}},
625-
{Id: "gav://jfrog:pack22:1.2.3", Name: "jfrog:pack22", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}},
624+
{Id: "gav://jfrog:pack21:1.2.3", Name: "jfrog:pack21", Version: "1.2.3", PreferredLocation: &formats.Location{File: filepath.Join("root", "dir", "file")}},
625+
{Id: "gav://jfrog:pack22:1.2.3", Name: "jfrog:pack22", Version: "1.2.3", PreferredLocation: &formats.Location{File: filepath.Join("root", "dir", "file")}},
626626
},
627627
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"}}},
628628
},
@@ -800,7 +800,7 @@ func TestExtractComponentDirectComponentsInBOM(t *testing.T) {
800800
},
801801
impactPaths: [][]formats.ComponentRow{{{Id: "root", Name: "Root Component", Version: "1.0.0"}, {Id: "direct1", Name: "Direct 1", Version: "2.0.0"}}},
802802
expectedDirects: []formats.ComponentRow{
803-
{Id: "direct1", Name: "Direct 1", Version: "2.0.0", Location: &formats.Location{File: "package.json"}},
803+
{Id: "direct1", Name: "Direct 1", Version: "2.0.0", Evidences: []formats.Location{{File: "package.json"}}},
804804
},
805805
},
806806
{
@@ -2869,3 +2869,109 @@ func TestExtractCdxDependenciesCves(t *testing.T) {
28692869
})
28702870
}
28712871
}
2872+
2873+
func TestGetBestLocation(t *testing.T) {
2874+
tests := []struct {
2875+
name string
2876+
component formats.ComponentRow
2877+
expected string
2878+
}{
2879+
{
2880+
name: "Component with no location",
2881+
},
2882+
{
2883+
name: "Component with preferred location",
2884+
component: formats.ComponentRow{PreferredLocation: &formats.Location{File: "package.json"}},
2885+
expected: "package.json",
2886+
},
2887+
{
2888+
name: "Component with evidences (npm)",
2889+
component: formats.ComponentRow{Evidences: []formats.Location{{File: "package.json"}, {File: "package-lock.json"}}},
2890+
expected: "package.json",
2891+
},
2892+
{
2893+
name: "Component with evidences (pip)",
2894+
component: formats.ComponentRow{Evidences: []formats.Location{{File: "requirements.txt"}, {File: "requirements.lock"}}},
2895+
expected: "requirements.txt",
2896+
},
2897+
{
2898+
name: "Component with preferred location and evidences",
2899+
component: formats.ComponentRow{PreferredLocation: &formats.Location{File: "package.json"}, Evidences: []formats.Location{{File: "package-lock.json"}}},
2900+
expected: "package.json",
2901+
},
2902+
}
2903+
2904+
for _, test := range tests {
2905+
t.Run(test.name, func(t *testing.T) {
2906+
assert.Equal(t, test.expected, GetBestLocation(test.component))
2907+
})
2908+
}
2909+
}
2910+
2911+
func TestCdxEvidencesToPreferredLocation(t *testing.T) {
2912+
tests := []struct {
2913+
name string
2914+
component cyclonedx.Component
2915+
expected *formats.Location
2916+
}{
2917+
{
2918+
name: "Component with no location",
2919+
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}}}},
2920+
expected: &formats.Location{File: "package.json"},
2921+
},
2922+
{
2923+
name: "Component with preferred location",
2924+
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}}}},
2925+
expected: &formats.Location{File: "package.json"},
2926+
},
2927+
{
2928+
name: "Component with evidences (npm)",
2929+
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}, {Location: "package-lock.json"}}}},
2930+
expected: &formats.Location{File: "package.json"},
2931+
},
2932+
{
2933+
name: "Component with evidences (pip)",
2934+
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "requirements.txt"}, {Location: "requirements.lock"}}}},
2935+
expected: &formats.Location{File: "requirements.txt"},
2936+
},
2937+
}
2938+
2939+
for _, test := range tests {
2940+
t.Run(test.name, func(t *testing.T) {
2941+
assert.Equal(t, test.expected, CdxEvidencesToPreferredLocation(test.component))
2942+
})
2943+
}
2944+
}
2945+
2946+
func TestCdxEvidencesToLocations(t *testing.T) {
2947+
tests := []struct {
2948+
name string
2949+
component cyclonedx.Component
2950+
expected []formats.Location
2951+
}{
2952+
{
2953+
name: "Component with no location",
2954+
},
2955+
{
2956+
name: "Component with preferred location",
2957+
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}}}},
2958+
expected: []formats.Location{{File: "package.json"}},
2959+
},
2960+
{
2961+
name: "Component with evidences (npm)",
2962+
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "package.json"}, {Location: "package-lock.json"}}}},
2963+
expected: []formats.Location{{File: "package.json"}, {File: "package-lock.json"}},
2964+
},
2965+
{
2966+
name: "Component with evidences (pip)",
2967+
component: cyclonedx.Component{Evidence: &cyclonedx.Evidence{Occurrences: &[]cyclonedx.EvidenceOccurrence{{Location: "requirements.txt"}, {Location: "requirements.lock"}}}},
2968+
expected: []formats.Location{{File: "requirements.txt"}, {File: "requirements.lock"}},
2969+
},
2970+
}
2971+
2972+
for _, test := range tests {
2973+
t.Run(test.name, func(t *testing.T) {
2974+
assert.Equal(t, test.expected, CdxEvidencesToLocations(test.component))
2975+
})
2976+
}
2977+
}

0 commit comments

Comments
 (0)