Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion policy/enforcer/policyenforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
6 changes: 3 additions & 3 deletions policy/local/localconvertor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}},
}
}
Expand Down
36 changes: 34 additions & 2 deletions utils/formats/cdxutils/cyclonedxutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -178,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]()
Expand Down
91 changes: 91 additions & 0 deletions utils/formats/cdxutils/cyclonedxutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -809,6 +879,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", Dependencies: &[]string{"root2"}},
{Ref: "root2", Dependencies: &[]string{"direct1"}},
},
},
}

for _, tt := range tests {
Expand Down
1 change: 1 addition & 0 deletions utils/formats/simplejsonapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
43 changes: 33 additions & 10 deletions utils/results/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,20 @@ 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
var compImpactPathRows []formats.ComponentRow
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),
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -1369,13 +1379,26 @@ func CdxToFixedVersions(affectedVersions *[]cyclonedx.AffectedVersions) (fixedVe
return
}

func GetDirectDependenciesAsComponentRows(component cyclonedx.Component, components []cyclonedx.Component, dependencies []cyclonedx.Dependency) (directComponents []formats.ComponentRow) {
for _, parent := range cdxutils.SearchParents(component.BOMRef, components, dependencies...) {
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{
Name: parent.Name,
Version: parent.Version,
Location: CdxEvidenceToLocation(parent),
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.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
}
Expand Down
Loading
Loading