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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 71 additions & 26 deletions commands/audit/sca/swift/swift.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"github.com/jfrog/gofrog/datastructures"
version2 "github.com/jfrog/gofrog/version"
"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"
Expand All @@ -22,7 +23,12 @@ import (
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"
VersionForMainModule = "0.0.0"
VERSION_SYNTAX_TYPE_INDEX = 1
EXACT_VERSION_INDEX = 2
FROM_VERSION_INDEX = 3
START_RANGE_INDEX = 4
END_RANGE_INDEX = 5
)

type Dependencies struct {
Expand All @@ -36,7 +42,7 @@ func GetTechDependencyLocation(directDependencyName, directDependencyVersion str
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)
log.Warn("Cannot support other files besides Package.swift: %s", descriptorPath)
continue
}
data, err := os.ReadFile(descriptorPath)
Expand Down Expand Up @@ -90,42 +96,81 @@ func parseSwiftLine(line, directDependencyName, directDependencyVersion, descrip
return foundDependency, tempIndex, startLine, startCol
}

func handleNonRangeMatches(match, name, fixVersion string, index int, submatches []string) string {
log.Debug("Fixing dependency", name, "from version", submatches[index], "to", fixVersion)
return strings.Replace(match, submatches[index], fixVersion, 1)
}

func handleRangeMatches(match, name, fixVersion string, submatches []string) string {
startVersion := submatches[START_RANGE_INDEX]
endVersion := submatches[END_RANGE_INDEX]
if version2.NewVersion(fixVersion).Compare(startVersion) < 1 && version2.NewVersion(fixVersion).Compare(endVersion) == 1 {
// Replace the start of the range with `fixVersion`
log.Debug("Fixing dependency", name, "from start version", startVersion, "to", fixVersion)
return strings.Replace(match, startVersion, fixVersion, 1)
}
return match
}

func updateDependency(content, name, version, fixVersion string) string {
urlPattern := `(?:https://|http://|sso://)?` + regexp.QuoteMeta(strings.TrimSuffix(name, ".git")) + `(?:\.git)?`
pattern := `\.package\(url:\s*"` + urlPattern + `",\s*(exact:\s*"(` + version + `)"|from:\s*"(` + version + `)"|"([\d\.]+)"\.\.\s*<?\s*"([\d\.]+)")\)`
re := regexp.MustCompile(pattern)
result := re.ReplaceAllStringFunc(content, func(match string) string {
submatches := re.FindStringSubmatch(match)
if len(submatches) == 0 {
return match
}

// Handle exact match
if len(submatches) > VERSION_SYNTAX_TYPE_INDEX && strings.Contains(submatches[VERSION_SYNTAX_TYPE_INDEX], "exact") {
return handleNonRangeMatches(match, name, fixVersion, EXACT_VERSION_INDEX, submatches)
}

// Handle from match
if len(submatches) > VERSION_SYNTAX_TYPE_INDEX && strings.Contains(submatches[VERSION_SYNTAX_TYPE_INDEX], "from") {
return handleNonRangeMatches(match, name, fixVersion, FROM_VERSION_INDEX, submatches)
}

// Handle range case
if len(submatches) > 5 && submatches[START_RANGE_INDEX] != "" && submatches[END_RANGE_INDEX] != "" {
return handleRangeMatches(match, name, fixVersion, submatches)
}
return match
})
return result
}

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)
log.Warn("Cannot support other files besides Package.swift: ", descriptorPath)
continue
}
data, err := os.ReadFile(descriptorPath)
var newLines []string
if err != nil {
log.Warn("Error reading file: ", descriptorPath, err)
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
updatedContent := updateDependency(string(data), dependencyName, dependencyVersion, fixVersion)
if strings.Compare(string(data), updatedContent) != 0 {
if err = os.WriteFile(descriptorPath, []byte(updatedContent), 0644); err != nil {
return fmt.Errorf("failed to write file: %v", err)
}
// 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)
currentDir, err := coreutils.GetWorkingDirectory()
if err != nil {
return fmt.Errorf("could not run swift build due to %s", err)
}
}
output := strings.Join(newLines, "\n")
err = os.WriteFile(descriptorPath, []byte(output), 0644)
if err != nil {
return fmt.Errorf("failed to write file: %v", err)
_, exePath, err := getSwiftVersionAndExecPath()
if err != nil {
return fmt.Errorf("could not run swift build due to %s", err)
}
if _, err = runSwiftCmd(exePath, currentDir, []string{"build"}); err != nil {
return fmt.Errorf("could not run swift build due to %s", err)
}
} else {
log.Debug("No fixes were done in file", descriptorPath)
}
}
return nil
Expand Down
55 changes: 31 additions & 24 deletions commands/audit/sca/swift/swift_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@ func TestBuildSwiftDependencyList(t *testing.T) {
techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-atomics:1.2.0",
techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-collections:1.1.4",
techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-system:1.4.0",
techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-http-types:1.0.2",
techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-nio:2.76.1",
techutils.Swift.GetPackageTypeId() + 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, techutils.Swift.GetPackageTypeId()+packageInfo)
assert.Len(t, rootNode[0].Nodes, 9)
assert.Len(t, rootNode[0].Nodes, 11)

child1 := tests.GetAndAssertNode(t, rootNode[0].Nodes, "github.com/apple/swift-algorithms:1.2.0")
assert.Len(t, child1.Nodes, 1)
Expand All @@ -64,14 +66,14 @@ func TestGetTechDependencyLocation(t *testing.T) {
defer cleanUp()
currentDir, err := coreutils.GetWorkingDirectory()
assert.NoError(t, err)
locations, err := GetTechDependencyLocation("github.com/apple/swift-algorithms", "1.2.0", filepath.Join(currentDir, "Package.swift"))
locations, err := GetTechDependencyLocation("github.com/apple/swift-algorithms", "1.1.0", filepath.Join(currentDir, "Package.swift"))
assert.NoError(t, err)
assert.Len(t, locations, 1)
assert.Equal(t, *locations[0].PhysicalLocation.Region.StartLine, 10)
assert.Equal(t, *locations[0].PhysicalLocation.Region.StartColumn, 10)
assert.Equal(t, *locations[0].PhysicalLocation.Region.EndLine, 31)
assert.Equal(t, *locations[0].PhysicalLocation.Region.EndColumn, 80)
assert.Contains(t, *locations[0].PhysicalLocation.Region.Snippet.Text, "github.com/apple/swift-algorithms\", from: \"1.2.0\"")
assert.Contains(t, *locations[0].PhysicalLocation.Region.Snippet.Text, "github.com/apple/swift-algorithms\", from: \"1.1.0\"")
}

func TestSwiftLineParse(t *testing.T) {
Expand All @@ -90,26 +92,31 @@ func TestSwiftLineParseFoundOnlyDependencyName(t *testing.T) {
assert.Equal(t, startCol, 23)
}

func TestFixTechDependencySingleLocation(t *testing.T) {
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "swift"))
defer cleanUp()
currentDir, err := coreutils.GetWorkingDirectory()
assert.NoError(t, err)
err = FixTechDependency("github.com/apple/swift-nio-http2", "1.0.0", "1.0.1", filepath.Join(currentDir, "Package.swift"))
assert.NoError(t, err)
file, err := os.ReadFile(filepath.Join(currentDir, "Package.swift"))
assert.NoError(t, err)
assert.Contains(t, string(file), ".package(url: \"https://github.com/apple/swift-nio-http2\", \"1.0.1\"..<\"1.19.1\")")
}
func TestFixTechDependencySingleLocation_Range(t *testing.T) {
testCases := []struct {
testName string
dependencyName string
dependencyVersion string
fixVersion string
stringToFind string
}{
{testName: "TestSingleLocation_Range", dependencyName: "github.com/apple/swift-nio-http2", dependencyVersion: "1.8.2", fixVersion: "1.8.3", stringToFind: ".package(url: \"https://github.com/apple/swift-nio-http2\", \"1.8.3\"..<\"1.19.1\")"},
{testName: "TestSingleLocation_From", dependencyName: "github.com/apple/swift-algorithms", dependencyVersion: "1.1.0", fixVersion: "1.2.0", stringToFind: ".package(url: \"https://github.com/apple/swift-algorithms\", from: \"1.2.0\""},
{testName: "TestSingleLocation_Exact", dependencyName: "github.com/apple/swift-http-types", dependencyVersion: "1.0.2", fixVersion: "1.0.3", stringToFind: ".package(url: \"https://github.com/apple/swift-http-types\", exact: \"1.0.3\""},
{testName: "TestNoLocations_FixOutOfRange", dependencyName: "github.com/apple/swift-nio-http2", dependencyVersion: "1.8.3", fixVersion: "1.19.2", stringToFind: ".package(url: \"https://github.com/apple/swift-nio-http2\", \"1.0.0\"..<\"1.19.1\")"},
}
for _, tc := range testCases {
t.Run(tc.testName, func(t *testing.T) {
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "swift"))
defer cleanUp()
currentDir, err := coreutils.GetWorkingDirectory()
assert.NoError(t, err)
err = FixTechDependency(tc.dependencyName, tc.dependencyVersion, tc.fixVersion, filepath.Join(currentDir, "Package.swift"))
assert.NoError(t, err)
file, err := os.ReadFile(filepath.Join(currentDir, "Package.swift"))
assert.NoError(t, err)
assert.Contains(t, string(file), tc.stringToFind)
})
}

func TestFixTechDependencyNoLocations(t *testing.T) {
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "swift"))
defer cleanUp()
currentDir, err := coreutils.GetWorkingDirectory()
assert.NoError(t, err)
err = FixTechDependency("github.com/apple/swift-nio-http2", "1.8.2", "1.8.3", filepath.Join(currentDir, "Package.swift"))
assert.NoError(t, err)
file, err := os.ReadFile(filepath.Join(currentDir, "Package.swift"))
assert.NoError(t, err)
assert.Contains(t, string(file), ".package(url: \"https://github.com/apple/swift-nio-http2\", \"1.0.0\"..<\"1.19.1\")")
}
3 changes: 2 additions & 1 deletion tests/testdata/projects/package-managers/swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ let package = Package(
.macOS(.v10_15),
],
dependencies: [
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-algorithms", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-nio-http2", "1.0.0"..<"1.19.1"),
.package(url: "https://github.com/apple/swift-http-types", exact: "1.0.2")
]
)
Loading