diff --git a/.github/actions/install-and-setup/action.yml b/.github/actions/install-and-setup/action.yml index 91dea5f5..cb447fd9 100644 --- a/.github/actions/install-and-setup/action.yml +++ b/.github/actions/install-and-setup/action.yml @@ -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'}} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fce6c3e1..85db85e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/audit_test.go b/audit_test.go index a37f6b96..a927acf4 100644 --- a/audit_test.go +++ b/audit_test.go @@ -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), "") @@ -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)) diff --git a/commands/audit/sca/cocoapods/podcommand.go b/commands/audit/sca/cocoapods/podcommand.go index 4a675d01..a11ed68c 100644 --- a/commands/audit/sca/cocoapods/podcommand.go +++ b/commands/audit/sca/cocoapods/podcommand.go @@ -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 } diff --git a/commands/audit/sca/swift/swift.go b/commands/audit/sca/swift/swift.go new file mode 100644 index 00000000..08cf1ee1 --- /dev/null +++ b/commands/audit/sca/swift/swift.go @@ -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) + } +} diff --git a/commands/audit/sca/swift/swift_test.go b/commands/audit/sca/swift/swift_test.go new file mode 100644 index 00000000..2eebeddb --- /dev/null +++ b/commands/audit/sca/swift/swift_test.go @@ -0,0 +1,115 @@ +package swift + +import ( + "fmt" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/owenrumney/go-sarif/v2/sarif" + "os" + "path/filepath" + "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 TestBuildSwiftDependencyList(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "swift")) + defer cleanUp() + // Run getModulesDependencyTrees + server := &config.ServerDetails{ + Url: "https://api.swift.here", + ArtifactoryUrl: "https://api.swift.here/artifactory", + User: "user", + AccessToken: "sdsdccs2232", + } + currentDir, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + packageName, err := GetMainPackageName(currentDir) + assert.NoError(t, err) + packageInfo := fmt.Sprintf("%s:%s", packageName, VersionForMainModule) + expectedUniqueDeps := []string{ + techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-algorithms:1.2.0", + techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-numerics:1.0.2", + techutils.Swift.GetPackageTypeId() + "github.com/apple/swift-nio-http2:1.19.0", + 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-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) + + child1 := tests.GetAndAssertNode(t, rootNode[0].Nodes, "github.com/apple/swift-algorithms:1.2.0") + assert.Len(t, child1.Nodes, 1) + + child2 := tests.GetAndAssertNode(t, rootNode[0].Nodes, "github.com/apple/swift-numerics:1.0.2") + assert.Len(t, child2.Nodes, 0) +} + +func TestGetTechDependencyLocation(t *testing.T) { + _, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "swift")) + 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")) + 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\"") +} + +func TestSwiftLineParse(t *testing.T) { + var swiftPositions []*sarif.Location + foundDependency, _, startLine, startCol := parseSwiftLine(".package(url: \"https://github.com/apple/swift-algorithms\", from: \"1.2.0\")", "github.com/apple/swift-algorithms", "1.2.0", "test", 0, 0, 0, 0, []string{".package(url: \"https://github.com/apple/swift-algorithms\", from: \"1.2.0\")"}, false, &swiftPositions) + assert.Equal(t, foundDependency, false) + assert.Equal(t, startLine, 0) + assert.Equal(t, startCol, 23) +} + +func TestSwiftLineParseFoundOnlyDependencyName(t *testing.T) { + var swiftPositions []*sarif.Location + foundDependency, _, startLine, startCol := parseSwiftLine(".package(url: \"https://github.com/apple/swift-algorithms\", from: \"1.2.0\")", "github.com/apple/swift-algorithms", "6.2.4", "test", 0, 0, 0, 0, []string{".package(url: \"https://github.com/apple/swift-algorithms\", from: \"1.2.0\")"}, false, &swiftPositions) + assert.Equal(t, foundDependency, true) + assert.Equal(t, startLine, 0) + 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 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\")") +} diff --git a/commands/audit/sca/swift/swiftcommand.go b/commands/audit/sca/swift/swiftcommand.go new file mode 100644 index 00000000..f9e0181b --- /dev/null +++ b/commands/audit/sca/swift/swiftcommand.go @@ -0,0 +1,73 @@ +package swift + +import ( + "bytes" + "fmt" + "github.com/jfrog/gofrog/version" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "os/exec" + "strings" +) + +const ( + minSupportedSwiftVersion = "5.7.0" +) + +type SwiftCommand struct { + cmdName string + swiftVersion *version.Version + executablePath string +} + +func getSwiftVersionAndExecPath() (*version.Version, string, error) { + swiftExecPath, err := exec.LookPath("swift") + if err != nil { + return nil, "", fmt.Errorf("could not find the 'swift' executable in the system PATH %w", err) + } + log.Debug("Using swift executable:", swiftExecPath) + versionData, err := runSwiftCmd(swiftExecPath, "", []string{"--version"}) + if err != nil { + return nil, "", err + } + return version.NewVersion(strings.TrimSpace(string(versionData))), swiftExecPath, nil +} + +func runSwiftCmd(executablePath, srcPath string, swiftArgs []string) (stdResult []byte, err error) { + args := make([]string, 0) + for i := 0; i < len(swiftArgs); i++ { + if strings.TrimSpace(swiftArgs[i]) != "" { + args = append(args, swiftArgs[i]) + } + } + log.Debug("Running 'swift " + strings.Join(swiftArgs, " ") + "' command.") + command := exec.Command(executablePath, args...) + command.Dir = srcPath + outBuffer := bytes.NewBuffer([]byte{}) + command.Stdout = outBuffer + errBuffer := bytes.NewBuffer([]byte{}) + command.Stderr = errBuffer + 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("swift '%s' standard output is:\n%s", strings.Join(args, " "), strings.TrimSpace(string(stdResult)))) + return +} + +func (sc *SwiftCommand) PreparePrerequisites() error { + log.Debug("Preparing prerequisites...") + var err error + sc.swiftVersion, sc.executablePath, err = getSwiftVersionAndExecPath() + if err != nil { + return err + } + if sc.swiftVersion.Compare(minSupportedSwiftVersion) > 0 { + return errorutils.CheckErrorf( + "JFrog CLI swift %s command requires swift client version %s or higher. The Current version is: %s", sc.cmdName, minSupportedSwiftVersion, sc.swiftVersion.GetVersion()) + } + return nil +} diff --git a/commands/audit/scarunner.go b/commands/audit/scarunner.go index 22c99b83..b24e36a1 100644 --- a/commands/audit/scarunner.go +++ b/commands/audit/scarunner.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jfrog/jfrog-cli-security/commands/audit/sca/swift" biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/build-info-go/utils/pythonutils" @@ -256,7 +257,19 @@ func GetTechDependencyTree(params xrayutils.AuditParams, artifactoryServerDetail case techutils.Nuget: depTreeResult.FullDepTrees, uniqueDeps, err = nuget.BuildDependencyTree(params) case techutils.Cocoapods: + xrayVersion := params.GetXrayVersion() + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, scangraph.CocoapodsScanMinXrayVersion) + if err != nil { + return depTreeResult, fmt.Errorf("your xray version %s does not allow cocoapods scanning", xrayVersion) + } depTreeResult.FullDepTrees, uniqueDeps, err = cocoapods.BuildDependencyTree(params) + case techutils.Swift: + xrayVersion := params.GetXrayVersion() + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, scangraph.SwiftScanMinXrayVersion) + if err != nil { + return depTreeResult, fmt.Errorf("your xray version %s does not allow swift scanning", xrayVersion) + } + depTreeResult.FullDepTrees, uniqueDeps, err = swift.BuildDependencyTree(params) default: err = errorutils.CheckErrorf("%s is currently not supported", string(tech)) } diff --git a/tests/config.go b/tests/config.go index e7534fe8..f1cc6af3 100644 --- a/tests/config.go +++ b/tests/config.go @@ -52,6 +52,7 @@ var ( TestAuditGo *bool TestAuditPython *bool TestAuditCocoapods *bool + TestAuditSwift *bool JfrogUrl *string JfrogUser *string @@ -104,6 +105,7 @@ func init() { TestAuditGo = flag.Bool("test.audit.Go", false, "Run Go technologies (GoLang) audit integration tests") TestAuditPython = flag.Bool("test.audit.Python", false, "Run Python technologies (Pip, PipEnv, Poetry) audit integration tests") TestAuditCocoapods = flag.Bool("test.audit.Cocoapods", false, "Run Cocoapods technologies audit integration tests") + TestAuditSwift = flag.Bool("test.audit.Swift", false, "Run Swift technologies audit integration tests") JfrogUrl = flag.String("jfrog.url", getTestUrlDefaultValue(), "JFrog platform url") JfrogUser = flag.String("jfrog.user", getTestUserDefaultValue(), "JFrog platform username") @@ -119,7 +121,7 @@ func init() { func InitTestFlags() { flag.Parse() // If no test types flags were set, run all types - shouldRunAllTests := !isAtLeastOneFlagSet(TestUnit, TestArtifactory, TestXray, TestXsc, TestAuditGeneral, TestAuditJas, TestAuditJavaScript, TestAuditJava, TestAuditCTypes, TestAuditGo, TestAuditPython, TestAuditCocoapods, TestScan, TestDockerScan, TestCuration, TestEnrich, TestGit) + shouldRunAllTests := !isAtLeastOneFlagSet(TestUnit, TestArtifactory, TestXray, TestXsc, TestAuditGeneral, TestAuditJas, TestAuditJavaScript, TestAuditJava, TestAuditCTypes, TestAuditGo, TestAuditPython, TestAuditCocoapods, TestAuditSwift, TestScan, TestDockerScan, TestCuration, TestEnrich, TestGit) if shouldRunAllTests { log.Info("Running all tests. To run only specific tests, please specify the desired test flags.") *TestUnit = true @@ -134,6 +136,7 @@ func InitTestFlags() { *TestAuditGo = true *TestAuditPython = true *TestAuditCocoapods = true + *TestAuditSwift = true *TestScan = true *TestDockerScan = true *TestCuration = true diff --git a/tests/testdata/projects/package-managers/swift/Package.resolved b/tests/testdata/projects/package-managers/swift/Package.resolved new file mode 100644 index 00000000..efa949ea --- /dev/null +++ b/tests/testdata/projects/package-managers/swift/Package.resolved @@ -0,0 +1,68 @@ +{ + "pins" : [ + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "914081701062b11e3bb9e21accc379822621995e", + "version" : "2.76.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2", + "state" : { + "revision" : "39ed0e753596afadad920e302ae769b28f3a982b", + "version" : "1.19.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", + "version" : "1.4.0" + } + } + ], + "version" : 2 +} diff --git a/tests/testdata/projects/package-managers/swift/Package.swift b/tests/testdata/projects/package-managers/swift/Package.swift new file mode 100644 index 00000000..76d87a90 --- /dev/null +++ b/tests/testdata/projects/package-managers/swift/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version:5.9 + +import PackageDescription + +let package = Package( + name: "test", + platforms: [ + .macOS(.v10_15), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-nio-http2", "1.0.0"..<"1.19.1"), + ] +) diff --git a/tests/utils/integration/test_integrationutils.go b/tests/utils/integration/test_integrationutils.go index e5a1e5bc..c95a3005 100644 --- a/tests/utils/integration/test_integrationutils.go +++ b/tests/utils/integration/test_integrationutils.go @@ -133,6 +133,13 @@ func InitAuditCocoapodsTest(t *testing.T, minVersion string) { testUtils.ValidateXrayVersion(t, minVersion) } +func InitAuditSwiftTest(t *testing.T, minVersion string) { + if !*configTests.TestAuditSwift { + t.Skip(getSkipTestMsg("Audit command Swift technologies integration", "--test.audit.Swift")) + } + testUtils.ValidateXrayVersion(t, minVersion) +} + func InitAuditPythonTest(t *testing.T, minVersion string) { if !*configTests.TestAuditPython { t.Skip(getSkipTestMsg("Audit command Python technologies (Pip, PipEnv, Poetry) integration", "--test.audit.Python")) diff --git a/utils/auditbasicparams.go b/utils/auditbasicparams.go index 57d11b78..ddc04d78 100644 --- a/utils/auditbasicparams.go +++ b/utils/auditbasicparams.go @@ -44,6 +44,7 @@ type AuditParams interface { IsRecursiveScan() bool SkipAutoInstall() bool AllowPartialResults() bool + GetXrayVersion() string } type AuditBasicParams struct { diff --git a/utils/techutils/techutils.go b/utils/techutils/techutils.go index 5ed8e76b..549df5c1 100644 --- a/utils/techutils/techutils.go +++ b/utils/techutils/techutils.go @@ -39,6 +39,7 @@ const ( Oci Technology = "oci" Conan Technology = "conan" Cocoapods Technology = "cocoapods" + Swift Technology = "swift" NoTech Technology = "" ) const Pypi = "pypi" @@ -56,6 +57,7 @@ const ( CPP CodeLanguage = "C++" // CocoapodsLang package can have multiple languages CocoapodsLang CodeLanguage = "Any" + SwiftLang CodeLanguage = "Any" ) // Associates a technology with project type (used in config commands for the package-managers). @@ -72,6 +74,7 @@ var TechToProjectType = map[Technology]project.ProjectType{ Nuget: project.Nuget, Dotnet: project.Dotnet, Cocoapods: project.Cocoapods, + Swift: project.Swift, } var packageTypes = map[string]string{ @@ -205,6 +208,12 @@ var technologiesData = map[Technology]TechData{ formal: "Cocoapods", packageTypeId: "cocoapods://", }, + Swift: { + indicators: []string{"Package.swift", "Package.resolved"}, + packageDescriptors: []string{"Package.swift", "Package.resolved"}, + formal: "Swift", + packageTypeId: "swift://", + }, } var ( @@ -246,6 +255,7 @@ func TechnologyToLanguage(technology Technology) CodeLanguage { Yarn: JavaScript, Pnpm: JavaScript, Cocoapods: CocoapodsLang, + Swift: SwiftLang, } return languageMap[technology] } diff --git a/utils/xray/scangraph/scangraph.go b/utils/xray/scangraph/scangraph.go index bc6944af..253c17dc 100644 --- a/utils/xray/scangraph/scangraph.go +++ b/utils/xray/scangraph/scangraph.go @@ -9,8 +9,10 @@ import ( ) const ( - GraphScanMinXrayVersion = "3.29.0" - ScanTypeMinXrayVersion = "3.37.2" + GraphScanMinXrayVersion = "3.29.0" + ScanTypeMinXrayVersion = "3.37.2" + SwiftScanMinXrayVersion = "3.109.4" + CocoapodsScanMinXrayVersion = "3.103.3" ) func RunScanGraphAndGetResults(params *ScanGraphParams, xrayManager *xray.XrayServicesManager) (*services.ScanResponse, error) {