Skip to content

Commit

Permalink
Swift support audit (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
barv-jfrog authored Dec 31, 2024
1 parent c238f29 commit 218edb5
Show file tree
Hide file tree
Showing 15 changed files with 602 additions and 6 deletions.
16 changes: 16 additions & 0 deletions .github/actions/install-and-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,19 @@ runs:
python -m pip install conan
conan profile detect
shell: ${{ runner.os == 'Windows' && 'powershell' || 'bash' }}

- name: Install Swift on Linux
uses: swift-actions/setup-swift@v2
if: ${{ runner.os == 'Linux'}}

- name: Install Swift on MacOS
run: brew install swift
shell: ${{ runner.os == 'macOS' && 'sh' || 'bash' || 'pwsh' }}
if: ${{ runner.os == 'macOS'}}

- name: Install Swift on Windows
uses: compnerd/gha-setup-swift@main
with:
branch: swift-6.0.2-release
tag: 6.0.2-RELEASE
if: ${{ runner.os == 'Windows'}}
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ jobs:
testFlags: '--test.audit.C'
- name: 'Cocoapods Suite'
testFlags: '--test.audit.Cocoapods'

- name: 'Swift Suite'
testFlags: '--test.audit.Swift'

steps:
# Prepare the environment
- name: Checkout code
Expand Down
18 changes: 17 additions & 1 deletion audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,13 +447,20 @@ func TestXrayAuditPipJson(t *testing.T) {
}

func TestXrayAuditCocoapods(t *testing.T) {
integration.InitAuditCocoapodsTest(t, scangraph.GraphScanMinXrayVersion)
integration.InitAuditCocoapodsTest(t, scangraph.CocoapodsScanMinXrayVersion)
output := testXrayAuditCocoapods(t, string(format.Json))
validations.VerifyJsonResults(t, output, validations.ValidationParams{
Vulnerabilities: 1,
})
}

func TestXrayAuditSwift(t *testing.T) {
output := testXrayAuditSwift(t, string(format.Json))
validations.VerifyJsonResults(t, output, validations.ValidationParams{
Vulnerabilities: 1,
})
}

func TestXrayAuditPipSimpleJson(t *testing.T) {
integration.InitAuditPythonTest(t, scangraph.GraphScanMinXrayVersion)
output := testXrayAuditPip(t, string(format.SimpleJson), "")
Expand Down Expand Up @@ -495,6 +502,15 @@ func testXrayAuditCocoapods(t *testing.T, format string) string {
return securityTests.PlatformCli.RunCliCmdWithOutput(t, args...)
}

func testXrayAuditSwift(t *testing.T, format string) string {
integration.InitAuditSwiftTest(t, scangraph.SwiftScanMinXrayVersion)
_, cleanUp := securityTestUtils.CreateTestProjectEnvAndChdir(t, filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), "projects", "package-managers", "swift"))
defer cleanUp()
// Add dummy descriptor file to check that we run only specific audit
args := []string{"audit", "--format=" + format}
return securityTests.PlatformCli.RunCliCmdWithOutput(t, args...)
}

func TestXrayAuditPipenvJson(t *testing.T) {
integration.InitAuditPythonTest(t, scangraph.GraphScanMinXrayVersion)
output := testXrayAuditPipenv(t, string(format.Json))
Expand Down
2 changes: 1 addition & 1 deletion commands/audit/sca/cocoapods/podcommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func runPodCmd(executablePath, srcPath string, podArgs []string) (stdResult []by
err = fmt.Errorf("error while running '%s %s': %s\n%s", executablePath, strings.Join(args, " "), err.Error(), strings.TrimSpace(string(errResult)))
return
}
log.Debug("npm '" + strings.Join(args, " ") + "' standard output is:\n" + strings.TrimSpace(string(stdResult)))
log.Debug(fmt.Sprintf("cocoapods '%s' standard output is:\n%s", strings.Join(args, " "), strings.TrimSpace(string(stdResult))))
return
}

Expand Down
256 changes: 256 additions & 0 deletions commands/audit/sca/swift/swift.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package swift

import (
"bufio"
"encoding/json"
"fmt"
"github.com/jfrog/gofrog/datastructures"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-cli-security/utils"
"github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils"
"github.com/jfrog/jfrog-cli-security/utils/techutils"
"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 - We don't have information in swift on the current package, or main module, we only have information on its
// dependencies.
VersionForMainModule = "0.0.0"
)

type Dependencies struct {
Name string `json:"url,omitempty"`
Version string `json:"version,omitempty"`
Dependencies []*Dependencies `json:"dependencies,omitempty"`
}

func GetTechDependencyLocation(directDependencyName, directDependencyVersion string, descriptorPaths ...string) ([]*sarif.Location, error) {
var swiftPositions []*sarif.Location
for _, descriptorPath := range descriptorPaths {
path.Clean(descriptorPath)
if !strings.HasSuffix(descriptorPath, "Package.swift") {
log.Logger.Warn("Cannot support other files besides Package.swift: %s", descriptorPath)
continue
}
data, err := os.ReadFile(descriptorPath)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
var startLine, startCol int
foundDependency := false
var tempIndex int
for i, line := range lines {
foundDependency, tempIndex, startLine, startCol = parseSwiftLine(line, directDependencyName, directDependencyVersion, descriptorPath, i, tempIndex, startLine, startCol, lines, foundDependency, &swiftPositions)
}
}
return swiftPositions, nil
}

func parseSwiftLine(line, directDependencyName, directDependencyVersion, descriptorPath string, i, tempIndex, startLine, startCol int, lines []string, foundDependency bool, swiftPositions *[]*sarif.Location) (bool, int, int, int) {
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, ".package") {
foundDependency = false
} else if foundDependency && strings.Contains(line, directDependencyVersion) {
endLine := i
endCol := strings.Index(line, directDependencyVersion) + len(directDependencyVersion) + 1
var snippet string
// if the tech dependency is a one-liner
if endLine == startLine {
snippet = lines[startLine][startCol:endCol]
// else it is more than one line, so we need to parse all lines
} else {
for snippetLine := 0; 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]
}
}
}
*swiftPositions = append(*swiftPositions, sarifutils.CreateLocation(descriptorPath, startLine, endLine, startCol, endCol, snippet))
foundDependency = false
}
return foundDependency, tempIndex, startLine, startCol
}

func FixTechDependency(dependencyName, dependencyVersion, fixVersion string, descriptorPaths ...string) error {
for _, descriptorPath := range descriptorPaths {
path.Clean(descriptorPath)
if !strings.HasSuffix(descriptorPath, "Package.swift") {
log.Logger.Warn("Cannot support other files besides Package.swift: %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)
//nolint:gocritic
if index > tempIndex && foundDependency && strings.Contains(line, ".package") {
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 extractNameFromSwiftRepo(name string) string {
name = strings.TrimSuffix(name, ".git")
name = strings.TrimPrefix(name, "https://")
name = strings.TrimPrefix(name, "http://")
name = strings.TrimPrefix(name, "sso://")
return name
}

func GetSwiftDependenciesGraph(data *Dependencies, dependencyMap map[string][]string, versionMap map[string]string) {
data.Name = extractNameFromSwiftRepo(data.Name)
_, ok := dependencyMap[data.Name]
if !ok {
dependencyMap[data.Name] = []string{}
versionMap[data.Name] = data.Version
}
for _, dependency := range data.Dependencies {
dependency.Name = extractNameFromSwiftRepo(dependency.Name)
dependencyMap[data.Name] = append(dependencyMap[data.Name], dependency.Name)
GetSwiftDependenciesGraph(dependency, dependencyMap, versionMap)
}
}

func GetDependenciesData(exePath, currentDir string) (*Dependencies, error) {
result, err := runSwiftCmd(exePath, currentDir, []string{"package", "show-dependencies", "--format", "json"})
if err != nil {
return nil, err
}
var data *Dependencies
err = json.Unmarshal(result, &data)
if err != nil {
return nil, err
}
return data, nil
}

func GetMainPackageName(currentDir string) (string, error) {
file, err := os.Open(path.Join(currentDir, "Package.swift"))
if err != nil {
fmt.Println("Error opening file:", err)
return "", err
}
defer file.Close()

re := regexp.MustCompile(`name:\s*"([^"]+)"`)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := re.FindStringSubmatch(line)
if len(matches) > 1 {
return matches[1], nil
}
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", nil
}

func BuildDependencyTree(params utils.AuditParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) {
currentDir, err := coreutils.GetWorkingDirectory()
if err != nil {
return nil, nil, err
}
packageName, err := GetMainPackageName(currentDir)
if err != nil {
log.Warn("Failed to get package name from Package.swift file")
packageName = filepath.Base(currentDir)
}

packageInfo := fmt.Sprintf("%s:%s", packageName, VersionForMainModule)
version, exePath, err := getSwiftVersionAndExecPath()
if err != nil {
err = fmt.Errorf("failed while retrieving swift path: %s", err.Error())
return
}
log.Debug("Swift version: %s", version.GetVersion())
// Calculate pod dependencies
data, err := GetDependenciesData(exePath, currentDir)
if err != nil {
return nil, nil, err
}
uniqueDepsSet := datastructures.MakeSet[string]()
dependencyMap := make(map[string][]string)
versionMap := make(map[string]string)
data.Name = packageName
data.Version = VersionForMainModule
GetSwiftDependenciesGraph(data, dependencyMap, versionMap)
for key := range dependencyMap {
if key != packageName {
dependencyMap[packageName] = append(dependencyMap[packageName], key)
}
}
versionMap[packageName] = VersionForMainModule
rootNode := &xrayUtils.GraphNode{
Id: techutils.Swift.GetPackageTypeId() + packageInfo,
Nodes: []*xrayUtils.GraphNode{},
}
// Parse the dependencies into Xray dependency tree format
parseSwiftDependenciesList(rootNode, dependencyMap, versionMap, uniqueDepsSet)
dependencyTree = []*xrayUtils.GraphNode{rootNode}
uniqueDeps = uniqueDepsSet.ToSlice()
return
}

// Parse the dependencies into a Xray dependency tree format
func parseSwiftDependenciesList(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, techutils.Swift.GetPackageTypeId()), ":")[0]
currDepChildren := dependenciesGraph[pkgName]
for _, childName := range currDepChildren {
fullChildName := fmt.Sprintf("%s:%s", childName, versionMap[childName])
childNode := &xrayUtils.GraphNode{
Id: techutils.Swift.GetPackageTypeId() + fullChildName,
Nodes: []*xrayUtils.GraphNode{},
Parent: currNode,
}
currNode.Nodes = append(currNode.Nodes, childNode)
parseSwiftDependenciesList(childNode, dependenciesGraph, versionMap, uniqueDepsSet)
}
}
Loading

0 comments on commit 218edb5

Please sign in to comment.