diff --git a/buildscripts/download-jars.sh b/buildscripts/download-jars.sh index 28c4d7d2..fce3b1ea 100755 --- a/buildscripts/download-jars.sh +++ b/buildscripts/download-jars.sh @@ -9,7 +9,7 @@ # Once you have updated the versions mentioned below, please execute this script from the root directory of the jfrog-cli-core to ensure the JAR files are updated. GRADLE_DEP_TREE_VERSION="3.0.2" # Changing this version also requires a change in mavenDepTreeVersion within utils/java/mvn.go. -MAVEN_DEP_TREE_VERSION="1.1.0" +MAVEN_DEP_TREE_VERSION="1.1.1" curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/gradle-dep-tree/${GRADLE_DEP_TREE_VERSION}/gradle-dep-tree-${GRADLE_DEP_TREE_VERSION}.jar -o commands/audit/sca/java/resources/gradle-dep-tree.jar curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/maven-dep-tree/${MAVEN_DEP_TREE_VERSION}/maven-dep-tree-${MAVEN_DEP_TREE_VERSION}.jar -o commands/audit/sca/java/resources/maven-dep-tree.jar diff --git a/commands/audit/sca/common.go b/commands/audit/sca/common.go index 566ac82c..0613e7d7 100644 --- a/commands/audit/sca/common.go +++ b/commands/audit/sca/common.go @@ -178,7 +178,8 @@ func SuspectCurationBlockedError(isCurationCmd bool, tech coreutils.Technology, } switch tech { case coreutils.Maven: - if strings.Contains(cmdOutput, "status code: 403") || strings.Contains(cmdOutput, "status code: 500") { + if strings.Contains(cmdOutput, "status code: 403") || strings.Contains(strings.ToLower(cmdOutput), "403 forbidden") || + strings.Contains(cmdOutput, "status code: 500") { msgToUser = fmt.Sprintf(curationErrorMsgToUserTemplate, coreutils.Maven) } case coreutils.Pip: diff --git a/commands/audit/sca/common_test.go b/commands/audit/sca/common_test.go index a76361b4..8cda89e8 100644 --- a/commands/audit/sca/common_test.go +++ b/commands/audit/sca/common_test.go @@ -9,7 +9,6 @@ import ( "golang.org/x/exp/maps" "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-client-go/xray/services" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" @@ -61,13 +60,13 @@ func TestGetExcludePattern(t *testing.T) { } func TestBuildXrayDependencyTree(t *testing.T) { - treeHelper := make(map[string]coreXray.DepTreeNode) - rootDep := coreXray.DepTreeNode{Children: []string{"topDep1", "topDep2", "topDep3"}} - topDep1 := coreXray.DepTreeNode{Children: []string{"midDep1", "midDep2"}} - topDep2 := coreXray.DepTreeNode{Children: []string{"midDep2", "midDep3"}} - midDep1 := coreXray.DepTreeNode{Children: []string{"bottomDep1"}} - midDep2 := coreXray.DepTreeNode{Children: []string{"bottomDep2", "bottomDep3"}} - bottomDep3 := coreXray.DepTreeNode{Children: []string{"leafDep"}} + treeHelper := make(map[string]utils.DepTreeNode) + rootDep := utils.DepTreeNode{Children: []string{"topDep1", "topDep2", "topDep3"}} + topDep1 := utils.DepTreeNode{Children: []string{"midDep1", "midDep2"}} + topDep2 := utils.DepTreeNode{Children: []string{"midDep2", "midDep3"}} + midDep1 := utils.DepTreeNode{Children: []string{"bottomDep1"}} + midDep2 := utils.DepTreeNode{Children: []string{"bottomDep2", "bottomDep3"}} + bottomDep3 := utils.DepTreeNode{Children: []string{"leafDep"}} treeHelper["rootDep"] = rootDep treeHelper["topDep1"] = topDep1 treeHelper["topDep2"] = topDep2 @@ -116,7 +115,7 @@ func TestBuildXrayDependencyTree(t *testing.T) { topDep2Node.Parent = rootNode topDep3Node.Parent = rootNode - tree, uniqueDeps := coreXray.BuildXrayDependencyTree(treeHelper, "rootDep") + tree, uniqueDeps := utils.BuildXrayDependencyTree(treeHelper, "rootDep") assert.ElementsMatch(t, expectedUniqueDeps, maps.Keys(uniqueDeps)) assert.True(t, tests.CompareTree(tree, rootNode)) diff --git a/commands/audit/sca/java/deptreemanager.go b/commands/audit/sca/java/deptreemanager.go index e323fad1..fe577b37 100644 --- a/commands/audit/sca/java/deptreemanager.go +++ b/commands/audit/sca/java/deptreemanager.go @@ -2,12 +2,12 @@ package java import ( "encoding/json" + "github.com/jfrog/jfrog-cli-security/utils" "os" "strings" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/utils/xray" "github.com/jfrog/jfrog-client-go/utils/errorutils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" ) @@ -16,7 +16,7 @@ const ( GavPackageTypeIdentifier = "gav://" ) -func BuildDependencyTree(depTreeParams DepTreeParams, tech coreutils.Technology) ([]*xrayUtils.GraphNode, map[string][]string, error) { +func BuildDependencyTree(depTreeParams DepTreeParams, tech coreutils.Technology) ([]*xrayUtils.GraphNode, map[string]*utils.DepTreeNode, error) { if tech == coreutils.Maven { return buildMavenDependencyTree(&depTreeParams) } @@ -44,18 +44,18 @@ func NewDepTreeManager(params *DepTreeParams) DepTreeManager { // The structure of a dependency tree of a module in a Gradle/Maven project, as created by the gradle-dep-tree and maven-dep-tree plugins. type moduleDepTree struct { - Root string `json:"root"` - Nodes map[string]xray.DepTreeNode `json:"nodes"` + Root string `json:"root"` + Nodes map[string]utils.DepTreeNode `json:"nodes"` } // Reads the output files of the gradle-dep-tree and maven-dep-tree plugins and returns them as a slice of GraphNodes. // It takes the output of the plugin's run (which is a byte representation of a list of paths of the output files, separated by newlines) as input. -func getGraphFromDepTree(outputFilePaths string) (depsGraph []*xrayUtils.GraphNode, uniqueDepsMap map[string][]string, err error) { +func getGraphFromDepTree(outputFilePaths string) (depsGraph []*xrayUtils.GraphNode, uniqueDepsMap map[string]*utils.DepTreeNode, err error) { modules, err := parseDepTreeFiles(outputFilePaths) if err != nil { return } - uniqueDepsMap = map[string][]string{} + uniqueDepsMap = map[string]*utils.DepTreeNode{} for _, module := range modules { moduleTree, moduleUniqueDeps := GetModuleTreeAndDependencies(module) depsGraph = append(depsGraph, moduleTree) @@ -67,8 +67,8 @@ func getGraphFromDepTree(outputFilePaths string) (depsGraph []*xrayUtils.GraphNo } // Returns a dependency tree and a flat list of the module's dependencies for the given module -func GetModuleTreeAndDependencies(module *moduleDepTree) (*xrayUtils.GraphNode, map[string][]string) { - moduleTreeMap := make(map[string]xray.DepTreeNode) +func GetModuleTreeAndDependencies(module *moduleDepTree) (*xrayUtils.GraphNode, map[string]*utils.DepTreeNode) { + moduleTreeMap := make(map[string]utils.DepTreeNode) moduleDeps := module.Nodes for depName, dependency := range moduleDeps { dependencyId := GavPackageTypeIdentifier + depName @@ -77,12 +77,13 @@ func GetModuleTreeAndDependencies(module *moduleDepTree) (*xrayUtils.GraphNode, childId := GavPackageTypeIdentifier + childName childrenList = append(childrenList, childId) } - moduleTreeMap[dependencyId] = xray.DepTreeNode{ - Types: dependency.Types, - Children: childrenList, + moduleTreeMap[dependencyId] = utils.DepTreeNode{ + Classifier: dependency.Classifier, + Types: dependency.Types, + Children: childrenList, } } - return xray.BuildXrayDependencyTree(moduleTreeMap, GavPackageTypeIdentifier+module.Root) + return utils.BuildXrayDependencyTree(moduleTreeMap, GavPackageTypeIdentifier+module.Root) } func parseDepTreeFiles(jsonFilePaths string) ([]*moduleDepTree, error) { diff --git a/commands/audit/sca/java/gradle.go b/commands/audit/sca/java/gradle.go index 57ceff8b..dad131e0 100644 --- a/commands/audit/sca/java/gradle.go +++ b/commands/audit/sca/java/gradle.go @@ -4,6 +4,7 @@ import ( _ "embed" "errors" "fmt" + "github.com/jfrog/jfrog-cli-security/utils" "os" "os/exec" "path/filepath" @@ -56,7 +57,7 @@ type gradleDepTreeManager struct { DepTreeManager } -func buildGradleDependencyTree(params *DepTreeParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps map[string][]string, err error) { +func buildGradleDependencyTree(params *DepTreeParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps map[string]*utils.DepTreeNode, err error) { manager := &gradleDepTreeManager{DepTreeManager: NewDepTreeManager(params)} outputFileContent, err := manager.runGradleDepTree() if err != nil { diff --git a/commands/audit/sca/java/mvn.go b/commands/audit/sca/java/mvn.go index 0b616985..c7263a6d 100644 --- a/commands/audit/sca/java/mvn.go +++ b/commands/audit/sca/java/mvn.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/commands/audit/sca" + "github.com/jfrog/jfrog-cli-security/utils" "net/url" "os" "os/exec" @@ -24,7 +25,7 @@ const ( mavenDepTreeJarFile = "maven-dep-tree.jar" mavenDepTreeOutputFile = "mavendeptree.out" // Changing this version also requires a change in MAVEN_DEP_TREE_VERSION within buildscripts/download_jars.sh - mavenDepTreeVersion = "1.1.0" + mavenDepTreeVersion = "1.1.1" settingsXmlFile = "settings.xml" ) @@ -68,7 +69,7 @@ func NewMavenDepTreeManager(params *DepTreeParams, cmdName MavenDepTreeCmd) *Mav } } -func buildMavenDependencyTree(params *DepTreeParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps map[string][]string, err error) { +func buildMavenDependencyTree(params *DepTreeParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps map[string]*utils.DepTreeNode, err error) { manager := NewMavenDepTreeManager(params, Tree) outputFilePaths, clearMavenDepTreeRun, err := manager.RunMavenDepTree() if err != nil { diff --git a/commands/audit/sca/java/mvn_test.go b/commands/audit/sca/java/mvn_test.go index 3c614bc6..94a413a8 100644 --- a/commands/audit/sca/java/mvn_test.go +++ b/commands/audit/sca/java/mvn_test.go @@ -105,7 +105,7 @@ func TestMavenTreesMultiModule(t *testing.T) { expectedUniqueDeps := []string{ GavPackageTypeIdentifier + "javax.mail:mail:1.4", - GavPackageTypeIdentifier + "org.testng:testng:5.9", + GavPackageTypeIdentifier + "org.testng:testng:5.9-jdk15", GavPackageTypeIdentifier + "javax.servlet:servlet-api:2.5", GavPackageTypeIdentifier + "org.jfrog.test:multi:3.7-SNAPSHOT", GavPackageTypeIdentifier + "org.jfrog.test:multi3:3.7-SNAPSHOT", @@ -157,7 +157,7 @@ func TestMavenWrapperTrees(t *testing.T) { GavPackageTypeIdentifier + "org.springframework:spring-core:2.5.6", GavPackageTypeIdentifier + "org.jfrog.test:multi:3.7-SNAPSHOT", GavPackageTypeIdentifier + "org.jfrog.test:multi2:3.7-SNAPSHOT", - GavPackageTypeIdentifier + "org.testng:testng:5.9", + GavPackageTypeIdentifier + "org.testng:testng:5.9-jdk15", GavPackageTypeIdentifier + "hsqldb:hsqldb:1.8.0.10", GavPackageTypeIdentifier + "junit:junit:3.8.1", GavPackageTypeIdentifier + "javax.activation:activation:1.1", @@ -198,7 +198,8 @@ func TestMavenWrapperTreesTypes(t *testing.T) { // dependency of pom type depWithPomType := uniqueDeps["gav://org.webjars:lodash:4.17.21"] assert.NotEmpty(t, depWithPomType) - assert.Equal(t, depWithPomType[0], "pom") + types := *depWithPomType.Types + assert.Equal(t, types[0], "pom") existInTreePom := false for _, node := range tree[0].Nodes { if node.Id == "gav://org.webjars:lodash:4.17.21" { @@ -212,7 +213,8 @@ func TestMavenWrapperTreesTypes(t *testing.T) { // dependency of jar type depWithJarType := uniqueDeps["gav://junit:junit:4.11"] assert.NotEmpty(t, depWithJarType) - assert.Equal(t, depWithJarType[0], "jar") + types = *depWithJarType.Types + assert.Equal(t, types[0], "jar") existInTreeJar := false for _, node := range tree[0].Nodes { if node.Id == "gav://junit:junit:4.11" { @@ -221,6 +223,12 @@ func TestMavenWrapperTreesTypes(t *testing.T) { existInTreeJar = true } } + // dependency with classifier + depWithJarClassifier1 := uniqueDeps["gav://commons-io:commons-io:1.2-flavor1"] + assert.NotEmpty(t, depWithJarClassifier1) + depWithJarClassifier2 := uniqueDeps["gav://commons-io:commons-io:1.2-flavor2"] + assert.NotEmpty(t, depWithJarClassifier2) + assert.True(t, existInTreeJar) } diff --git a/commands/audit/sca/java/resources/maven-dep-tree.jar b/commands/audit/sca/java/resources/maven-dep-tree.jar index f8d7ff40..d1863b87 100644 Binary files a/commands/audit/sca/java/resources/maven-dep-tree.jar and b/commands/audit/sca/java/resources/maven-dep-tree.jar differ diff --git a/commands/audit/sca/npm/npm.go b/commands/audit/sca/npm/npm.go index 00478a7a..20bc531d 100644 --- a/commands/audit/sca/npm/npm.go +++ b/commands/audit/sca/npm/npm.go @@ -7,7 +7,6 @@ import ( buildinfo "github.com/jfrog/build-info-go/entities" "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/npm" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-client-go/utils/log" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" @@ -105,7 +104,7 @@ func addIgnoreScriptsFlag(npmArgs []string) []string { // Parse the dependencies into an Xray dependency tree format func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo *biutils.PackageInfo) (*xrayUtils.GraphNode, []string) { - treeMap := make(map[string]coreXray.DepTreeNode) + treeMap := make(map[string]utils.DepTreeNode) for _, dependency := range dependencies { dependencyId := utils.NpmPackageTypeIdentifier + dependency.Id for _, requestedByNode := range dependency.RequestedBy { @@ -119,7 +118,7 @@ func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo * treeMap[parent] = depTreeNode } } - graph, nodeMapTypes := coreXray.BuildXrayDependencyTree(treeMap, utils.NpmPackageTypeIdentifier+packageInfo.BuildInfoModuleId()) + graph, nodeMapTypes := utils.BuildXrayDependencyTree(treeMap, utils.NpmPackageTypeIdentifier+packageInfo.BuildInfoModuleId()) return graph, maps.Keys(nodeMapTypes) } diff --git a/commands/audit/sca/nuget/nuget.go b/commands/audit/sca/nuget/nuget.go index c3da7bc8..ffec7824 100644 --- a/commands/audit/sca/nuget/nuget.go +++ b/commands/audit/sca/nuget/nuget.go @@ -16,7 +16,6 @@ import ( "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/dotnet" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray" "github.com/jfrog/jfrog-cli-security/commands/audit/sca" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" @@ -219,7 +218,7 @@ func runDotnetRestore(wd string, params utils.AuditParams, toolType bidotnet.Too func parseNugetDependencyTree(buildInfo *entities.BuildInfo) (nodes []*xrayUtils.GraphNode, allUniqueDeps []string) { uniqueDepsSet := datastructures.MakeSet[string]() for _, module := range buildInfo.Modules { - treeMap := make(map[string]coreXray.DepTreeNode) + treeMap := make(map[string]utils.DepTreeNode) for _, dependency := range module.Dependencies { dependencyId := nugetPackageTypeIdentifier + dependency.Id parent := nugetPackageTypeIdentifier + dependency.RequestedBy[0][0] @@ -231,7 +230,7 @@ func parseNugetDependencyTree(buildInfo *entities.BuildInfo) (nodes []*xrayUtils } treeMap[parent] = depTreeNode } - dependencyTree, uniqueDeps := coreXray.BuildXrayDependencyTree(treeMap, nugetPackageTypeIdentifier+module.Id) + dependencyTree, uniqueDeps := utils.BuildXrayDependencyTree(treeMap, nugetPackageTypeIdentifier+module.Id) nodes = append(nodes, dependencyTree) for _, uniqueDep := range maps.Keys(uniqueDeps) { uniqueDepsSet.Add(uniqueDep) diff --git a/commands/audit/sca/pnpm/pnpm.go b/commands/audit/sca/pnpm/pnpm.go index 39dedce5..5ae20b2e 100644 --- a/commands/audit/sca/pnpm/pnpm.go +++ b/commands/audit/sca/pnpm/pnpm.go @@ -20,7 +20,6 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" biutils "github.com/jfrog/build-info-go/utils" - coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" ) @@ -143,7 +142,7 @@ func parsePnpmLSContent(projectInfo []pnpmLsProject) (dependencyTrees []*xrayUti uniqueDepsSet := datastructures.MakeSet[string]() for _, project := range projectInfo { // Parse the dependencies into Xray dependency tree format - dependencyTree, uniqueProjectDeps := coreXray.BuildXrayDependencyTree(createProjectDependenciesTree(project), getDependencyId(project.Name, project.Version)) + dependencyTree, uniqueProjectDeps := utils.BuildXrayDependencyTree(createProjectDependenciesTree(project), getDependencyId(project.Name, project.Version)) // Add results dependencyTrees = append(dependencyTrees, dependencyTree) uniqueDepsSet.AddElements(maps.Keys(uniqueProjectDeps)...) @@ -152,8 +151,8 @@ func parsePnpmLSContent(projectInfo []pnpmLsProject) (dependencyTrees []*xrayUti return } -func createProjectDependenciesTree(project pnpmLsProject) map[string]coreXray.DepTreeNode { - treeMap := make(map[string]coreXray.DepTreeNode) +func createProjectDependenciesTree(project pnpmLsProject) map[string]utils.DepTreeNode { + treeMap := make(map[string]utils.DepTreeNode) directDependencies := []string{} // Handle production-dependencies for depName, dependency := range project.Dependencies { @@ -168,7 +167,7 @@ func createProjectDependenciesTree(project pnpmLsProject) map[string]coreXray.De appendTransitiveDependencies(directDependency, dependency.Dependencies, treeMap) } if len(directDependencies) > 0 { - treeMap[getDependencyId(project.Name, project.Version)] = coreXray.DepTreeNode{Children: directDependencies} + treeMap[getDependencyId(project.Name, project.Version)] = utils.DepTreeNode{Children: directDependencies} } return treeMap } @@ -178,13 +177,13 @@ func getDependencyId(depName, version string) string { return utils.NpmPackageTypeIdentifier + depName + ":" + version } -func appendTransitiveDependencies(parent string, dependencies map[string]pnpmLsDependency, result map[string]coreXray.DepTreeNode) { +func appendTransitiveDependencies(parent string, dependencies map[string]pnpmLsDependency, result map[string]utils.DepTreeNode) { for depName, dependency := range dependencies { dependencyId := getDependencyId(depName, dependency.Version) if node, ok := result[parent]; ok { node.Children = appendUniqueChild(node.Children, dependencyId) } else { - result[parent] = coreXray.DepTreeNode{Children: []string{dependencyId}} + result[parent] = utils.DepTreeNode{Children: []string{dependencyId}} } appendTransitiveDependencies(dependencyId, dependency.Dependencies, result) } diff --git a/commands/audit/sca/yarn/yarn.go b/commands/audit/sca/yarn/yarn.go index c70e623c..6a1fe54d 100644 --- a/commands/audit/sca/yarn/yarn.go +++ b/commands/audit/sca/yarn/yarn.go @@ -13,7 +13,6 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" - coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" @@ -200,7 +199,7 @@ func runYarnInstallAccordingToVersion(curWd, yarnExecPath string, installCommand // Parse the dependencies into a Xray dependency tree format func parseYarnDependenciesMap(dependencies map[string]*biutils.YarnDependency, rootXrayId string) (*xrayUtils.GraphNode, []string) { - treeMap := make(map[string]coreXray.DepTreeNode) + treeMap := make(map[string]utils.DepTreeNode) for _, dependency := range dependencies { xrayDepId := getXrayDependencyId(dependency) var subDeps []string @@ -208,10 +207,10 @@ func parseYarnDependenciesMap(dependencies map[string]*biutils.YarnDependency, r subDeps = append(subDeps, getXrayDependencyId(dependencies[biutils.GetYarnDependencyKeyFromLocator(subDepPtr.Locator)])) } if len(subDeps) > 0 { - treeMap[xrayDepId] = coreXray.DepTreeNode{Children: subDeps} + treeMap[xrayDepId] = utils.DepTreeNode{Children: subDeps} } } - graph, uniqDeps := coreXray.BuildXrayDependencyTree(treeMap, rootXrayId) + graph, uniqDeps := utils.BuildXrayDependencyTree(treeMap, rootXrayId) return graph, maps.Keys(uniqDeps) } diff --git a/commands/audit/scarunner.go b/commands/audit/scarunner.go index 9b4d8870..cc145e6e 100644 --- a/commands/audit/scarunner.go +++ b/commands/audit/scarunner.go @@ -4,11 +4,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/jfrog/build-info-go/utils/pythonutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "os" "time" + "github.com/jfrog/build-info-go/utils/pythonutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/common/project" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -58,9 +59,9 @@ func runScaScan(params *AuditParams, results *xrayutils.Results) (err error) { }() for _, scan := range scans { // Run the scan - log.Info("Running SCA scan for", scan.Technology, "vulnerable dependencies in", scan.WorkingDirectory, "directory...") + log.Info("Running SCA scan for", scan.Technology, "vulnerable dependencies in", scan.Target, "directory...") if wdScanErr := executeScaScan(serverDetails, params, scan); wdScanErr != nil { - err = errors.Join(err, fmt.Errorf("audit command in '%s' failed:\n%s", scan.WorkingDirectory, wdScanErr.Error())) + err = errors.Join(err, fmt.Errorf("audit command in '%s' failed:\n%s", scan.Target, wdScanErr.Error())) continue } // Add the scan to the results @@ -91,11 +92,11 @@ func getScaScansToPreform(params *AuditParams) (scansToPreform []*xrayutils.ScaS } if len(workingDirs) == 0 { // Requested technology (from params) descriptors/indicators was not found, scan only requested directory for this technology. - scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{WorkingDirectory: requestedDirectory, Technology: tech}) + scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{Target: requestedDirectory, Technology: tech}) } for workingDir, descriptors := range workingDirs { // Add scan for each detected working directory. - scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{WorkingDirectory: workingDir, Technology: tech, Descriptors: descriptors}) + scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{Target: workingDir, Technology: tech, Descriptors: descriptors}) } } } @@ -114,7 +115,7 @@ func getRequestedDescriptors(params *AuditParams) map[coreutils.Technology][]str // This method will change the working directory to the scan's working directory. func executeScaScan(serverDetails *config.ServerDetails, params *AuditParams, scan *xrayutils.ScaScanResult) (err error) { // Get the dependency tree for the technology in the working directory. - if err = os.Chdir(scan.WorkingDirectory); err != nil { + if err = os.Chdir(scan.Target); err != nil { return errorutils.CheckError(err) } treeResult, techErr := GetTechDependencyTree(params.AuditBasicParams, scan.Technology) @@ -214,7 +215,7 @@ func GetTechDependencyTree(params xrayutils.AuditParams, tech coreutils.Technolo return } var uniqueDeps []string - var uniqDepsWithTypes map[string][]string + var uniqDepsWithTypes map[string]*xrayutils.DepTreeNode startTime := time.Now() switch tech { @@ -349,14 +350,18 @@ func SetResolutionRepoIfExists(params xrayutils.AuditParams, tech coreutils.Tech return } -func createFlatTreeWithTypes(uniqueDeps map[string][]string) (*xrayCmdUtils.GraphNode, error) { +func createFlatTreeWithTypes(uniqueDeps map[string]*xrayutils.DepTreeNode) (*xrayCmdUtils.GraphNode, error) { if err := logDeps(uniqueDeps); err != nil { return nil, err } var uniqueNodes []*xrayCmdUtils.GraphNode - for uniqueDep, types := range uniqueDeps { - p := types - uniqueNodes = append(uniqueNodes, &xrayCmdUtils.GraphNode{Id: uniqueDep, Types: &p}) + for uniqueDep, nodeAttr := range uniqueDeps { + node := &xrayCmdUtils.GraphNode{Id: uniqueDep} + if nodeAttr != nil { + node.Types = nodeAttr.Types + node.Classifier = nodeAttr.Classifier + } + uniqueNodes = append(uniqueNodes, node) } return &xrayCmdUtils.GraphNode{Id: "root", Nodes: uniqueNodes}, nil } diff --git a/commands/audit/scarunner_test.go b/commands/audit/scarunner_test.go index 68162094..897b0426 100644 --- a/commands/audit/scarunner_test.go +++ b/commands/audit/scarunner_test.go @@ -137,8 +137,8 @@ func TestGetScaScansToPreform(t *testing.T) { }, expected: []*xrayutils.ScaScanResult{ { - Technology: coreutils.Maven, - WorkingDirectory: filepath.Join(dir, "dir", "maven"), + Technology: coreutils.Maven, + Target: filepath.Join(dir, "dir", "maven"), Descriptors: []string{ filepath.Join(dir, "dir", "maven", "pom.xml"), filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), @@ -146,14 +146,14 @@ func TestGetScaScansToPreform(t *testing.T) { }, }, { - Technology: coreutils.Npm, - WorkingDirectory: filepath.Join(dir, "dir", "npm"), - Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, + Technology: coreutils.Npm, + Target: filepath.Join(dir, "dir", "npm"), + Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, }, { - Technology: coreutils.Go, - WorkingDirectory: filepath.Join(dir, "dir", "go"), - Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, + Technology: coreutils.Go, + Target: filepath.Join(dir, "dir", "go"), + Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, }, }, }, @@ -167,8 +167,8 @@ func TestGetScaScansToPreform(t *testing.T) { }, expected: []*xrayutils.ScaScanResult{ { - Technology: coreutils.Maven, - WorkingDirectory: filepath.Join(dir, "dir", "maven"), + Technology: coreutils.Maven, + Target: filepath.Join(dir, "dir", "maven"), Descriptors: []string{ filepath.Join(dir, "dir", "maven", "pom.xml"), filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), @@ -176,34 +176,34 @@ func TestGetScaScansToPreform(t *testing.T) { }, }, { - Technology: coreutils.Npm, - WorkingDirectory: filepath.Join(dir, "dir", "npm"), - Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, + Technology: coreutils.Npm, + Target: filepath.Join(dir, "dir", "npm"), + Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, }, { - Technology: coreutils.Go, - WorkingDirectory: filepath.Join(dir, "dir", "go"), - Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, + Technology: coreutils.Go, + Target: filepath.Join(dir, "dir", "go"), + Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, }, { - Technology: coreutils.Yarn, - WorkingDirectory: filepath.Join(dir, "yarn"), - Descriptors: []string{filepath.Join(dir, "yarn", "package.json")}, + Technology: coreutils.Yarn, + Target: filepath.Join(dir, "yarn"), + Descriptors: []string{filepath.Join(dir, "yarn", "package.json")}, }, { - Technology: coreutils.Pip, - WorkingDirectory: filepath.Join(dir, "yarn", "Pip"), - Descriptors: []string{filepath.Join(dir, "yarn", "Pip", "requirements.txt")}, + Technology: coreutils.Pip, + Target: filepath.Join(dir, "yarn", "Pip"), + Descriptors: []string{filepath.Join(dir, "yarn", "Pip", "requirements.txt")}, }, { - Technology: coreutils.Pipenv, - WorkingDirectory: filepath.Join(dir, "yarn", "Pipenv"), - Descriptors: []string{filepath.Join(dir, "yarn", "Pipenv", "Pipfile")}, + Technology: coreutils.Pipenv, + Target: filepath.Join(dir, "yarn", "Pipenv"), + Descriptors: []string{filepath.Join(dir, "yarn", "Pipenv", "Pipfile")}, }, { - Technology: coreutils.Nuget, - WorkingDirectory: filepath.Join(dir, "Nuget"), - Descriptors: []string{filepath.Join(dir, "Nuget", "project.sln"), filepath.Join(dir, "Nuget", "Nuget-sub", "project.csproj")}, + Technology: coreutils.Nuget, + Target: filepath.Join(dir, "Nuget"), + Descriptors: []string{filepath.Join(dir, "Nuget", "project.sln"), filepath.Join(dir, "Nuget", "Nuget-sub", "project.csproj")}, }, }, }, diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index 1b4e78d5..e31b20ea 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -619,7 +619,7 @@ func getUrlNameAndVersionByTech(tech coreutils.Technology, node *xrayUtils.Graph case coreutils.Npm: return getNpmNameScopeAndVersion(node.Id, artiUrl, repo, coreutils.Npm.String()) case coreutils.Maven: - return getMavenNameScopeAndVersion(node.Id, artiUrl, repo, node.Types) + return getMavenNameScopeAndVersion(node.Id, artiUrl, repo, node) case coreutils.Pip: downloadUrls, name, version = getPythonNameVersion(node.Id, downloadUrlsMap) @@ -648,18 +648,22 @@ func getPythonNameVersion(id string, downloadUrlsMap map[string]string) (downloa // input- id: gav://org.apache.tomcat.embed:tomcat-embed-jasper:8.0.33 // input - repo: libs-release -// output - downloadUrl: /libs-release/org/apache/tomcat/embed/tomcat-embed-jasper/8.0.33/tomcat-embed-jasper-8.0.33.jar -func getMavenNameScopeAndVersion(id, artiUrl, repo string, types *[]string) (downloadUrls []string, name, scope, version string) { +// output - downloadUrl: /libs-release/org/apache/tomcat/embed/tomcat-embed-jasper/8.0.33/tomcat-embed-jasper-8.0.33-jdk15.jar +func getMavenNameScopeAndVersion(id, artiUrl, repo string, node *xrayUtils.GraphNode) (downloadUrls []string, name, scope, version string) { id = strings.TrimPrefix(id, "gav://") allParts := strings.Split(id, ":") if len(allParts) < 3 { return } nameVersion := allParts[1] + "-" + allParts[2] + versionDir := allParts[2] + if node != nil && node.Classifier != nil && *node.Classifier != "" { + versionDir = strings.TrimSuffix(versionDir, "-"+*node.Classifier) + } packagePath := strings.Join(strings.Split(allParts[0], "."), "/") + "/" + - allParts[1] + "/" + allParts[2] + "/" + nameVersion - if types != nil { - for _, fileType := range *types { + allParts[1] + "/" + versionDir + "/" + nameVersion + if node.Types != nil { + for _, fileType := range *node.Types { // curation service supports maven only for jar and war file types. if fileType == "jar" || fileType == "war" { downloadUrls = append(downloadUrls, strings.TrimSuffix(artiUrl, "/")+"/"+repo+"/"+packagePath+"."+fileType) diff --git a/commands/scan/buildscan.go b/commands/scan/buildscan.go index bbd18d0b..2bcbd66d 100644 --- a/commands/scan/buildscan.go +++ b/commands/scan/buildscan.go @@ -2,6 +2,7 @@ package scan import ( "errors" + "fmt" "github.com/jfrog/jfrog-cli-core/v2/common/build" outputFormat "github.com/jfrog/jfrog-cli-core/v2/common/format" @@ -130,7 +131,7 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS scanResults := xrutils.NewAuditResults() scanResults.XrayVersion = xrayVersion - scanResults.ScaResults = []xrutils.ScaScanResult{{XrayResults: scanResponse}} + scanResults.ScaResults = []xrutils.ScaScanResult{{Target: fmt.Sprintf("%s (%s)", params.BuildName, params.BuildNumber), XrayResults: scanResponse}} resultsPrinter := xrutils.NewResultsWriter(scanResults). SetOutputFormat(bsc.outputFormat). @@ -143,7 +144,9 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS if bsc.outputFormat != outputFormat.Table { // Print the violations and/or vulnerabilities as part of one JSON. - err = resultsPrinter.PrintScanResults() + if err = resultsPrinter.PrintScanResults(); err != nil { + return + } } else { // Print two different tables for violations and vulnerabilities (if needed) @@ -160,6 +163,7 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS } } } + err = utils.RecordSecurityCommandOutput(utils.ScanCommandSummaryResult{Results: scanResults.GetSummary(), Section: utils.Build}) return } diff --git a/commands/scan/scan.go b/commands/scan/scan.go index c8035d84..c707fd22 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -34,6 +34,11 @@ import ( type FileContext func(string) parallel.TaskFunc type indexFileHandlerFunc func(file string) +type ScanInfo struct { + Target string + Result *services.ScanResponse +} + const ( BypassArchiveLimitsMinXrayVersion = "3.59.0" indexingCommand = "graph" @@ -213,7 +218,7 @@ func (scanCmd *ScanCommand) Run() (err error) { } // resultsArr is a two-dimensional array. Each array in it contains a list of ScanResponses that were requested and collected by a specific thread. - resultsArr := make([][]*services.ScanResponse, threads) + resultsArr := make([][]*ScanInfo, threads) fileProducerConsumer := parallel.NewRunner(scanCmd.threads, 20000, false) fileProducerErrors := make([][]formats.SimpleJsonError, threads) indexedFileProducerConsumer := parallel.NewRunner(scanCmd.threads, 20000, false) @@ -225,10 +230,10 @@ func (scanCmd *ScanCommand) Run() (err error) { scanCmd.performScanTasks(fileProducerConsumer, indexedFileProducerConsumer) // Handle results - flatResults := []services.ScanResponse{} + flatResults := []xrutils.ScaScanResult{} for _, arr := range resultsArr { for _, res := range arr { - flatResults = append(flatResults, *res) + flatResults = append(flatResults, xrutils.ScaScanResult{Target: res.Target, XrayResults: []services.ScanResponse{*res.Result}}) } } if scanCmd.progress != nil { @@ -248,7 +253,7 @@ func (scanCmd *ScanCommand) Run() (err error) { scanResults := xrutils.NewAuditResults() scanResults.XrayVersion = xrayVersion - scanResults.ScaResults = []xrutils.ScaScanResult{{XrayResults: flatResults}} + scanResults.ScaResults = flatResults if err = xrutils.NewResultsWriter(scanResults). SetOutputFormat(scanCmd.outputFormat). @@ -264,10 +269,15 @@ func (scanCmd *ScanCommand) Run() (err error) { if err != nil { return err } + + if err = utils.RecordSecurityCommandOutput(utils.ScanCommandSummaryResult{Results: scanResults.GetSummary(), Section: utils.Binary}); err != nil { + return err + } + // If includeVulnerabilities is false it means that context was provided, so we need to check for build violations. // If user provided --fail=false, don't fail the build. if scanCmd.fail && !scanCmd.includeVulnerabilities { - if xrutils.CheckIfFailBuild(flatResults) { + if xrutils.CheckIfFailBuild(scanResults.GetScaScansXrayResults()) { return xrutils.NewFailBuildError() } } @@ -286,7 +296,7 @@ func (scanCmd *ScanCommand) CommandName() string { return "xr_scan" } -func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, resultsArr [][]*services.ScanResponse, fileErrors, indexedFileErrors [][]formats.SimpleJsonError, fileCollectingErrorsQueue *clientutils.ErrorsQueue, xrayVersion string) { +func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, resultsArr [][]*ScanInfo, fileErrors, indexedFileErrors [][]formats.SimpleJsonError, fileCollectingErrorsQueue *clientutils.ErrorsQueue, xrayVersion string) { go func() { defer fileProducer.Done() // Iterate over file-spec groups and produce indexing tasks. @@ -305,7 +315,7 @@ func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer p }() } -func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, indexedFileProducer parallel.Runner, resultsArr [][]*services.ScanResponse, fileErrors, indexedFileErrors [][]formats.SimpleJsonError, xrayVersion string) FileContext { +func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, indexedFileProducer parallel.Runner, resultsArr [][]*ScanInfo, fileErrors, indexedFileErrors [][]formats.SimpleJsonError, xrayVersion string) FileContext { return func(filePath string) parallel.TaskFunc { return func(threadId int) (err error) { logMsgPrefix := clientutils.GetLogMsgPrefix(threadId, false) @@ -355,7 +365,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, indexedFil indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) return } - resultsArr[threadId] = append(resultsArr[threadId], scanResults) + resultsArr[threadId] = append(resultsArr[threadId], &ScanInfo{Target: filePath, Result: scanResults}) return } diff --git a/formats/summary.go b/formats/summary.go new file mode 100644 index 00000000..e43c8a5c --- /dev/null +++ b/formats/summary.go @@ -0,0 +1,111 @@ +package formats + +type SummaryResults struct { + Scans []ScanSummaryResult `json:"scans"` +} + +func (sr SummaryResults) GetTotalIssueCount() (total int) { + for _, scan := range sr.Scans { + total += scan.GetTotalIssueCount() + } + return +} + +type ScanSummaryResult struct { + Target string `json:"target,omitempty"` + ScaScanResults *ScaSummaryCount `json:"sca,omitempty"` + IacScanResults *SummaryCount `json:"iac,omitempty"` + SecretsScanResults *SummaryCount `json:"secrets,omitempty"` + SastScanResults *SummaryCount `json:"sast,omitempty"` +} + +type SummarySubScanType string + +const ( + ScaScan SummarySubScanType = "SCA" + IacScan SummarySubScanType = "IAC" + SecretsScan SummarySubScanType = "Secrets" + SastScan SummarySubScanType = "SAST" +) + +// Severity -> Count +type SummaryCount map[string]int + +func (sc SummaryCount) GetTotal() int { + total := 0 + for _, count := range sc { + total += count + } + return total +} + +// Severity -> Applicable status -> Count +type ScaSummaryCount map[string]SummaryCount + +func (sc ScaSummaryCount) GetTotal() (total int) { + for _, count := range sc { + total += count.GetTotal() + } + return +} + +func (sc ScaSummaryCount) GetSeverityCountsWithoutStatus() (severityCounts SummaryCount) { + severityCounts = SummaryCount{} + for severity, statusCounts := range sc { + for _, count := range statusCounts { + severityCounts[severity] += count + } + } + return +} + +func (s *ScanSummaryResult) HasIssues() bool { + return s.GetTotalIssueCount() > 0 +} + +func (s *ScanSummaryResult) GetTotalIssueCount() (total int) { + if s.ScaScanResults != nil { + total += s.ScaScanResults.GetTotal() + } + if s.IacScanResults != nil { + total += s.IacScanResults.GetTotal() + } + if s.SecretsScanResults != nil { + total += s.SecretsScanResults.GetTotal() + } + if s.SastScanResults != nil { + total += s.SastScanResults.GetTotal() + } + return +} + +func (s *ScanSummaryResult) GetSubScansWithIssues() []SummarySubScanType { + subScans := []SummarySubScanType{} + if s.SecretsScanResults != nil && s.SecretsScanResults.GetTotal() > 0 { + subScans = append(subScans, SecretsScan) + } + if s.SastScanResults != nil && s.SastScanResults.GetTotal() > 0 { + subScans = append(subScans, SastScan) + } + if s.IacScanResults != nil && s.IacScanResults.GetTotal() > 0 { + subScans = append(subScans, IacScan) + } + if s.ScaScanResults != nil && s.ScaScanResults.GetTotal() > 0 { + subScans = append(subScans, ScaScan) + } + return subScans +} + +func (s *ScanSummaryResult) GetSubScanTotalIssueCount(subScanType SummarySubScanType) (count int) { + switch subScanType { + case ScaScan: + count = s.ScaScanResults.GetTotal() + case IacScan: + count = s.IacScanResults.GetTotal() + case SecretsScan: + count = s.SecretsScanResults.GetTotal() + case SastScan: + count = s.SastScanResults.GetTotal() + } + return +} diff --git a/formats/summary_test.go b/formats/summary_test.go new file mode 100644 index 00000000..7a216662 --- /dev/null +++ b/formats/summary_test.go @@ -0,0 +1,104 @@ +package formats + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSummaryCount(t *testing.T) { + testCases := []struct { + name string + count SummaryCount + expected int + }{ + {"Empty", SummaryCount{}, 0}, + {"Single", SummaryCount{"High": 1}, 1}, + {"Multiple", SummaryCount{"High": 1, "Medium": 2, "Low": 3}, 6}, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.expected, testCase.count.GetTotal()) + }) + } +} + +func TestScaSummaryCount(t *testing.T) { + testCases := []struct { + name string + count ScaSummaryCount + expected int + expectedSeverityCountsWithoutStatus SummaryCount + }{ + {"Empty", ScaSummaryCount{}, 0, SummaryCount{}}, + {"Single-NoStatus", ScaSummaryCount{"High": SummaryCount{"": 1}}, 1, SummaryCount{"High": 1}}, + {"Single-Status", ScaSummaryCount{"High": SummaryCount{"Applicable": 1}}, 1, SummaryCount{"High": 1}}, + { + "Multiple-NoStatus", + ScaSummaryCount{"High": SummaryCount{"": 1}, "Medium": SummaryCount{"": 2}, "Low": SummaryCount{"": 3}}, + 6, + SummaryCount{"High": 1, "Medium": 2, "Low": 3}, + }, + { + "Multiple-Status", + ScaSummaryCount{"High": SummaryCount{"Applicable": 1}, "Medium": SummaryCount{"": 2}, "Low": SummaryCount{"Applicable": 3, "Not Applicable": 3}}, + 9, + SummaryCount{"High": 1, "Medium": 2, "Low": 6}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.expected, testCase.count.GetTotal()) + assert.Equal(t, testCase.expectedSeverityCountsWithoutStatus, testCase.count.GetSeverityCountsWithoutStatus()) + }) + } +} + +func TestScanSummaryResult(t *testing.T) { + testCases := []struct { + name string + result *ScanSummaryResult + expectedTotalIssueCount int + expectedSubScansWithIssues []SummarySubScanType + expectedSubScansIssuesCount map[SummarySubScanType]int + }{ + { + "Empty", + &ScanSummaryResult{}, + 0, + []SummarySubScanType{}, + map[SummarySubScanType]int{}, + }, + { + "Single", + &ScanSummaryResult{ + ScaScanResults: &ScaSummaryCount{"High": SummaryCount{"Applicable": 1}}, + }, + 1, + []SummarySubScanType{ScaScan}, + map[SummarySubScanType]int{ScaScan: 1}, + }, + { + "Multiple", + &ScanSummaryResult{ + ScaScanResults: &ScaSummaryCount{"High": SummaryCount{"Applicable": 1}}, + SastScanResults: &SummaryCount{"High": 1}, + }, + 2, + []SummarySubScanType{SastScan, ScaScan}, + map[SummarySubScanType]int{SastScan: 1, ScaScan: 1}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.expectedTotalIssueCount > 0, testCase.result.HasIssues()) + assert.Equal(t, testCase.expectedTotalIssueCount, testCase.result.GetTotalIssueCount()) + if assert.Equal(t, testCase.expectedSubScansWithIssues, testCase.result.GetSubScansWithIssues()) { + for subScan, expectedCount := range testCase.expectedSubScansIssuesCount { + assert.Equal(t, expectedCount, testCase.result.GetSubScanTotalIssueCount(subScan)) + } + } + }) + } + +} diff --git a/go.mod b/go.mod index b8af635c..cb06e0c9 100644 --- a/go.mod +++ b/go.mod @@ -4,17 +4,17 @@ go 1.21 require ( github.com/gookit/color v1.5.4 - github.com/jfrog/build-info-go v1.9.26 + github.com/jfrog/build-info-go v1.9.27 github.com/jfrog/gofrog v1.7.1 github.com/jfrog/jfrog-apps-config v1.0.1 - github.com/jfrog/jfrog-cli-core/v2 v2.51.0 - github.com/jfrog/jfrog-client-go v1.40.1 + github.com/jfrog/jfrog-cli-core/v2 v2.53.0 + github.com/jfrog/jfrog-client-go v1.40.2 github.com/magiconair/properties v1.8.7 github.com/owenrumney/go-sarif/v2 v2.3.0 github.com/stretchr/testify v1.9.0 - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 - golang.org/x/sync v0.6.0 - golang.org/x/text v0.14.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/sync v0.7.0 + golang.org/x/text v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -31,7 +31,7 @@ require ( github.com/c-bata/go-prompt v0.2.5 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect @@ -40,7 +40,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-git/go-git/v5 v5.11.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -48,7 +48,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jedib0t/go-pretty/v6 v6.5.6 // indirect + github.com/jedib0t/go-pretty/v6 v6.5.9 // indirect github.com/jfrog/archiver/v3 v3.6.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.4 // indirect @@ -72,8 +72,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sergi/go-diff v1.1.0 // indirect - github.com/skeema/knownhosts v1.2.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect @@ -81,19 +81,19 @@ require ( github.com/spf13/viper v1.18.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/ulikunitz/xz v0.5.11 // indirect - github.com/urfave/cli v1.22.14 // indirect + github.com/urfave/cli v1.22.15 // indirect github.com/vbauerster/mpb/v7 v7.5.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/mod v0.16.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/tools v0.19.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/tools v0.21.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 7edec7cd..30ff2df8 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -60,16 +60,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= -github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA= github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= @@ -92,20 +92,20 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.5.6 h1:nKXVLqPfAwY7sWcYXdNZZZ2fjqDpAtj9UeWupgfUxSg= -github.com/jedib0t/go-pretty/v6 v6.5.6/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg= +github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= +github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/jfrog/archiver/v3 v3.6.0 h1:OVZ50vudkIQmKMgA8mmFF9S0gA47lcag22N13iV3F1w= github.com/jfrog/archiver/v3 v3.6.0/go.mod h1:fCAof46C3rAXgZurS8kNRNdSVMKBbZs+bNNhPYxLldI= -github.com/jfrog/build-info-go v1.9.26 h1:1Ddc6+Ecvhc+UMnKhRVG1jGM6fYNwA49207azTBGBc8= -github.com/jfrog/build-info-go v1.9.26/go.mod h1:8T7/ajM9aGshvgpwCtXwIFpyF/R6CEn4W+/FLryNXWw= +github.com/jfrog/build-info-go v1.9.27 h1:7RWJcajqtNNbGHuYkgOLUIG7mmRKF0yxC7mvYAbdVlU= +github.com/jfrog/build-info-go v1.9.27/go.mod h1:8T7/ajM9aGshvgpwCtXwIFpyF/R6CEn4W+/FLryNXWw= github.com/jfrog/gofrog v1.7.1 h1:ME1Meg4hukAT/7X6HUQCVSe4DNjMZACCP8aCY37EW/w= github.com/jfrog/gofrog v1.7.1/go.mod h1:X7bjfWoQDN0Z4FQGbE91j3gbPP7Urwzm4Z8tkvrlbRI= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-core/v2 v2.51.0 h1:nESbCpSTPZx1av0W9tdmWLxKaPSL1SaZinbZGtYNeFI= -github.com/jfrog/jfrog-cli-core/v2 v2.51.0/go.mod h1:064wSSHVI3ZIVi/a94yJqzs+ACM+9JK/u9tQ1sfTK6A= -github.com/jfrog/jfrog-client-go v1.40.1 h1:ISSSV7/IUS8R+QCPfH2lVKLburbv2Xn07fvNyDc17rI= -github.com/jfrog/jfrog-client-go v1.40.1/go.mod h1:FprEW0Sqhj6ZSFTFk9NCni+ovFAYMA3zCBmNX4hGXgQ= +github.com/jfrog/jfrog-cli-core/v2 v2.53.0 h1:qdZ1Svb1hGyRx2QviJtarhcA8eet8QtYU054nKzlhDg= +github.com/jfrog/jfrog-cli-core/v2 v2.53.0/go.mod h1:l101ZcbHy/FLieCx1xDtjONgkqsoLDNaqVT7b4KJ5OQ= +github.com/jfrog/jfrog-client-go v1.40.2 h1:zdCWPPT11r0bMGnAXGhZPb3RrIINhiTFCceQABhguZ4= +github.com/jfrog/jfrog-client-go v1.40.2/go.mod h1:m3hIn12eFWk5nJH1swPRtFrjXbiiCscOpX+v/vCdmNI= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -181,11 +181,11 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -199,6 +199,7 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -215,8 +216,8 @@ github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1ump github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= +github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= github.com/vbauerster/mpb/v7 v7.5.3 h1:BkGfmb6nMrrBQDFECR/Q7RkKCw7ylMetCb4079CGs4w= github.com/vbauerster/mpb/v7 v7.5.3/go.mod h1:i+h4QY6lmLvBNK2ah1fSreiw3ajskRlBp9AhY/PnuOE= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= @@ -244,14 +245,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -261,14 +262,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -295,15 +296,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -313,14 +314,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -334,7 +335,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/tests/testdata/other/jobSummary/multi_command_job.md b/tests/testdata/other/jobSummary/multi_command_job.md new file mode 100644 index 00000000..e26061cf --- /dev/null +++ b/tests/testdata/other/jobSummary/multi_command_job.md @@ -0,0 +1,16 @@ +#### Builds +| Status | Id | Details | +|--------|----|---------| +| ✅ | build-name (build-number) | | +| ❌ | build-name (build-number) |
Vulnerabilities found 3
└── 3 SCA 🔴 2 High
🟡 1 Low
| +#### Artifacts +| Status | Id | Details | +|--------|----|---------| +| ❌ | /binary-name |
Vulnerabilities found 3
└── 3 Secrets 🔴 2 High
🟡 1 Low
| +| ✅ | other-root/dir/binary-name2 | | +#### Modules +| Status | Id | Details | +|--------|----|---------| +| ❌ | /application1 |
Vulnerabilities found 14
├── 1 SAST 🟡 1 Low
├── 5 IAC 🟠 5 Medium
└── 8 SCA ❗️ 3 Critical (2 Not Applicable)
🔴 4 High (1 Applicable, 1 Not Applicable)
🟡 1 Low
| +| ❌ | /application2 |
Vulnerabilities found 1
└── 1 SCA 🔴 1 High (1 Not Applicable)
| +| ✅ | /dir/application3 | | \ No newline at end of file diff --git a/tests/testdata/other/jobSummary/single_issue.md b/tests/testdata/other/jobSummary/single_issue.md new file mode 100644 index 00000000..a9df2a5b --- /dev/null +++ b/tests/testdata/other/jobSummary/single_issue.md @@ -0,0 +1 @@ +
❌ Vulnerabilities found 3
└── 3 Secrets 🔴 2 High
🟡 1 Low
\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/single_no_issue.md b/tests/testdata/other/jobSummary/single_no_issue.md new file mode 100644 index 00000000..37677dd0 --- /dev/null +++ b/tests/testdata/other/jobSummary/single_no_issue.md @@ -0,0 +1,3 @@ +``` +✅ No vulnerabilities were found +``` \ No newline at end of file diff --git a/tests/testdata/projects/package-managers/maven/maven-example-with-many-types/pom.xml b/tests/testdata/projects/package-managers/maven/maven-example-with-many-types/pom.xml index 6d2096c9..5fc62ef7 100644 --- a/tests/testdata/projects/package-managers/maven/maven-example-with-many-types/pom.xml +++ b/tests/testdata/projects/package-managers/maven/maven-example-with-many-types/pom.xml @@ -42,6 +42,14 @@ commons-io 1.2 test + flavor1 + + + commons-io + commons-io + 1.2 + test + flavor2 org.webjars diff --git a/utils/analyticsmetrics_test.go b/utils/analyticsmetrics_test.go index 12f0bfb0..6b9fcf36 100644 --- a/utils/analyticsmetrics_test.go +++ b/utils/analyticsmetrics_test.go @@ -2,15 +2,16 @@ package utils import ( "errors" + "os" + "testing" + "time" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/jfrog/jfrog-client-go/xray/services" xscservices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" - "os" - "testing" - "time" ) const ( @@ -73,15 +74,24 @@ func TestAddGeneralEvent(t *testing.T) { func TestAnalyticsMetricsService_createAuditResultsFromXscAnalyticsBasicGeneralEvent(t *testing.T) { usageCallback := tests.SetEnvWithCallbackAndAssert(t, coreutils.ReportUsage, "true") defer usageCallback() - vulnerabilities := []services.Vulnerability{{IssueId: "CVE-123", Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}} + vulnerabilities := []services.Vulnerability{{IssueId: "XRAY-ID", Cves: []services.Cve{{Id: "CVE-123"}}, Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}} scaResults := []ScaScanResult{{XrayResults: []services.ScanResponse{{Vulnerabilities: vulnerabilities}}}} auditResults := Results{ ScaResults: scaResults, ExtendedScanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{{}, {}}, - SecretsScanResults: []*sarif.Run{{}, {}}, - IacScanResults: []*sarif.Run{{}, {}}, - SastScanResults: []*sarif.Run{{}, {}}, + ApplicabilityScanResults: []*sarif.Run{CreateRunWithDummyResults(CreateDummyPassingResult("applic_CVE-123"))}, + SecretsScanResults: []*sarif.Run{ + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("", 0, 0, 0, 0, ""))), + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("", 1, 1, 1, 1, ""))), + }, + IacScanResults: []*sarif.Run{ + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("", 0, 0, 0, 0, ""))), + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("", 1, 1, 1, 1, ""))), + }, + SastScanResults: []*sarif.Run{ + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("", 0, 0, 0, 0, ""))), + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("", 1, 1, 1, 1, ""))), + }, }, } testStruct := []struct { diff --git a/utils/analyzermanager.go b/utils/analyzermanager.go index 464520b9..f825337c 100644 --- a/utils/analyzermanager.go +++ b/utils/analyzermanager.go @@ -55,6 +55,21 @@ func (as ApplicabilityStatus) String() string { return string(as) } +func convertToApplicabilityStatus(status string) ApplicabilityStatus { + switch status { + case Applicable.String(): + return Applicable + case NotApplicable.String(): + return NotApplicable + case ApplicabilityUndetermined.String(): + return ApplicabilityUndetermined + case NotCovered.String(): + return NotCovered + default: + return NotScanned + } +} + type JasScanType string const ( diff --git a/utils/results.go b/utils/results.go index 3a1d8668..43870ac4 100644 --- a/utils/results.go +++ b/utils/results.go @@ -3,6 +3,7 @@ package utils import ( "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-security/formats" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/owenrumney/go-sarif/v2/sarif" ) @@ -59,6 +60,15 @@ func (r *Results) IsScaIssuesFound() bool { return false } +func (r *Results) getScaScanResultByTarget(target string) *ScaScanResult { + for _, scan := range r.ScaResults { + if scan.Target == target { + return &scan + } + } + return nil +} + func (r *Results) IsIssuesFound() bool { if r.IsScaIssuesFound() { return true @@ -71,44 +81,48 @@ func (r *Results) IsIssuesFound() bool { // Counts the total number of unique findings in the provided results. // A unique SCA finding is identified by a unique pair of vulnerability's/violation's issueId and component id or by a result returned from one of JAS scans. -func (r *Results) CountScanResultsFindings() int { - var totalFindings int - totalFindings += getScaResultsUniqueFindingsAmount(&r.ScaResults) - - if r.ExtendedScanResults != nil { - totalFindings += len(r.ExtendedScanResults.SastScanResults) - totalFindings += len(r.ExtendedScanResults.IacScanResults) - totalFindings += len(r.ExtendedScanResults.SecretsScanResults) - } - - return totalFindings +func (r *Results) CountScanResultsFindings() (total int) { + return formats.SummaryResults{Scans: r.getScanSummaryByTargets()}.GetTotalIssueCount() } -func getScaResultsUniqueFindingsAmount(scaScanResults *[]ScaScanResult) int { - uniqueXrayFindings := datastructures.MakeSet[string]() - - for _, scaResult := range *scaScanResults { - for _, xrayResult := range scaResult.XrayResults { - // XrayResults may contain Vulnerabilities OR Violations, but not both. Therefore, only one of them will be counted - for _, vulnerability := range xrayResult.Vulnerabilities { - for compId := range vulnerability.Components { - uniqueXrayFindings.Add(vulnerability.IssueId + compId) - } - } +func (r *Results) GetSummary() (summary formats.SummaryResults) { + if len(r.ScaResults) <= 1 { + summary.Scans = r.getScanSummaryByTargets() + return + } + for _, scaScan := range r.ScaResults { + summary.Scans = append(summary.Scans, r.getScanSummaryByTargets(scaScan.Target)...) + } + return +} - for _, violation := range xrayResult.Violations { - for compId := range violation.Components { - uniqueXrayFindings.Add(violation.IssueId + compId) - } - } +// Returns a summary for the provided targets. If no targets are provided, a summary for all targets is returned. +func (r *Results) getScanSummaryByTargets(targets ...string) (summaries []formats.ScanSummaryResult) { + if len(targets) == 0 { + // No filter, one scan summary for all targets + summaries = append(summaries, getScanSummary(r.ExtendedScanResults, r.ScaResults...)) + return + } + for _, target := range targets { + // Get target sca results + targetScaResults := []ScaScanResult{} + if targetScaResult := r.getScaScanResultByTarget(target); targetScaResult != nil { + targetScaResults = append(targetScaResults, *targetScaResult) + } + // Get target extended results + targetExtendedResults := r.ExtendedScanResults + if targetExtendedResults != nil { + targetExtendedResults = targetExtendedResults.GetResultsForTarget(target) } + summaries = append(summaries, getScanSummary(targetExtendedResults, targetScaResults...)) } - return uniqueXrayFindings.Size() + return } type ScaScanResult struct { - Technology coreutils.Technology `json:"Technology"` - WorkingDirectory string `json:"WorkingDirectory"` + // Could be working directory (audit), file path (binary scan) or build name+number (build scan) + Target string `json:"Target"` + Technology coreutils.Technology `json:"Technology,omitempty"` XrayResults []services.ScanResponse `json:"XrayResults,omitempty"` Descriptors []string `json:"Descriptors,omitempty"` IsMultipleRootProject *bool `json:"IsMultipleRootProject,omitempty"` @@ -137,3 +151,12 @@ func (e *ExtendedScanResults) IsIssuesFound() bool { GetResultsLocationCount(e.IacScanResults...) > 0 || GetResultsLocationCount(e.SastScanResults...) > 0 } + +func (e *ExtendedScanResults) GetResultsForTarget(target string) (result *ExtendedScanResults) { + return &ExtendedScanResults{ + ApplicabilityScanResults: GetRunsByWorkingDirectory(target, e.ApplicabilityScanResults...), + SecretsScanResults: GetRunsByWorkingDirectory(target, e.SecretsScanResults...), + IacScanResults: GetRunsByWorkingDirectory(target, e.IacScanResults...), + SastScanResults: GetRunsByWorkingDirectory(target, e.SastScanResults...), + } +} diff --git a/utils/results_test.go b/utils/results_test.go new file mode 100644 index 00000000..40fde1e3 --- /dev/null +++ b/utils/results_test.go @@ -0,0 +1,160 @@ +package utils + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-security/formats" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/stretchr/testify/assert" +) + +func TestGetScaScanResultByTarget(t *testing.T) { + target1 := &ScaScanResult{Target: "target1"} + target2 := &ScaScanResult{Target: "target2"} + testCases := []struct { + name string + results Results + target string + expected *ScaScanResult + }{ + { + name: "Sca scan result by target", + results: Results{ + ScaResults: []ScaScanResult{ + *target1, + *target2, + }, + }, + target: "target1", + expected: target1, + }, + { + name: "Sca scan result by target not found", + results: Results{ + ScaResults: []ScaScanResult{ + *target1, + *target2, + }, + }, + target: "target3", + expected: nil, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := testCase.results.getScaScanResultByTarget(testCase.target) + assert.Equal(t, testCase.expected, result) + }) + } +} + +func TestGetSummary(t *testing.T) { + dummyScaVulnerabilities := []services.Vulnerability{ + {IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, + {IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, + } + dummyExtendedScanResults := &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + CreateRunWithDummyResults(CreateDummyPassingResult("applic_CVE-2")).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), + }), + }, + SecretsScanResults: []*sarif.Run{ + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("target1/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), + }), + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("target2/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target2")), + }), + }, + SastScanResults: []*sarif.Run{ + CreateRunWithDummyResults(CreateResultWithLocations("", "", "note", CreateLocation("target1/file2", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), + }), + }, + } + + testCases := []struct { + name string + results Results + expected formats.SummaryResults + findingCount int + }{ + { + name: "Empty results", + results: Results{ScaResults: []ScaScanResult{}}, + expected: formats.SummaryResults{Scans: []formats.ScanSummaryResult{{}}}, + findingCount: 0, + }, + { + name: "One module result", + results: Results{ + ScaResults: []ScaScanResult{{ + Target: "target1", + XrayResults: []services.ScanResponse{{Vulnerabilities: dummyScaVulnerabilities}}, + }}, + ExtendedScanResults: dummyExtendedScanResults, + }, + expected: formats.SummaryResults{ + Scans: []formats.ScanSummaryResult{ + { + Target: "target1", + ScaScanResults: &formats.ScaSummaryCount{ + "Critical": formats.SummaryCount{"Undetermined": 1}, + "High": formats.SummaryCount{"Not Applicable": 1}, + }, + SecretsScanResults: &formats.SummaryCount{"Low": 2}, + SastScanResults: &formats.SummaryCount{"Low": 1}, + }, + }, + }, + findingCount: 5, + }, + { + name: "Multiple module results", + results: Results{ + ScaResults: []ScaScanResult{ + { + Target: "target1", + XrayResults: []services.ScanResponse{{Vulnerabilities: dummyScaVulnerabilities}}, + }, + { + Target: "target2", + XrayResults: []services.ScanResponse{{Vulnerabilities: dummyScaVulnerabilities}}, + }, + }, + ExtendedScanResults: dummyExtendedScanResults, + }, + expected: formats.SummaryResults{ + Scans: []formats.ScanSummaryResult{ + { + Target: "target1", + ScaScanResults: &formats.ScaSummaryCount{ + "Critical": formats.SummaryCount{"Undetermined": 1}, + "High": formats.SummaryCount{"Not Applicable": 1}, + }, + SecretsScanResults: &formats.SummaryCount{"Low": 1}, + SastScanResults: &formats.SummaryCount{"Low": 1}, + }, + { + Target: "target2", + ScaScanResults: &formats.ScaSummaryCount{ + "Critical": formats.SummaryCount{"": 1}, + "High": formats.SummaryCount{"": 1}, + }, + SecretsScanResults: &formats.SummaryCount{"Low": 1}, + }, + }, + }, + findingCount: 5, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := testCase.results.GetSummary() + assert.Equal(t, testCase.expected, result) + assert.Equal(t, testCase.findingCount, testCase.results.CountScanResultsFindings()) + }) + } +} diff --git a/utils/resultstable.go b/utils/resultstable.go index b8da7325..1badb738 100644 --- a/utils/resultstable.go +++ b/utils/resultstable.go @@ -93,7 +93,7 @@ func prepareViolations(violations []services.Violation, results *Results, multip cves := convertCves(violation.Cves) if results.ExtendedScanResults.EntitledForJas { for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i], results.ExtendedScanResults.ApplicabilityScanResults, violation.Components) + cves[i].Applicability = getCveApplicabilityField(cves[i].Id, results.ExtendedScanResults.ApplicabilityScanResults, violation.Components) } } applicabilityStatus := getApplicableCveStatus(results.ExtendedScanResults.EntitledForJas, results.ExtendedScanResults.ApplicabilityScanResults, cves) @@ -218,7 +218,7 @@ func prepareVulnerabilities(vulnerabilities []services.Vulnerability, results *R cves := convertCves(vulnerability.Cves) if results.ExtendedScanResults.EntitledForJas { for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i], results.ExtendedScanResults.ApplicabilityScanResults, vulnerability.Components) + cves[i].Applicability = getCveApplicabilityField(cves[i].Id, results.ExtendedScanResults.ApplicabilityScanResults, vulnerability.Components) } } applicabilityStatus := getApplicableCveStatus(results.ExtendedScanResults.EntitledForJas, results.ExtendedScanResults.ApplicabilityScanResults, cves) @@ -951,7 +951,7 @@ func getApplicableCveStatus(entitledForJas bool, applicabilityScanResults []*sar return getFinalApplicabilityStatus(applicableStatuses) } -func getCveApplicabilityField(cve formats.CveRow, applicabilityScanResults []*sarif.Run, components map[string]services.Component) *formats.Applicability { +func getCveApplicabilityField(cveId string, applicabilityScanResults []*sarif.Run, components map[string]services.Component) *formats.Applicability { if len(applicabilityScanResults) == 0 { return nil } @@ -960,14 +960,14 @@ func getCveApplicabilityField(cve formats.CveRow, applicabilityScanResults []*sa resultFound := false var applicabilityStatuses []ApplicabilityStatus for _, applicabilityRun := range applicabilityScanResults { - if rule, _ := applicabilityRun.GetRuleById(CveToApplicabilityRuleId(cve.Id)); rule != nil { + if rule, _ := applicabilityRun.GetRuleById(CveToApplicabilityRuleId(cveId)); rule != nil { applicability.ScannerDescription = GetRuleFullDescription(rule) status := getApplicabilityStatusFromRule(rule) if status != "" { applicabilityStatuses = append(applicabilityStatuses, status) } } - result, _ := applicabilityRun.GetResultByRuleId(CveToApplicabilityRuleId(cve.Id)) + result, _ := applicabilityRun.GetResultByRuleId(CveToApplicabilityRuleId(cveId)) if result == nil { continue } diff --git a/utils/resultstable_test.go b/utils/resultstable_test.go index 74202b57..af83a690 100644 --- a/utils/resultstable_test.go +++ b/utils/resultstable_test.go @@ -573,7 +573,7 @@ func TestGetApplicableCveValue(t *testing.T) { for _, testCase := range testCases { cves := convertCves(testCase.cves) for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i], testCase.scanResults.ApplicabilityScanResults, nil) + cves[i].Applicability = getCveApplicabilityField(cves[i].Id, testCase.scanResults.ApplicabilityScanResults, nil) } applicableValue := getApplicableCveStatus(testCase.scanResults.EntitledForJas, testCase.scanResults.ApplicabilityScanResults, cves) assert.Equal(t, testCase.expectedResult, applicableValue) diff --git a/utils/sarifutils.go b/utils/sarifutils.go index 0da57714..2996a384 100644 --- a/utils/sarifutils.go +++ b/utils/sarifutils.go @@ -97,12 +97,18 @@ func isSameLocation(location *sarif.Location, other *sarif.Location) bool { if location == other { return true } - return GetLocationFileName(location) == GetLocationFileName(other) && - GetLocationSnippet(location) == GetLocationSnippet(other) && - GetLocationStartLine(location) == GetLocationStartLine(other) && - GetLocationStartColumn(location) == GetLocationStartColumn(other) && - GetLocationEndLine(location) == GetLocationEndLine(other) && - GetLocationEndColumn(location) == GetLocationEndColumn(other) + return GetLocationId(location) == GetLocationId(other) +} + +func GetLocationId(location *sarif.Location) string { + return fmt.Sprintf("%s:%s:%d:%d:%d:%d", + GetLocationFileName(location), + GetLocationSnippet(location), + GetLocationStartLine(location), + GetLocationStartColumn(location), + GetLocationEndLine(location), + GetLocationEndColumn(location), + ) } func GetResultsLocationCount(runs ...*sarif.Run) (count int) { @@ -114,6 +120,20 @@ func GetResultsLocationCount(runs ...*sarif.Run) (count int) { return } +func GetRunsByWorkingDirectory(workingDirectory string, runs ...*sarif.Run) (filteredRuns []*sarif.Run) { + for _, run := range runs { + for _, invocation := range run.Invocations { + runWorkingDir := GetInvocationWorkingDirectory(invocation) + if runWorkingDir == workingDirectory { + filteredRuns = append(filteredRuns, run) + break + } + } + } + return + +} + func GetResultMsgText(result *sarif.Result) string { if result.Message.Text != nil { return *result.Message.Text diff --git a/utils/securityJobSummary.go b/utils/securityJobSummary.go new file mode 100644 index 00000000..a8c3d245 --- /dev/null +++ b/utils/securityJobSummary.go @@ -0,0 +1,420 @@ +package utils + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-core/v2/commandsummary" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-security/formats" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" + "golang.org/x/exp/maps" +) + +const ( + Build SecuritySummarySection = "Builds" + Binary SecuritySummarySection = "Artifacts" + Modules SecuritySummarySection = "Modules" +) + +type SecuritySummarySection string + +type ScanCommandSummaryResult struct { + Section SecuritySummarySection `json:"section"` + Results formats.SummaryResults `json:"results"` +} + +type SecurityCommandsSummary struct { + BuildScanCommands []formats.SummaryResults `json:"buildScanCommands"` + ScanCommands []formats.SummaryResults `json:"scanCommands"` + AuditCommands []formats.SummaryResults `json:"auditCommands"` +} + +// Manage the job summary for security commands +func SecurityCommandsJobSummary() (js *commandsummary.CommandSummary, err error) { + return commandsummary.New(&SecurityCommandsSummary{}, "security") +} + +// Record the security command output +func RecordSecurityCommandOutput(content ScanCommandSummaryResult) (err error) { + if !commandsummary.ShouldRecordSummary() { + return + } + manager, err := SecurityCommandsJobSummary() + if err != nil || manager == nil { + return + } + return manager.Record(content) +} + +func (scs *SecurityCommandsSummary) GenerateMarkdownFromFiles(dataFilePaths []string) (markdown string, err error) { + if err = loadContentFromFiles(dataFilePaths, scs); err != nil { + return "", fmt.Errorf("failed while creating security markdown: %w", err) + } + return ConvertSummaryToString(*scs) +} + +func loadContentFromFiles(dataFilePaths []string, scs *SecurityCommandsSummary) (err error) { + for _, dataFilePath := range dataFilePaths { + // Load file content + var cmdResults ScanCommandSummaryResult + if err = commandsummary.UnmarshalFromFilePath(dataFilePath, &cmdResults); err != nil { + return fmt.Errorf("failed while Unmarshal '%s': %w", dataFilePath, err) + } + // Append the new data + switch cmdResults.Section { + case Build: + scs.BuildScanCommands = append(scs.BuildScanCommands, cmdResults.Results) + case Binary: + scs.ScanCommands = append(scs.ScanCommands, cmdResults.Results) + case Modules: + scs.AuditCommands = append(scs.AuditCommands, cmdResults.Results) + } + } + return +} + +func (scs *SecurityCommandsSummary) GetOrderedSectionsWithContent() (sections []SecuritySummarySection) { + if len(scs.BuildScanCommands) > 0 { + sections = append(sections, Build) + } + if len(scs.ScanCommands) > 0 { + sections = append(sections, Binary) + } + if len(scs.AuditCommands) > 0 { + sections = append(sections, Modules) + } + return + +} + +func (scs *SecurityCommandsSummary) GetSectionSummaries(section SecuritySummarySection) (summaries []formats.SummaryResults) { + switch section { + case Build: + summaries = scs.BuildScanCommands + case Binary: + summaries = scs.ScanCommands + case Modules: + summaries = scs.AuditCommands + } + return +} + +func ConvertSummaryToString(results SecurityCommandsSummary) (summary string, err error) { + sectionsWithContent := results.GetOrderedSectionsWithContent() + addSectionTitle := len(sectionsWithContent) > 1 + var sectionSummary string + for i, section := range sectionsWithContent { + if sectionSummary, err = GetSummaryString(results.GetSectionSummaries(section)...); err != nil { + return + } + if addSectionTitle { + if i > 0 { + summary += "\n" + } + summary += fmt.Sprintf("#### %s\n", section) + } + summary += sectionSummary + } + return +} + +func GetSummaryString(summaries ...formats.SummaryResults) (str string, err error) { + parsed := 0 + singleScan := isSingleCommandAndScan(summaries...) + wd, err := coreutils.GetWorkingDirectory() + if err != nil { + return + } + if !singleScan { + str += "| Status | Id | Details |\n|--------|----|---------|\n" + } + for i := range summaries { + if !singleScan { + updateSummaryNamesToRelativePath(&summaries[i], wd) + } + for _, scan := range summaries[i].Scans { + if parsed > 0 { + str += "\n" + } + str += GetScanSummaryString(scan, singleScan) + parsed++ + } + } + return +} + +func isSingleCommandAndScan(summaries ...formats.SummaryResults) bool { + if len(summaries) != 1 { + return false + } + if len(summaries[0].Scans) != 1 { + return false + } + // One command and one scan + return true +} + +func GetScanSummaryString(summary formats.ScanSummaryResult, singleData bool) (content string) { + // single data -> no table + hasIssues := summary.HasIssues() + if !hasIssues { + if singleData { + return "```\n✅ No vulnerabilities were found\n```" + } + return fmt.Sprintf("| ✅ | %s | |", summary.Target) + } + issueDetails := getDetailsString(summary) + if singleData { + return fmt.Sprintf("
❌ %s
", issueDetails) + } + return fmt.Sprintf("| ❌ | %s |
%s
|", summary.Target, issueDetails) +} + +func getDetailsString(summary formats.ScanSummaryResult) (content string) { + content = fmt.Sprintf("Vulnerabilities found %d", summary.GetTotalIssueCount()) + // Display sub scans with issues + subScansWithIssues := summary.GetSubScansWithIssues() + for i, subScanType := range subScansWithIssues { + content += fmt.Sprintf("
%s", getListItemPrefix(i, len(subScansWithIssues))) + subScanPrefix := fmt.Sprintf("%d ", summary.GetSubScanTotalIssueCount(subScanType)) + switch subScanType { + case formats.ScaScan: + subScanPrefix += "SCA " + case formats.IacScan: + subScanPrefix += "IAC " + case formats.SecretsScan: + subScanPrefix += "Secrets " + case formats.SastScan: + subScanPrefix += "SAST " + } + content += subScanPrefix + getSubScanSummaryCountsString(summary, subScanType, getPrefixPadding(subScanPrefix)) + } + return +} + +func getPrefixPadding(prefix string) int { + // 4 spaces for the list item prefix (len not equal to actual length) + return 4 + len(prefix) +} + +func getListItemPrefix(index, total int) (content string) { + if index == total-1 { + content += "└── " + return + } + content += "├── " + return +} + +func getSubScanSummaryCountsString(summary formats.ScanSummaryResult, subScanType formats.SummarySubScanType, padding int) (content string) { + switch subScanType { + case formats.ScaScan: + content += GetScaSummaryCountString(*summary.ScaScanResults, padding) + case formats.IacScan: + content += GetSeveritySummaryCountString(*summary.IacScanResults, padding) + case formats.SecretsScan: + content += GetSeveritySummaryCountString(*summary.SecretsScanResults, padding) + case formats.SastScan: + content += GetSeveritySummaryCountString(*summary.SastScanResults, padding) + } + return +} + +func hasApplicableDataToDisplayInSummary(summary formats.ScaSummaryCount) bool { + for _, statuses := range summary { + sorted := getSummarySortedKeysToDisplay(maps.Keys(statuses)...) + for _, status := range sorted { + if _, ok := statuses[status]; ok && statuses[status] > 0 { + return true + } + } + } + return false +} + +func GetScaSummaryCountString(summary formats.ScaSummaryCount, padding int) (content string) { + severityCount := len(summary) + if severityCount == 0 { + return + } + if !hasApplicableDataToDisplayInSummary(summary) { + return GetSeveritySummaryCountString(summary.GetSeverityCountsWithoutStatus(), padding) + } + // Display contextual-analysis details + keys := getSummarySortedKeysToDisplay(maps.Keys(summary)...) + for i, severity := range keys { + if i > 0 { + content += "
" + strings.Repeat(" ", padding) + } + statusCounts := summary[severity] + content += fmt.Sprintf("%s%s", + fmt.Sprintf(summaryContentToFormatString[severity], statusCounts.GetTotal()), + GetSummaryContentString(statusCounts, ", ", true), + ) + } + return +} + +var summaryContentToFormatString = map[string]string{ + "Critical": `❗️ %d Critical`, + "High": `🔴 %d High`, + "Medium": `🟠 %d Medium`, + "Low": `🟡 %d Low`, + "Unknown": `⚪️ %d Unknown`, + string(Applicable): "%d " + string(Applicable), + string(NotApplicable): "%d " + string(NotApplicable), +} + +func getSummarySortedKeysToDisplay(keys ...string) (sorted []string) { + if len(keys) == 0 { + return + } + keysSet := datastructures.MakeSetFromElements(keys...) + allowedSorted := []string{ + "Critical", "High", "Medium", "Low", "Unknown", + string(Applicable), string(NotApplicable), + } + for _, key := range allowedSorted { + if keysSet.Exists(key) { + sorted = append(sorted, key) + } + } + return +} + +func GetSeveritySummaryCountString(summary formats.SummaryCount, padding int) (content string) { + return GetSummaryContentString(summary, "
"+strings.Repeat(" ", padding), false) +} + +func GetSummaryContentString(summary formats.SummaryCount, delimiter string, wrap bool) (content string) { + // sort and filter + keys := getSummarySortedKeysToDisplay(maps.Keys(summary)...) + if len(keys) == 0 { + return + } + for i, key := range keys { + if i > 0 { + content += delimiter + } + content += fmt.Sprintf(summaryContentToFormatString[key], summary[key]) + } + if wrap { + content = fmt.Sprintf(" (%s)", content) + } + return +} + +func updateSummaryNamesToRelativePath(summary *formats.SummaryResults, wd string) { + for i, scan := range summary.Scans { + if scan.Target == "" { + continue + } + if !strings.HasPrefix(scan.Target, wd) { + continue + } + if scan.Target == wd { + summary.Scans[i].Target = filepath.Base(wd) + } + summary.Scans[i].Target = strings.TrimPrefix(scan.Target, wd) + } +} + +func getScanSummary(extendedScanResults *ExtendedScanResults, scaResults ...ScaScanResult) (summary formats.ScanSummaryResult) { + if len(scaResults) == 1 { + summary.Target = scaResults[0].Target + } + if extendedScanResults == nil { + summary.ScaScanResults = getScaSummaryResults(&scaResults) + return + } + summary.ScaScanResults = getScaSummaryResults(&scaResults, extendedScanResults.ApplicabilityScanResults...) + summary.IacScanResults = getJASSummaryCount(extendedScanResults.IacScanResults...) + summary.SecretsScanResults = getJASSummaryCount(extendedScanResults.SecretsScanResults...) + summary.SastScanResults = getJASSummaryCount(extendedScanResults.SastScanResults...) + return +} + +type SeverityWithApplicable struct { + SeverityInfo *TableSeverity + ApplicabilityStatus ApplicabilityStatus +} + +func getCveId(cve services.Cve, defaultIssueId string) string { + if cve.Id == "" { + return defaultIssueId + } + return cve.Id +} + +func getUniqueVulnerabilitiesInfo(cves []services.Cve, issueId, severity string, components map[string]services.Component, applicableRuns ...*sarif.Run) (uniqueFindings map[string]SeverityWithApplicable) { + uniqueFindings = map[string]SeverityWithApplicable{} + for _, cve := range cves { + cveId := getCveId(cve, issueId) + for compId := range components { + applicableStatus := NotScanned + if applicableInfo := getCveApplicabilityField(cveId, applicableRuns, components); applicableInfo != nil { + applicableStatus = convertToApplicabilityStatus(applicableInfo.Status) + } + uniqueFindings[cveId+compId] = SeverityWithApplicable{SeverityInfo: GetSeverity(severity, applicableStatus), ApplicabilityStatus: applicableStatus} + } + } + return +} + +func getScaSummaryResults(scaScanResults *[]ScaScanResult, applicableRuns ...*sarif.Run) *formats.ScaSummaryCount { + uniqueFindings := map[string]SeverityWithApplicable{} + if len(*scaScanResults) == 0 { + return nil + } + // Aggregate unique findings + for _, scaResult := range *scaScanResults { + for _, xrayResult := range scaResult.XrayResults { + for _, vulnerability := range xrayResult.Vulnerabilities { + vulUniqueFindings := getUniqueVulnerabilitiesInfo(vulnerability.Cves, vulnerability.IssueId, vulnerability.Severity, vulnerability.Components, applicableRuns...) + for key, value := range vulUniqueFindings { + uniqueFindings[key] = value + } + } + for _, violation := range xrayResult.Violations { + vioUniqueFindings := getUniqueVulnerabilitiesInfo(violation.Cves, violation.IssueId, violation.Severity, violation.Components, applicableRuns...) + for key, value := range vioUniqueFindings { + uniqueFindings[key] = value + } + } + } + } + // Create summary + summary := formats.ScaSummaryCount{} + for _, severityWithApplicable := range uniqueFindings { + severity := severityWithApplicable.SeverityInfo.Severity + status := severityWithApplicable.ApplicabilityStatus.String() + if _, ok := summary[severity]; !ok { + summary[severity] = formats.SummaryCount{} + } + summary[severity][status]++ + } + return &summary +} + +func getJASSummaryCount(runs ...*sarif.Run) *formats.SummaryCount { + if len(runs) == 0 { + return nil + } + count := formats.SummaryCount{} + issueToSeverity := map[string]string{} + for _, run := range runs { + for _, result := range run.Results { + for _, location := range result.Locations { + issueToSeverity[GetLocationId(location)] = GetResultSeverity(result) + } + } + } + for _, severity := range issueToSeverity { + count[severity]++ + } + return &count +} diff --git a/utils/securityJobSummary_test.go b/utils/securityJobSummary_test.go new file mode 100644 index 00000000..7edb73c2 --- /dev/null +++ b/utils/securityJobSummary_test.go @@ -0,0 +1,141 @@ +package utils + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/jfrog/jfrog-cli-security/formats" + "github.com/stretchr/testify/assert" +) + +var ( + summaryExpectedContentDir = filepath.Join("..", "tests", "testdata", "other", "jobSummary") +) + +func TestConvertSummaryToString(t *testing.T) { + wd, err := os.Getwd() + assert.NoError(t, err) + + testCases := []struct { + name string + summary SecurityCommandsSummary + expectedContentPath string + }{ + { + name: "One Section - No Issues", + summary: getDummySecurityCommandsSummary( + ScanCommandSummaryResult{ + Section: Binary, + Results: formats.SummaryResults{Scans: []formats.ScanSummaryResult{{Target: filepath.Join(wd, "binary-name")}}}, + }, + ), + expectedContentPath: filepath.Join(summaryExpectedContentDir, "single_no_issue.md"), + }, + { + name: "One Section - With Issues", + summary: getDummySecurityCommandsSummary( + ScanCommandSummaryResult{ + Section: Build, + Results: formats.SummaryResults{Scans: []formats.ScanSummaryResult{{ + Target: "build-name (build-number)", + SecretsScanResults: &formats.SummaryCount{"Low": 1, "High": 2}, + }}}, + }, + ), + expectedContentPath: filepath.Join(summaryExpectedContentDir, "single_issue.md"), + }, + { + name: "Multiple Sections", + summary: getDummySecurityCommandsSummary( + ScanCommandSummaryResult{ + Section: Build, + Results: formats.SummaryResults{Scans: []formats.ScanSummaryResult{{Target: "build-name (build-number)"}}}, + }, + ScanCommandSummaryResult{ + Section: Build, + Results: formats.SummaryResults{Scans: []formats.ScanSummaryResult{{ + Target: "build-name (build-number)", + ScaScanResults: &formats.ScaSummaryCount{"Low": formats.SummaryCount{"": 1}, "High": formats.SummaryCount{"": 2}}, + }}}, + }, + ScanCommandSummaryResult{ + Section: Binary, + Results: formats.SummaryResults{Scans: []formats.ScanSummaryResult{ + { + Target: filepath.Join(wd, "binary-name"), + ScaScanResults: &formats.ScaSummaryCount{}, + SecretsScanResults: &formats.SummaryCount{"Low": 1, "High": 2}, + }, + { + Target: filepath.Join("other-root", "dir", "binary-name2"), + ScaScanResults: &formats.ScaSummaryCount{}, + }, + }}, + }, + ScanCommandSummaryResult{ + Section: Modules, + Results: formats.SummaryResults{Scans: []formats.ScanSummaryResult{ + { + Target: filepath.Join(wd, "application1"), + SastScanResults: &formats.SummaryCount{"Low": 1}, + IacScanResults: &formats.SummaryCount{"Medium": 5}, + ScaScanResults: &formats.ScaSummaryCount{ + "Critical": formats.SummaryCount{"Undetermined": 1, "Not Applicable": 2}, + "High": formats.SummaryCount{"Applicable": 1, "Not Applicable": 1, "Not Covered": 2}, + "Low": formats.SummaryCount{"Undetermined": 1}, + }, + }, + { + Target: filepath.Join(wd, "application2"), + ScaScanResults: &formats.ScaSummaryCount{ + "High": formats.SummaryCount{"Not Applicable": 1}, + }, + }, + { + Target: filepath.Join(wd, "dir", "application3"), + }, + }}, + }, + ), + expectedContentPath: filepath.Join(summaryExpectedContentDir, "multi_command_job.md"), + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // Read expected content from file + expectedContent := getOutputFromFile(t, testCase.expectedContentPath) + summary, err := ConvertSummaryToString(testCase.summary) + assert.NoError(t, err) + assert.Equal(t, expectedContent, summary) + }) + } +} + +func getOutputFromFile(t *testing.T, path string) string { + content, err := os.ReadFile(path) + assert.NoError(t, err) + return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(content), "\r\n", "\n"), "/", string(filepath.Separator)), "<"+string(filepath.Separator), "= maxUniqueAppearances || childNode.NodeHasLoop() { + continue + } + currNode.Nodes = append(currNode.Nodes, childNode) + populateXrayDependencyTree(childNode, treeHelper, dependencyAppearances) + } +}