Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cocoapods audit #196

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 275 additions & 0 deletions commands/audit/sca/cocoapods/cocoapods.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package cocoapods

import (
"errors"
"fmt"
"github.com/jfrog/gofrog/datastructures"
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/cocoapods"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-security/formats/sarifutils"
"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"
"github.com/owenrumney/go-sarif/v2/sarif"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)

const (
VersionForMainModule = "0.0.0"
)

var (
mainDepRegex = regexp.MustCompile(`- ([\w/+.\-]+) \(([\d.]+)\)`)
subDepRegex = regexp.MustCompile(`\s{2}- ([\w/+.\-]+)`)
versionRegex = regexp.MustCompile(`\((\d+(\.\d+){0,2})\)`)
)

func GetTechDependencyLocation(directDependencyName, directDependencyVersion string, descriptorPaths ...string) ([]*sarif.Location, error) {
var podPositions []*sarif.Location
for _, descriptorPath := range descriptorPaths {
path.Clean(descriptorPath)
if !strings.HasSuffix(descriptorPath, "Podfile") {
log.Logger.Warn("Cannot support other files besides Podfile: %s", descriptorPath)
continue
}
data, err := os.ReadFile(descriptorPath)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
var startLine, startCol, endLine, endCol int
foundDependency := false
var tempIndex int
for i, line := range lines {
if strings.Contains(line, directDependencyName) {
startLine = i
startCol = strings.Index(line, directDependencyName)
foundDependency = true
tempIndex = i
}
// This means we are in a new dependency (we cannot find dependency name and version together)
if i > tempIndex && foundDependency && strings.Contains(line, "pod") {
foundDependency = false
} else if foundDependency && strings.Contains(line, directDependencyVersion) {
endLine = i
endCol = len(line)
var snippet string
if endLine == startLine {
snippet = lines[startLine][startCol:endCol]
} else {
for snippetLine := 1; snippetLine < endLine-startLine+1; snippetLine++ {
switch snippetLine {
case 0:
snippet += "\n" + lines[snippetLine][startLine:]
case endLine - startLine:
snippet += "\n" + lines[snippetLine][:endCol]
default:
snippet += "\n" + lines[snippetLine]
}
}
}
podPositions = append(podPositions, sarifutils.CreateLocation(descriptorPath, startLine, endLine, startCol, endCol, snippet))
foundDependency = false
}
}
}
return podPositions, nil
}

func FixTechDependency(dependencyName, dependencyVersion, fixVersion string, descriptorPaths ...string) error {
for _, descriptorPath := range descriptorPaths {
path.Clean(descriptorPath)
if !strings.HasSuffix(descriptorPath, "Podfile") {
log.Logger.Warn("Cannot support other files besides Podfile: %s", descriptorPath)
continue
}
data, err := os.ReadFile(descriptorPath)
var newLines []string
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
foundDependency := false
var tempIndex int
for index, line := range lines {
if strings.Contains(line, dependencyName) {
foundDependency = true
tempIndex = index
}
// This means we are in a new dependency (we cannot find dependency name and version together)
if index > tempIndex && foundDependency && strings.Contains(line, "pod") {

Check failure on line 104 in commands/audit/sca/cocoapods/cocoapods.go

View workflow job for this annotation

GitHub Actions / Static-Check

ifElseChain: rewrite if-else to switch statement (gocritic)
foundDependency = false
} else if foundDependency && strings.Contains(line, dependencyVersion) {
newLine := strings.Replace(line, dependencyVersion, fixVersion, 1)
newLines = append(newLines, newLine)
foundDependency = false
} else {
newLines = append(newLines, line)
}
}
output := strings.Join(newLines, "\n")
err = os.WriteFile(descriptorPath, []byte(output), 0644)
if err != nil {
return fmt.Errorf("failed to write file: %v", err)
}
}
return nil
}

func GetPackageName(longPkgName string) string {
if strings.Contains(longPkgName, "/") {
splitNameParts := strings.Split(longPkgName, "/")
longPkgName = splitNameParts[0]
}
return longPkgName
}

func GetPodDependenciesGraph(data string) (map[string][]string, map[string]string) {
var currentMainDep string
lines := strings.Split(data, "\n")
dependencyMap := make(map[string][]string, len(lines))
versionMap := make(map[string]string, len(lines))
for _, line := range lines {
line = strings.ReplaceAll(line, "\"", "")
mainDepMatch := mainDepRegex.FindStringSubmatch(line)
if len(mainDepMatch) == 3 {
versionMatch := versionRegex.FindStringSubmatch(line)
currentMainDep = GetPackageName(mainDepMatch[1])
_, ok := dependencyMap[currentMainDep]
if !ok {
dependencyMap[currentMainDep] = []string{}
versionMap[currentMainDep] = versionMatch[1]
}
continue
}
subDepMatch := subDepRegex.FindStringSubmatch(line)
if len(subDepMatch) == 2 && currentMainDep != "" {
subDependency := subDepMatch[1]
if subDependency == GetPackageName(subDependency) {
dependencyMap[currentMainDep] = append(dependencyMap[currentMainDep], subDependency)
}
}
}
return dependencyMap, versionMap
}

func extractPodsSection(filePath string) (string, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
content := string(data)
startIndex := strings.Index(content, "PODS:")
if startIndex == -1 {
return "", fmt.Errorf("PODS: section not found")
}
subContent := content[startIndex:]
endIndex := strings.Index(subContent, "DEPENDENCIES:")
if endIndex == -1 {
endIndex = strings.Index(subContent, "SPEC REPOS:")
}
if endIndex != -1 {
subContent = subContent[:endIndex]
}
return subContent, nil
}

func GetDependenciesData(exePath, currentDir string) (string, error) {
_, _, err := cocoapods.RunPodCmd(exePath, currentDir, []string{"install"})
if err != nil {
return "", err
}
result, err := extractPodsSection(filepath.Join(currentDir, "Podfile.lock"))
if err != nil {
return "", err
}
return result, nil
}

func BuildDependencyTree(params utils.AuditParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) {
currentDir, err := coreutils.GetWorkingDirectory()
if err != nil {
return nil, nil, err
}

clearResolutionServerFunc, err := configPodResolutionServerIfNeeded(params)
if err != nil {
err = fmt.Errorf("failed while configuring a resolution server: %s", err.Error())
return nil, nil, err
}
defer func() {
if clearResolutionServerFunc != nil {
err = errors.Join(err, clearResolutionServerFunc())
}
}()

packageName := filepath.Base(currentDir)
packageInfo := fmt.Sprintf("%s:%s", packageName, VersionForMainModule)
_, podExecutablePath, err := cocoapods.GetPodVersionAndExecPath()
if err != nil {
err = fmt.Errorf("failed while retrieving pod path: %s", err.Error())
return
}
// Calculate pod dependencies
data, err := GetDependenciesData(podExecutablePath, currentDir)
if err != nil {
return nil, nil, err
}
uniqueDepsSet := datastructures.MakeSet[string]()
dependenciesGraph, versionMap := GetPodDependenciesGraph(data)
for key, _ := range dependenciesGraph {
if key != packageName {
dependenciesGraph[packageName] = append(dependenciesGraph[packageName], key)
}
}
versionMap[packageName] = VersionForMainModule
rootNode := &xrayUtils.GraphNode{
Id: utils.CocoapodsPackageTypeIdentifier + packageInfo,
Nodes: []*xrayUtils.GraphNode{},
}
// Parse the dependencies into Xray dependency tree format
parsePodDependenciesList(rootNode, dependenciesGraph, versionMap, uniqueDepsSet)
dependencyTree = []*xrayUtils.GraphNode{rootNode}
uniqueDeps = uniqueDepsSet.ToSlice()
return
}

// Generates a .netrc file to configure an Artifactory server as the resolver server.
func configPodResolutionServerIfNeeded(params utils.AuditParams) (clearResolutionServerFunc func() error, err error) {
// If we don't have an artifactory repo's name we don't need to configure any Artifactory server as resolution server
if params.DepsRepo() == "" {
return
}

serverDetails, err := params.ServerDetails()
if err != nil {
return
}

clearResolutionServerFunc, err = cocoapods.SetArtifactoryAsResolutionServer(serverDetails, params.DepsRepo())
return
}

// Parse the dependencies into an Xray dependency tree format
func parsePodDependenciesList(currNode *xrayUtils.GraphNode, dependenciesGraph map[string][]string, versionMap map[string]string, uniqueDepsSet *datastructures.Set[string]) {
if currNode.NodeHasLoop() {
return
}
uniqueDepsSet.Add(currNode.Id)
pkgName := strings.Split(strings.TrimPrefix(currNode.Id, utils.CocoapodsPackageTypeIdentifier), ":")[0]
currDepChildren := dependenciesGraph[pkgName]
for _, childName := range currDepChildren {
fullChildName := fmt.Sprintf("%s:%s", childName, versionMap[childName])
childNode := &xrayUtils.GraphNode{
Id: utils.CocoapodsPackageTypeIdentifier + fullChildName,
Nodes: []*xrayUtils.GraphNode{},
Parent: currNode,
}
currNode.Nodes = append(currNode.Nodes, childNode)
parsePodDependenciesList(childNode, dependenciesGraph, versionMap, uniqueDepsSet)
}
}
81 changes: 81 additions & 0 deletions commands/audit/sca/cocoapods/cocoapods_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cocoapods

import (
"fmt"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-core/v2/utils/tests"
"os"
"path/filepath"
"strings"
"testing"

"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca"
xrayutils "github.com/jfrog/jfrog-cli-security/utils"

"github.com/stretchr/testify/assert"
)

func TestBuildGoDependencyList(t *testing.T) {
// Create and change directory to test workspace
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "cocoapods"))
defer cleanUp()

// Run getModulesDependencyTrees
server := &config.ServerDetails{
Url: "https://api.cocoapods.here",
ArtifactoryUrl: "https://api.cocoapods.here/artifactory",
User: "user",
AccessToken: "sdsdccs2232",
}
currentDir, err := coreutils.GetWorkingDirectory()
assert.NoError(t, err)
packageName := filepath.Base(currentDir)
packageInfo := fmt.Sprintf("%s:%s", packageName, VersionForMainModule)
expectedUniqueDeps := []string{
xrayutils.CocoapodsPackageTypeIdentifier + "AppAuth:1.7.5",
xrayutils.CocoapodsPackageTypeIdentifier + "GoogleSignIn:6.2.4",
xrayutils.CocoapodsPackageTypeIdentifier + "GTMAppAuth:1.3.1",
xrayutils.CocoapodsPackageTypeIdentifier + "GTMSessionFetcher:2.3.0",
xrayutils.CocoapodsPackageTypeIdentifier + packageInfo,
}

auditBasicParams := (&xrayutils.AuditBasicParams{}).SetServerDetails(server)
rootNode, uniqueDeps, err := BuildDependencyTree(auditBasicParams)
assert.NoError(t, err)
assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected")
assert.NotEmpty(t, rootNode)

assert.Equal(t, rootNode[0].Id, xrayutils.CocoapodsPackageTypeIdentifier+packageInfo)
assert.Len(t, rootNode[0].Nodes, 4)

child1 := tests.GetAndAssertNode(t, rootNode[0].Nodes, "GTMSessionFetcher:2.3.0")
assert.Len(t, child1.Nodes, 0)

child2 := tests.GetAndAssertNode(t, rootNode[0].Nodes, "GoogleSignIn:6.2.4")
assert.Len(t, child2.Nodes, 2)
}

func TestGetTechDependencyLocation(t *testing.T) {
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "cocoapods"))
defer cleanUp()
currentDir, err := coreutils.GetWorkingDirectory()
assert.NoError(t, err)
locations, err := GetTechDependencyLocation("GoogleSignIn", "6.2.4", filepath.Join(currentDir, "Podfile"))
assert.NoError(t, err)
assert.Len(t, locations, 1)
assert.Equal(t, *locations[0].PhysicalLocation.Region.Snippet.Text, "GoogleSignIn', '~> 6.2.4'")
}

func TestFixTechDependency(t *testing.T) {
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "cocoapods"))
defer cleanUp()
currentDir, err := coreutils.GetWorkingDirectory()
assert.NoError(t, err)
err = FixTechDependency("GoogleSignIn", "6.2.4", "6.2.5", filepath.Join(currentDir, "Podfile"))
assert.NoError(t, err)
file, err := os.ReadFile(filepath.Join(currentDir, "Podfile"))
assert.NoError(t, err)
lines := strings.Split(string(file), "\n")
assert.Contains(t, lines, "pod 'GoogleSignIn', '~> 6.2.5'")
}
4 changes: 4 additions & 0 deletions commands/audit/scarunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/cocoapods"

"github.com/jfrog/build-info-go/utils/pythonutils"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/conan"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
Expand Down Expand Up @@ -266,6 +268,8 @@ func GetTechDependencyTree(params xrayutils.AuditParams, artifactoryServerDetail
})
case techutils.Nuget:
depTreeResult.FullDepTrees, uniqueDeps, err = nuget.BuildDependencyTree(params)
case techutils.Cocoapods:
depTreeResult.FullDepTrees, uniqueDeps, err = cocoapods.BuildDependencyTree(params)
default:
err = errorutils.CheckErrorf("%s is currently not supported", string(tech))
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/jfrog/gofrog v1.7.6
github.com/jfrog/jfrog-apps-config v1.0.1
github.com/jfrog/jfrog-cli-core/v2 v2.56.0
github.com/jfrog/jfrog-client-go v1.47.0
github.com/jfrog/jfrog-client-go v1.47.1
github.com/magiconair/properties v1.8.7
github.com/owenrumney/go-sarif/v2 v2.3.0
github.com/stretchr/testify v1.9.0
Expand Down Expand Up @@ -114,7 +114,7 @@ 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/barv-jfrog/jfrog-cli-core/v2 v2.0.0-20240930095207-bde88fbefc70

// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go dev

Expand Down
Loading
Loading