Skip to content

Commit

Permalink
Add support for Pnpm SCA scan
Browse files Browse the repository at this point in the history
  • Loading branch information
attiasas committed Jan 31, 2024
1 parent 0fc5abe commit 71bbec6
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 8 deletions.
2 changes: 1 addition & 1 deletion commands/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func RunAudit(auditParams *AuditParams) (results *xrayutils.Results, err error)
}

// The sca scan doesn't require the analyzer manager, so it can run separately from the analyzer manager download routine.
results.ScaError = runScaScan(auditParams, results) // runScaScan(auditParams, results)
results.ScaError = runScaScan(auditParams, results)

// Wait for the Download of the AnalyzerManager to complete.
if err = errGroup.Wait(); err != nil {
Expand Down
38 changes: 34 additions & 4 deletions commands/audit/sca/common.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package sca

import (
"bytes"
"fmt"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-core/v2/utils/tests"
"github.com/jfrog/jfrog-cli-security/scangraph"
Expand All @@ -10,10 +16,6 @@ import (
"github.com/jfrog/jfrog-client-go/utils/log"
"github.com/jfrog/jfrog-client-go/xray/services"
xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
"os/exec"
"path/filepath"
"strings"
"testing"
)

func RunXrayDependenciesTreeScanGraph(dependencyTree *xrayUtils.GraphNode, progress ioUtils.ProgressMgr, technology coreutils.Technology, scanGraphParams *scangraph.ScanGraphParams) (results []services.ScanResponse, err error) {
Expand Down Expand Up @@ -63,6 +65,34 @@ func LogExecutableVersion(executable string) {
log.Debug(fmt.Sprintf("Used %q version: %s", executable, version))
}

func RunCmdAndGetOutput(executablePath, workingDir string, rawArgs ...string) (stdResult, errResult []byte, err error) {
// Prepare the command
args := make([]string, 0)
for i := 0; i < len(rawArgs); i++ {
if strings.TrimSpace(rawArgs[i]) != "" {
args = append(args, rawArgs[i])
}
}
cmdName := filepath.Base(executablePath)
command := exec.Command(executablePath, args...)
command.Dir = workingDir
outBuffer := bytes.NewBuffer([]byte{})
command.Stdout = outBuffer
errBuffer := bytes.NewBuffer([]byte{})
command.Stderr = errBuffer
// Run the command
log.Debug(fmt.Sprintf("Running '%s %s' command at %s", cmdName, strings.Join(rawArgs, " "), workingDir))
err = command.Run()
errResult = errBuffer.Bytes()
stdResult = outBuffer.Bytes()
if err != nil {
err = fmt.Errorf("error while running '%s %s': %s\n%s", executablePath, strings.Join(args, " "), err.Error(), strings.TrimSpace(string(errResult)))
return
}
log.Debug(fmt.Sprintf("%s '%s' standard output is:\n%s", cmdName, strings.Join(args, " "), strings.TrimSpace(string(stdResult))))
return
}

// BuildImpactPathsForScanResponse builds the full impact paths for each vulnerability found in the scanResult argument, using the dependencyTrees argument.
// Returns the updated services.ScanResponse slice.
func BuildImpactPathsForScanResponse(scanResult []services.ScanResponse, dependencyTree []*xrayUtils.GraphNode) []services.ScanResponse {
Expand Down
137 changes: 137 additions & 0 deletions commands/audit/sca/pnpm/pnpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package pnpm

import (
"encoding/json"
"errors"
"os/exec"
"strings"

"github.com/jfrog/gofrog/datastructures"
"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"
"github.com/jfrog/jfrog-client-go/utils/log"

coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray"
xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
)

type pnpmLsProject struct {
Name string `json:"name"`
Version string `json:"version"`
Dependencies map[string]pnpmLsDependency `json:"dependencies,omitempty"`
}

type pnpmLsDependency struct {
From string `json:"from"`
Version string `json:"version"`
Dependencies map[string]pnpmLsDependency `json:"dependencies,omitempty"`
// binary location
Resolved string `json:"resolved"`
}

func BuildDependencyTree(params utils.AuditParams) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) {
currentDir, err := coreutils.GetWorkingDirectory()
if err != nil {
return
}
pnpmExecPath, err := getPnpmExecPath()
if err != nil {
return
}
// Run 'pnpm ls...' command and parse the returned result to create a dependencies map.
projectInfo, err := calculateDependencies(pnpmExecPath, currentDir)
if err != nil {
return
}
dependencyTrees, uniqueDeps = parsePnpmDependenciesList(projectInfo)
return
}

func getPnpmExecPath() (string, error) {
pnpmExecPath, err := exec.LookPath("pnpm")
if err != nil {
return "", err
}
if pnpmExecPath == "" {
return "", errors.New("could not find the 'pnpm' executable in the system PATH")
}
log.Debug("Using pnpm executable:", pnpmExecPath)
// Validate pnpm version
_, _, err = sca.RunCmdAndGetOutput(pnpmExecPath, "", "--version")
if err != nil {
return "", err
}
return pnpmExecPath, nil
}

// Run 'pnpm ls ...' command and parse the returned result to create a dependencies map of.
func calculateDependencies(executablePath, workingDir string) ([]pnpmLsProject, error) {
npmLsCmdContent, errData, err := sca.RunCmdAndGetOutput(executablePath, workingDir, "ls", "--depth", "Infinity", "--json", "--long")
if err != nil {
return nil, err
} else if len(errData) > 0 {
log.Warn("Encountered some issues while running 'pnpm ls' command:\n" + strings.TrimSpace(string(errData)))
}
output := &[]pnpmLsProject{}
if err := json.Unmarshal(npmLsCmdContent, output); err != nil {
return nil, err
}
return *output, nil
}

func parsePnpmDependenciesList(projectInfo []pnpmLsProject) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string) {
uniqueDepsSet := datastructures.MakeSet[string]()
for _, project := range projectInfo {
treeMap := createProjectDependenciesTree(project)
// Parse the dependencies into Xray dependency tree format
dependencyTree, uniqueProjectDeps := coreXray.BuildXrayDependencyTree(treeMap, getDependencyId(project.Name, project.Version))
dependencyTrees = append(dependencyTrees, dependencyTree)
// Add the dependencies to the unique dependencies set
uniqueDepsSet.AddElements(uniqueProjectDeps...)
}
uniqueDeps = uniqueDepsSet.ToSlice()
return
}

func createProjectDependenciesTree(project pnpmLsProject) map[string][]string {
treeMap := make(map[string][]string)
// Create a map of the project's dependencies
directDependencies := []string{}
projectId := getDependencyId(project.Name, project.Version)
for depName, dependency := range project.Dependencies {
directDependency := getDependencyId(depName, dependency.Version)
directDependencies = append(directDependencies, directDependency)
appendTransitiveDependencies(directDependency, dependency.Dependencies, treeMap)
}
if len(directDependencies) > 0 {
treeMap[projectId] = directDependencies
}
return treeMap
}

// Return npm://<name>:<version> of a dependency
func getDependencyId(depName, version string) string {
return utils.NpmPackageTypeIdentifier + depName + ":" + version
}

func appendTransitiveDependencies(parent string, dependencies map[string]pnpmLsDependency, result map[string][]string) {
for depName, dependency := range dependencies {
dependencyId := getDependencyId(depName, dependency.Version)
if children, ok := result[parent]; ok {
result[parent] = appendUniqueChild(children, dependencyId)
} else {
result[parent] = []string{dependencyId}
}
appendTransitiveDependencies(dependencyId, dependency.Dependencies, result)
}
}

func appendUniqueChild(children []string, candidateDependency string) []string {
for _, existingChild := range children {
if existingChild == candidateDependency {
return children
}
}
return append(children, candidateDependency)
}
1 change: 1 addition & 0 deletions commands/audit/sca/pnpm/pnpm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package pnpm
3 changes: 3 additions & 0 deletions commands/audit/scarunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
_go "github.com/jfrog/jfrog-cli-security/commands/audit/sca/go"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/npm"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/nuget"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/pnpm"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/python"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/yarn"
"github.com/jfrog/jfrog-cli-security/scangraph"
Expand Down Expand Up @@ -205,6 +206,8 @@ func GetTechDependencyTree(params xrayutils.AuditParams, tech coreutils.Technolo
fullDependencyTrees, uniqueDeps, err = java.BuildDependencyTree(serverDetails, params.DepsRepo(), params.UseWrapper(), params.IsMavenDepTreeInstalled(), tech)
case coreutils.Npm:
fullDependencyTrees, uniqueDeps, err = npm.BuildDependencyTree(params)
case coreutils.Pnpm:
fullDependencyTrees, uniqueDeps, err = pnpm.BuildDependencyTree(params)
case coreutils.Yarn:
fullDependencyTrees, uniqueDeps, err = yarn.BuildDependencyTree(params)
case coreutils.Go:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ require (
gopkg.in/warnings.v0 v0.1.2 // indirect
)

// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev
replace github.com/jfrog/jfrog-cli-core/v2 => github.com/attiasas/jfrog-cli-core/v2 v2.0.0-20240131150727-f47214b2b342

// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go dev
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer5
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/attiasas/jfrog-cli-core/v2 v2.0.0-20240131150727-f47214b2b342 h1:HgTRNuOWxEXLZ7ydUeviOq5pVD033iNMgE4GNhPznZE=
github.com/attiasas/jfrog-cli-core/v2 v2.0.0-20240131150727-f47214b2b342/go.mod h1:RVn4pIkR5fPUnr8gFXt61ou3pCNrrDdRQUpcolP4lhw=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
Expand Down Expand Up @@ -93,8 +95,6 @@ github.com/jfrog/gofrog v1.5.1 h1:2AXL8hHu1jJFMIoCqTp2OyRUfEqEp4nC7J8fwn6KtwE=
github.com/jfrog/gofrog v1.5.1/go.mod h1:SZ1EPJUruxrVGndOzHd+LTiwWYKMlHqhKD+eu+v5Hqg=
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.47.12 h1:xsEVdzbdhNGkI8Ey4Othx5+zpgCMnT99Uy71LOn+Q7k=
github.com/jfrog/jfrog-cli-core/v2 v2.47.12/go.mod h1:RVn4pIkR5fPUnr8gFXt61ou3pCNrrDdRQUpcolP4lhw=
github.com/jfrog/jfrog-client-go v1.36.1 h1:22Ucy5XdEP1yHEjbN8zOt2dZys5rbwcwhC3l3pcOdf4=
github.com/jfrog/jfrog-client-go v1.36.1/go.mod h1:y1WF6eiZ7V2DortiwjpMEicEH6NIJH+hOXI5QI2W3NU=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
Expand Down

0 comments on commit 71bbec6

Please sign in to comment.