From 34c51b0258ce9158193eb5bd38ea0f232a515b17 Mon Sep 17 00:00:00 2001 From: barv Date: Thu, 4 Dec 2025 12:57:19 +0200 Subject: [PATCH 1/5] Malicious code scanner --- cli/docs/flags.go | 4 + cli/docs/maliciousscan/help.go | 13 + cli/scancommands.go | 47 +++ commands/maliciousscan/maliciousscan.go | 275 ++++++++++++++++++ jas/common.go | 1 + jas/maliciouscode/maliciouscodescanner.go | 149 ++++++++++ .../maliciouscodescanner_test.go | 147 ++++++++++ jas/runner/jasrunner.go | 1 - maliciousscan_test.go | 111 +++++++ .../malicious-scan/contain-malicious.sarif | 235 +++++++++++++++ .../other/malicious-scan/no-malicious.sarif | 38 +++ .../jas/jas/malicious/malicious1_v0.pkl | 8 + tests/validations/test_validation.go | 2 + utils/formats/conversion.go | 12 + utils/formats/simplejsonapi.go | 2 + utils/formats/summary.go | 9 +- utils/formats/table.go | 9 + utils/jasutils/jasutils.go | 5 +- utils/results/conversion/convertor.go | 7 +- .../cyclonedxparser/cyclonedxparser.go | 22 ++ .../conversion/sarifparser/sarifparser.go | 21 +- .../simplejsonparser/simplejsonparser.go | 15 + .../conversion/summaryparser/summaryparser.go | 15 + .../conversion/tableparser/tableparser.go | 5 + utils/results/output/resultwriter.go | 9 + utils/results/results.go | 23 +- utils/utils.go | 8 +- 27 files changed, 1178 insertions(+), 15 deletions(-) create mode 100644 cli/docs/maliciousscan/help.go create mode 100644 commands/maliciousscan/maliciousscan.go create mode 100644 jas/maliciouscode/maliciouscodescanner.go create mode 100644 jas/maliciouscode/maliciouscodescanner_test.go create mode 100644 maliciousscan_test.go create mode 100644 tests/testdata/other/malicious-scan/contain-malicious.sarif create mode 100644 tests/testdata/other/malicious-scan/no-malicious.sarif create mode 100644 tests/testdata/projects/jas/jas/malicious/malicious1_v0.pkl diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 9760a3941..935ea09d8 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -25,6 +25,7 @@ const ( GitCountContributors = "count-contributors" Enrich = "sbom-enrich" UploadCdx = "upload-cdx" + MaliciousScan = "malicious-scan" // TODO: Deprecated commands (remove at next CLI major version) AuditMvn = "audit-maven" @@ -175,6 +176,9 @@ var commandFlags = map[string][]string{ Enrich: { Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, }, + MaliciousScan: { + Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, OutputFormat, MinSeverity, AnalyzerManagerCustomPath, WorkingDirs, + }, BuildScan: { Url, XrayUrl, user, password, accessToken, ServerId, scanProjectKey, BuildVuln, OutputFormat, Fail, ExtendedTable, Rescan, InsecureTls, TriggerScanRetries, }, diff --git a/cli/docs/maliciousscan/help.go b/cli/docs/maliciousscan/help.go new file mode 100644 index 000000000..acc4d674d --- /dev/null +++ b/cli/docs/maliciousscan/help.go @@ -0,0 +1,13 @@ +package maliciousscan + +import ( + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" +) + +func GetDescription() string { + return "[Beta] Scan malicious models (pickle files, etc.) located in the working directory." +} + +func GetArguments() []components.Argument { + return []components.Argument{} +} diff --git a/cli/scancommands.go b/cli/scancommands.go index 0e134abdb..94ce5b27c 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -21,6 +21,7 @@ import ( flags "github.com/jfrog/jfrog-cli-security/cli/docs" auditSpecificDocs "github.com/jfrog/jfrog-cli-security/cli/docs/auditspecific" enrichDocs "github.com/jfrog/jfrog-cli-security/cli/docs/enrich" + maliciousScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/maliciousscan" mcpDocs "github.com/jfrog/jfrog-cli-security/cli/docs/mcp" auditDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/audit" buildScanDocs "github.com/jfrog/jfrog-cli-security/cli/docs/scan/buildscan" @@ -40,6 +41,7 @@ import ( "github.com/jfrog/jfrog-cli-security/commands/audit" "github.com/jfrog/jfrog-cli-security/commands/curation" + "github.com/jfrog/jfrog-cli-security/commands/maliciousscan" "github.com/jfrog/jfrog-cli-security/commands/scan" "github.com/jfrog/jfrog-cli-security/commands/upload" @@ -72,6 +74,15 @@ func getAuditAndScansCommands() []components.Command { Category: securityCategory, Action: EnrichCmd, }, + { + Name: "malicious-scan", + Aliases: []string{"ms"}, + Flags: flags.GetCommandFlags(flags.MaliciousScan), + Description: maliciousScanDocs.GetDescription(), + Arguments: maliciousScanDocs.GetArguments(), + Category: securityCategory, + Action: MaliciousScanCmd, + }, { Name: "build-scan", Aliases: []string{"bs"}, @@ -230,6 +241,42 @@ func EnrichCmd(c *components.Context) error { return commandsCommon.Exec(EnrichCmd) } +func MaliciousScanCmd(c *components.Context) error { + serverDetails, err := CreateServerDetailsFromFlags(c) + if err != nil { + return err + } + if err = validateConnectionInputs(serverDetails); err != nil { + return err + } + format, err := outputFormat.GetOutputFormat(c.GetStringFlagValue(flags.OutputFormat)) + if err != nil { + return err + } + threads, err := pluginsCommon.GetThreadsCount(c) + if err != nil { + return err + } + minSeverity, err := getMinimumSeverity(c) + if err != nil { + return err + } + workingDirs := []string{} + if c.GetStringFlagValue(flags.WorkingDirs) != "" { + workingDirs = splitByCommaAndTrim(c.GetStringFlagValue(flags.WorkingDirs)) + } + maliciousScanCmd := maliciousscan.NewMaliciousScanCommand(). + SetServerDetails(serverDetails). + SetWorkingDirs(workingDirs). + SetThreads(threads). + SetOutputFormat(format). + SetMinSeverityFilter(minSeverity) + if c.IsFlagSet(flags.AnalyzerManagerCustomPath) { + maliciousScanCmd.SetCustomAnalyzerManagerPath(c.GetStringFlagValue(flags.AnalyzerManagerCustomPath)) + } + return commandsCommon.Exec(maliciousScanCmd) +} + func ScanCmd(c *components.Context) error { if len(c.Arguments) == 0 && !c.IsFlagSet(flags.SpecFlag) { return pluginsCommon.PrintHelpAndReturnError("providing either a argument or the 'spec' option is mandatory", c) diff --git a/commands/maliciousscan/maliciousscan.go b/commands/maliciousscan/maliciousscan.go new file mode 100644 index 000000000..b728e4995 --- /dev/null +++ b/commands/maliciousscan/maliciousscan.go @@ -0,0 +1,275 @@ +package maliciousscan + +import ( + "errors" + "fmt" + "path/filepath" + "strings" + + "github.com/jfrog/jfrog-cli-core/v2/common/format" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-security/jas" + "github.com/jfrog/jfrog-cli-security/jas/maliciouscode" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/output" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-cli-security/utils/xray" + ioUtils "github.com/jfrog/jfrog-client-go/utils/io" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +type MaliciousScanCommand struct { + serverDetails *config.ServerDetails + workingDirs []string + threads int + outputFormat format.OutputFormat + minSeverityFilter severityutils.Severity + progress ioUtils.ProgressMgr + customAnalyzerManagerPath string +} + +func (cmd *MaliciousScanCommand) SetProgress(progress ioUtils.ProgressMgr) { + cmd.progress = progress +} + +func (cmd *MaliciousScanCommand) SetThreads(threads int) *MaliciousScanCommand { + cmd.threads = threads + return cmd +} + +func (cmd *MaliciousScanCommand) SetServerDetails(server *config.ServerDetails) *MaliciousScanCommand { + cmd.serverDetails = server + return cmd +} + +func (cmd *MaliciousScanCommand) SetWorkingDirs(workingDirs []string) *MaliciousScanCommand { + cmd.workingDirs = workingDirs + return cmd +} + +func (cmd *MaliciousScanCommand) SetOutputFormat(format format.OutputFormat) *MaliciousScanCommand { + cmd.outputFormat = format + return cmd +} + +func (cmd *MaliciousScanCommand) SetMinSeverityFilter(minSeverity severityutils.Severity) *MaliciousScanCommand { + cmd.minSeverityFilter = minSeverity + return cmd +} + +func (cmd *MaliciousScanCommand) SetCustomAnalyzerManagerPath(path string) *MaliciousScanCommand { + cmd.customAnalyzerManagerPath = path + return cmd +} + +func (cmd *MaliciousScanCommand) ServerDetails() (*config.ServerDetails, error) { + return cmd.serverDetails, nil +} + +func (cmd *MaliciousScanCommand) CommandName() string { + return "malicious_scan" +} + +func NewMaliciousScanCommand() *MaliciousScanCommand { + return &MaliciousScanCommand{} +} + +func (cmd *MaliciousScanCommand) Run() (err error) { + xrayVersion, entitledForJas, workingDirs, err := cmd.validateAndPrepare() + if err != nil { + return err + } + + cmdResults := cmd.initializeCommandResults(xrayVersion, entitledForJas) + populateScanTargets(cmdResults, workingDirs) + + scanner, err := cmd.createJasScanner() + if err != nil { + return err + } + + if err = cmd.runMaliciousScans(cmdResults, scanner); err != nil { + return err + } + + return cmd.outputResults(cmdResults) +} + +func (cmd *MaliciousScanCommand) validateAndPrepare() (xrayVersion string, entitledForJas bool, workingDirs []string, err error) { + xrayManager, xrayVersion, err := xray.CreateXrayServiceManagerAndGetVersion(cmd.serverDetails) + if err != nil { + return "", false, nil, err + } + + entitledForJas, err = jas.IsEntitledForJas(xrayManager, xrayVersion) + if err != nil { + return "", false, nil, err + } + if !entitledForJas { + return "", false, nil, errors.New("JAS (Advanced Security) feature is not entitled") + } + + log.Info("JFrog Xray version is:", xrayVersion) + + workingDirs, err = coreutils.GetFullPathsWorkingDirs(cmd.workingDirs) + if err != nil { + return "", false, nil, err + } + logScanPaths(workingDirs) + + return xrayVersion, entitledForJas, workingDirs, nil +} + +func (cmd *MaliciousScanCommand) initializeCommandResults(xrayVersion string, entitledForJas bool) *results.SecurityCommandResults { + cmdResults := results.NewCommandResults(utils.SourceCode) + cmdResults.SetXrayVersion(xrayVersion) + cmdResults.SetEntitledForJas(entitledForJas) + cmdResults.SetResultsContext(results.ResultContext{ + IncludeVulnerabilities: true, + }) + return cmdResults +} + +func (cmd *MaliciousScanCommand) createJasScanner() (*jas.JasScanner, error) { + scannerOptions := []jas.JasScannerOption{ + jas.WithEnvVars( + false, + jas.NotDiffScanEnvValue, + jas.GetAnalyzerManagerXscEnvVars( + "", + "", + "", + nil, + ), + ), + jas.WithMinSeverity(cmd.minSeverityFilter), + } + + scanner, err := jas.NewJasScanner(cmd.serverDetails, scannerOptions...) + if err != nil { + return nil, fmt.Errorf("failed to create JAS scanner: %w", err) + } + if scanner == nil { + return nil, errors.New("JAS scanner was not created") + } + + if err = cmd.setAnalyzerManagerPath(scanner); err != nil { + return nil, err + } + + log.Debug(fmt.Sprintf("Using analyzer manager executable at: %s", scanner.AnalyzerManager.AnalyzerManagerFullPath)) + return scanner, nil +} + +func (cmd *MaliciousScanCommand) setAnalyzerManagerPath(scanner *jas.JasScanner) error { + if cmd.customAnalyzerManagerPath == "" { + if err := jas.DownloadAnalyzerManagerIfNeeded(0); err != nil { + return fmt.Errorf("failed to download analyzer manager: %s", err.Error()) + } + var err error + if scanner.AnalyzerManager.AnalyzerManagerFullPath, err = jas.GetAnalyzerManagerExecutable(); err != nil { + return fmt.Errorf("failed to set analyzer manager executable path: %s", err.Error()) + } + } else { + scanner.AnalyzerManager.AnalyzerManagerFullPath = cmd.customAnalyzerManagerPath + log.Debug("using custom analyzer manager binary path") + } + return nil +} + +func (cmd *MaliciousScanCommand) runMaliciousScans(cmdResults *results.SecurityCommandResults, scanner *jas.JasScanner) error { + jasScanProducerConsumer := utils.NewSecurityParallelRunner(cmd.threads) + jasScanProducerConsumer.JasWg.Add(1) + createMaliciousScansTask := func(threadId int) (generalError error) { + defer func() { + jasScanProducerConsumer.JasWg.Done() + }() + for _, targetResult := range cmdResults.Targets { + vulnerabilitiesResults, err := maliciouscode.RunMaliciousScan( + scanner, + maliciouscode.MaliciousScannerType, + targetResult.Target, + len(cmdResults.Targets), + threadId, + ) + jasScanProducerConsumer.ResultsMu.Lock() + // Malicious code scans only return vulnerabilities, not violations + targetResult.AddJasScanResults(jasutils.MaliciousCode, vulnerabilitiesResults, nil, jas.GetAnalyzerManagerExitCode(err)) + jasScanProducerConsumer.ResultsMu.Unlock() + if err = jas.ParseAnalyzerManagerError(jasutils.MaliciousCode, err); err != nil { + _ = targetResult.AddTargetError(fmt.Errorf("failed to run malicious scan: %w", err), false) + } + } + return + } + + if _, addTaskErr := jasScanProducerConsumer.Runner.AddTaskWithError(createMaliciousScansTask, func(taskErr error) { + cmdResults.AddGeneralError(fmt.Errorf("failed while adding malicious scan tasks: %s", taskErr.Error()), false) + }); addTaskErr != nil { + return fmt.Errorf("failed to create malicious scan task: %w", addTaskErr) + } + + jasScanProducerConsumer.Start() + return nil +} + +func (cmd *MaliciousScanCommand) outputResults(cmdResults *results.SecurityCommandResults) error { + if err := output.NewResultsWriter(cmdResults). + SetOutputFormat(cmd.outputFormat). + SetPlatformUrl(cmd.serverDetails.Url). + SetPrintExtendedTable(false). + SetIsMultipleRootProject(cmdResults.HasMultipleTargets()). + SetSubScansPerformed([]utils.SubScanType{utils.MaliciousCodeScan}). + PrintScanResults(); err != nil { + return errors.Join(err, cmdResults.GetErrors()) + } + + if err := cmdResults.GetErrors(); err != nil { + return err + } + + log.Info("Malicious scan completed successfully.") + return nil +} + +func logScanPaths(workingDirs []string) { + if len(workingDirs) == 0 { + return + } + if len(workingDirs) == 1 { + log.Debug("Scanning path:", workingDirs[0]) + return + } + log.Debug("Scanning paths:", strings.Join(workingDirs, ", ")) +} + +func populateScanTargets(cmdResults *results.SecurityCommandResults, workingDirs []string) { + for _, requestedDirectory := range workingDirs { + if !fileutils.IsPathExists(requestedDirectory, false) { + log.Warn("The working directory", requestedDirectory, "doesn't exist. Skipping scan...") + continue + } + cmdResults.NewScanResults(results.ScanTarget{Target: requestedDirectory, Name: filepath.Base(requestedDirectory)}) + } + + if len(cmdResults.Targets) == 0 { + log.Warn("No scan targets were detected.") + return + } + + logScanTargetsInfo(cmdResults) +} + +func logScanTargetsInfo(cmdResults *results.SecurityCommandResults) { + if len(cmdResults.Targets) == 0 { + return + } + log.Info("Scanning", len(cmdResults.Targets), "target(s)...") + for _, targetResult := range cmdResults.Targets { + log.Info("Scanning target:", targetResult.Target) + } +} diff --git a/jas/common.go b/jas/common.go index b4152934d..974a4d044 100644 --- a/jas/common.go +++ b/jas/common.go @@ -60,6 +60,7 @@ type SpecificScannersExcludePatterns struct { SastExcludePatterns []string SecretsExcludePatterns []string IacExcludePatterns []string + MaliciousCodeExcludePatterns []string } type JasScannerOption func(f *JasScanner) error diff --git a/jas/maliciouscode/maliciouscodescanner.go b/jas/maliciouscode/maliciouscodescanner.go new file mode 100644 index 000000000..7f2414d0a --- /dev/null +++ b/jas/maliciouscode/maliciouscodescanner.go @@ -0,0 +1,149 @@ +package maliciouscode + +import ( + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/jfrog/jfrog-cli-security/jas" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" +) + +const ( + maliciousScanCommand = "mal" + malDocsUrlSuffix = "" + + MaliciousScannerType MaliciousScanType = "malicious-scan" // #nosec +) + +type MaliciousScanType string + +type MaliciousScanManager struct { + scanner *jas.JasScanner + scanType MaliciousScanType + + resultsToCompareFileName string + configFileName string + resultsFileName string +} + +func RunMaliciousScan(scanner *jas.JasScanner, scanType MaliciousScanType, sourceRoot string, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, err error) { + var scannerTempDir string + if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.MaliciousCode.String(), threadId); err != nil { + return + } + maliciousScanManager, err := newMaliciousScanManager(scanner, scanType, scannerTempDir, resultsToCompare...) + if err != nil { + return + } + startTime := time.Now() + logMsg := fmt.Sprintf("Running %s scan", utils.MaliciousCodeScan.ToTextString()) + if targetCount != 1 { + logMsg += fmt.Sprintf(" on target '%s'...", sourceRoot) + } else { + logMsg += "..." + } + log.Info(logMsg) + if vulnerabilitiesResults, err = maliciousScanManager.Run(sourceRoot); err != nil { + return + } + log.Info(utils.GetScanFindingsLog(utils.MaliciousCodeScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, threadId)) + return +} + +func newMaliciousScanManager(scanner *jas.JasScanner, scanType MaliciousScanType, scannerTempDir string, resultsToCompare ...*sarif.Run) (manager *MaliciousScanManager, err error) { + manager = &MaliciousScanManager{ + scanner: scanner, + scanType: scanType, + configFileName: filepath.Join(scannerTempDir, "config.yaml"), + resultsFileName: filepath.Join(scannerTempDir, "results.sarif"), + } + if len(resultsToCompare) == 0 { + return + } + log.Debug("Diff mode - Malicious code results to compare provided") + manager.resultsToCompareFileName = filepath.Join(scannerTempDir, "target.sarif") + if err = jas.SaveScanResultsToCompareAsReport(manager.resultsToCompareFileName, resultsToCompare...); err != nil { + return + } + return +} + +func (mal *MaliciousScanManager) Run(sourceRoot string) (vulnerabilitiesSarifRuns []*sarif.Run, err error) { + if err = mal.createConfigFile(sourceRoot, append(mal.scanner.Exclusions, mal.scanner.ScannersExclusions.MaliciousCodeExcludePatterns...)...); err != nil { + return + } + if err = mal.runAnalyzerManager(); err != nil { + return + } + // Malicious code scans only return vulnerabilities, not violations + vulnerabilitiesSarifRuns, _, err = jas.ReadJasScanRunsFromFile(mal.resultsFileName, sourceRoot, malDocsUrlSuffix, mal.scanner.MinSeverity) + return +} + +type maliciousScanConfig struct { + Scans []maliciousScanConfiguration `yaml:"scans"` +} + +type maliciousScanConfiguration struct { + Roots []string `yaml:"roots"` + Output string `yaml:"output"` + PathToResultsToCompare string `yaml:"target-result-file,omitempty"` + Type string `yaml:"type"` + SkippedDirs []string `yaml:"skipped-folders"` +} + +func (mal *MaliciousScanManager) createConfigFile(sourceRoot string, exclusions ...string) error { + root, err := filepath.Abs(sourceRoot) + if err != nil { + return err + } + roots := []string{root} + + // Process exclusions - convert to file exclude patterns if needed + excludePatterns := mal.getExcludePatterns(exclusions...) + + configFileContent := maliciousScanConfig{ + Scans: []maliciousScanConfiguration{ + { + Roots: roots, + Output: mal.resultsFileName, + PathToResultsToCompare: mal.resultsToCompareFileName, + Type: string(mal.scanType), + SkippedDirs: excludePatterns, + }, + }, + } + return jas.CreateScannersConfigFile(mal.configFileName, configFileContent, jasutils.MaliciousCode) +} + +func (mal *MaliciousScanManager) getExcludePatterns(exclusions ...string) []string { + if len(exclusions) == 0 { + return utils.DefaultJasExcludePatterns + } + // Convert exclusions to file exclude patterns + excludePatterns := make([]string, 0, len(exclusions)) + for _, exclusion := range exclusions { + pattern := exclusion + // Convert to file exclude pattern format if not already in that format + if !filepath.IsAbs(pattern) { + if !strings.HasPrefix(pattern, "**/") { + pattern = "**/" + pattern + } + if !strings.HasSuffix(pattern, "/**") { + pattern += "/**" + } + } + excludePatterns = append(excludePatterns, pattern) + } + return excludePatterns +} + +func (mal *MaliciousScanManager) runAnalyzerManager() error { + return mal.scanner.AnalyzerManager.Exec(mal.configFileName, maliciousScanCommand, filepath.Dir(mal.scanner.AnalyzerManager.AnalyzerManagerFullPath), mal.scanner.ServerDetails, mal.scanner.EnvVars) +} diff --git a/jas/maliciouscode/maliciouscodescanner_test.go b/jas/maliciouscode/maliciouscodescanner_test.go new file mode 100644 index 000000000..82bcc338c --- /dev/null +++ b/jas/maliciouscode/maliciouscodescanner_test.go @@ -0,0 +1,147 @@ +package maliciouscode + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/stretchr/testify/require" + + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli-security/jas" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/stretchr/testify/assert" +) + +func TestNewMaliciousScanManager(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + maliciousScanManager, err := newMaliciousScanManager(scanner, MaliciousScannerType, "tempDirPath") + require.NoError(t, err) + + assert.NotEmpty(t, maliciousScanManager) + assert.NotEmpty(t, maliciousScanManager.configFileName) + assert.NotEmpty(t, maliciousScanManager.resultsFileName) + assert.Equal(t, &jas.FakeServerDetails, maliciousScanManager.scanner.ServerDetails) + + assert.Empty(t, maliciousScanManager.resultsToCompareFileName) +} + +func TestNewMaliciousScanManagerWithFilesToCompare(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + tempDir, cleanUpTempDir := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer cleanUpTempDir() + + scanner.TempDir = tempDir + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.MaliciousCode.String(), 0) + require.NoError(t, err) + + maliciousScanManager, err := newMaliciousScanManager(scanner, MaliciousScannerType, scannerTempDir, sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyResult("test-markdown", "test-msg", "test-rule-id", "note"))) + require.NoError(t, err) + + // Check if path value exists and file is created + assert.NotEmpty(t, maliciousScanManager.resultsToCompareFileName) + assert.True(t, fileutils.IsPathExists(maliciousScanManager.resultsToCompareFileName, false)) +} + +func TestMaliciousScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.MaliciousCode.String(), 0) + require.NoError(t, err) + maliciousScanManager, err := newMaliciousScanManager(scanner, MaliciousScannerType, scannerTempDir) + require.NoError(t, err) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = maliciousScanManager.createConfigFile(currWd) + assert.NoError(t, err) + + defer func() { + err = os.Remove(maliciousScanManager.configFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(maliciousScanManager.configFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(maliciousScanManager.configFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) +} + +func TestRunAnalyzerManager_ReturnsGeneralError(t *testing.T) { + defer func() { + os.Clearenv() + }() + + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + maliciousScanManager, err := newMaliciousScanManager(scanner, MaliciousScannerType, "tempDirPath") + require.NoError(t, err) + assert.Error(t, maliciousScanManager.runAnalyzerManager()) +} + +func TestParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + // Arrange + maliciousScanManager, err := newMaliciousScanManager(scanner, MaliciousScannerType, "tempDirPath") + require.NoError(t, err) + maliciousScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "malicious-scan", "no-malicious.sarif") + + // Act + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(maliciousScanManager.resultsFileName, currWd, malDocsUrlSuffix, scanner.MinSeverity) + + // Assert + if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { + assert.Len(t, vulnerabilitiesResults, 1) + assert.Empty(t, vulnerabilitiesResults[0].Results) + } +} + +func TestParseResults_ResultsContainMalicious(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + + maliciousScanManager, err := newMaliciousScanManager(scanner, MaliciousScannerType, "tempDirPath") + require.NoError(t, err) + maliciousScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "malicious-scan", "contain-malicious.sarif") + + // Act + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(maliciousScanManager.resultsFileName, currWd, malDocsUrlSuffix, severityutils.Medium) + + // Assert + if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { + assert.Len(t, vulnerabilitiesResults, 1) + assert.NotEmpty(t, vulnerabilitiesResults[0].Results) + // The SARIF file has 8 results, but one has level "note" which may be filtered by Medium severity + // So we check for at least 6 results (the ones that should pass Medium severity filter) + assert.GreaterOrEqual(t, len(vulnerabilitiesResults[0].Results), 6) + } + assert.NoError(t, err) +} + +func TestGetMaliciousScanResults_AnalyzerManagerReturnsError(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + vulnerabilitiesResults, err := RunMaliciousScan(scanner, MaliciousScannerType, currWd, 1, 0) + assert.Error(t, err) + // The error message format is "failed to run MaliciousCode scan" (no space) + assert.ErrorContains(t, jas.ParseAnalyzerManagerError(jasutils.MaliciousCode, err), "failed to run MaliciousCode scan") + assert.Nil(t, vulnerabilitiesResults) +} diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index 1766db4c2..c88692450 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -99,7 +99,6 @@ func addJasScanTaskForModuleIfNeeded(params JasRunnerParams, subScan utils.SubSc return } if params.ConfigProfile != nil { - // This code section is related to CentralizedConfig integration in CI Next. log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run %s scan...", params.ConfigProfile.ProfileName, jasType)) enabled := false switch jasType { diff --git a/maliciousscan_test.go b/maliciousscan_test.go new file mode 100644 index 000000000..2633b36ee --- /dev/null +++ b/maliciousscan_test.go @@ -0,0 +1,111 @@ +package main + +import ( + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/jfrog/jfrog-cli-core/v2/common/format" + securityTests "github.com/jfrog/jfrog-cli-security/tests" + securityTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils" + securityIntegrationTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils/integration" + "github.com/jfrog/jfrog-cli-security/tests/validations" +) + +type maliciousScanCommandTestParams struct { + WorkingDirsToScan []string + Format format.OutputFormat + Threads int + MinSeverity string +} + +func getMaliciousScanCmdArgs(params maliciousScanCommandTestParams) (args []string) { + args = []string{"malicious-scan"} + if len(params.WorkingDirsToScan) > 0 { + args = append(args, "--working-dirs="+strings.Join(params.WorkingDirsToScan, ",")) + } + if params.Format != "" { + args = append(args, "--format="+string(params.Format)) + } + if params.Threads > 0 { + args = append(args, "--threads="+strconv.Itoa(params.Threads)) + } + if params.MinSeverity != "" { + args = append(args, "--min-severity="+params.MinSeverity) + } + return args +} + +func runMaliciousScan(t *testing.T, params maliciousScanCommandTestParams) (string, error) { + cleanUp := securityIntegrationTestUtils.UseTestHomeWithDefaultXrayConfig(t) + defer cleanUp() + return securityTests.PlatformCli.RunCliCmdWithOutputs(t, getMaliciousScanCmdArgs(params)...) +} + +func TestMaliciousScan(t *testing.T) { + testCases := []struct { + name string + format format.OutputFormat + projectPath string + expectedIssues int + }{ + { + name: "Malicious scan with findings (Simple JSON)", + format: format.SimpleJson, + projectPath: filepath.Join("projects", "jas", "jas", "malicious"), + expectedIssues: 1, + }, + { + name: "Malicious scan without findings (Simple JSON)", + format: format.SimpleJson, + projectPath: filepath.Join("projects", "empty_project", "python_project_with_no_deps"), + expectedIssues: 0, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fullProjectPath := filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), tc.projectPath) + _, cleanUp := securityTestUtils.CreateTestProjectEnvAndChdir(t, fullProjectPath) + defer cleanUp() + + params := maliciousScanCommandTestParams{ + Format: tc.format, + } + output, err := runMaliciousScan(t, params) + assert.NoError(t, err) + + validationsParams := validations.ValidationParams{ + Vulnerabilities: &validations.VulnerabilityCount{ + ValidateScan: &validations.ScanCount{MaliciousCode: tc.expectedIssues}, + }, + } + if tc.expectedIssues == 0 { + validationsParams.ExactResultsMatch = true + } + validations.ValidateCommandOutput(t, output, tc.format, validationsParams) + }) + } +} + +func TestMaliciousScanWithWorkingDirs(t *testing.T) { + maliciousProjectPath := filepath.Join(filepath.FromSlash(securityTests.GetTestResourcesPath()), "projects", "jas", "jas", "malicious") + _, cleanUp := securityTestUtils.CreateTestProjectEnvAndChdir(t, maliciousProjectPath) + defer cleanUp() + + params := maliciousScanCommandTestParams{ + WorkingDirsToScan: []string{"."}, + Format: format.SimpleJson, + } + output, err := runMaliciousScan(t, params) + assert.NoError(t, err) + + validationsParams := validations.ValidationParams{ + Vulnerabilities: &validations.VulnerabilityCount{ + ValidateScan: &validations.ScanCount{MaliciousCode: 1}, + }, + } + validations.ValidateCommandOutput(t, output, format.SimpleJson, validationsParams) +} diff --git a/tests/testdata/other/malicious-scan/contain-malicious.sarif b/tests/testdata/other/malicious-scan/contain-malicious.sarif new file mode 100644 index 000000000..fb4350bda --- /dev/null +++ b/tests/testdata/other/malicious-scan/contain-malicious.sarif @@ -0,0 +1,235 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Malicious Code scanner", + "rules": [ + { + "id": "entropy", + "shortDescription": { + "text": "Scanner for entropy" + } + } + ], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./jas_scanner", + "scan", + "mal_config_example.yaml" + ], + "workingDirectory": { + "uri": "malicious_scanner" + } + } + ], + "results": [ + { + "message": { + "text": "Malicious files were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.nodejs/applicable_base64.js" + }, + "region": { + "endColumn": 118, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 18, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Malicious files were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.nodejs/applicable_base64.js.approval.json" + }, + "region": { + "endColumn": 195, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 95, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Malicious files were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.nodejs/applicable_hex.js" + }, + "region": { + "endColumn": 138, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 18, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Malicious files were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.nodejs/applicable_hex.js.approval.json" + }, + "region": { + "endColumn": 215, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 95, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Malicious files were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.python/applicable_base64.py" + }, + "region": { + "endColumn": 112, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 12, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Malicious files were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.python/applicable_base64.py.approval.json" + }, + "region": { + "endColumn": 191, + "endLine": 1, + "snippet": { + "text": "2VTHzn1mKZ/n9apD5P6nxsajSQh8QhmyyKvUIRoZWAHCB8lSbBm3YWx5nOdZ1zPEOaA0zIZy1eFgHgfB2HkfAdVrbQj19kagXDVe" + }, + "startColumn": 91, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Malicious files were found" + }, + "level": "note", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.python/applicable_hex.py" + }, + "region": { + "endColumn": 132, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 12, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy" + }, + { + "message": { + "text": "Malicious files were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file://malicious_scanner/tests/req.malicious.python/applicable_hex.py.approval.json" + }, + "region": { + "endColumn": 211, + "endLine": 1, + "snippet": { + "text": "0159392e31dc912156e1cc6eab32a3d7df7154aecdf2ffe7d66f10da0d5706f7d9ba3183a366389112819b728b20026d04a4f6304da649beefc7fe49" + }, + "startColumn": 91, + "startLine": 1 + } + } + } + ], + "ruleId": "entropy", + "suppressions": [ + { + "kind": "inSource" + } + ] + } + ] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} \ No newline at end of file diff --git a/tests/testdata/other/malicious-scan/no-malicious.sarif b/tests/testdata/other/malicious-scan/no-malicious.sarif new file mode 100644 index 000000000..e2f6c36da --- /dev/null +++ b/tests/testdata/other/malicious-scan/no-malicious.sarif @@ -0,0 +1,38 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Malicious Code scanner", + "rules": [ + { + "id": "entropy", + "shortDescription": { + "text": "Scanner for entropy" + } + } + ], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./jas_scanner", + "scan", + "mal_config_example.yaml" + ], + "workingDirectory": { + "uri": "malicious_scanner" + } + } + ], + "results": [] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} + + diff --git a/tests/testdata/projects/jas/jas/malicious/malicious1_v0.pkl b/tests/testdata/projects/jas/jas/malicious/malicious1_v0.pkl new file mode 100644 index 000000000..b7d3d3f26 --- /dev/null +++ b/tests/testdata/projects/jas/jas/malicious/malicious1_v0.pkl @@ -0,0 +1,8 @@ +c__builtin__ +eval +p0 +(Vprint('456') +p1 +tp2 +Rp3 +. \ No newline at end of file diff --git a/tests/validations/test_validation.go b/tests/validations/test_validation.go index c21f1f525..9c362a5c0 100644 --- a/tests/validations/test_validation.go +++ b/tests/validations/test_validation.go @@ -60,6 +60,8 @@ type ScanCount struct { Iac int // Expected number of Secrets issues Secrets int + // Expected number of Malicious Code issues + MaliciousCode int } type SbomCount struct { diff --git a/utils/formats/conversion.go b/utils/formats/conversion.go index 1484458a0..fa180a6b1 100644 --- a/utils/formats/conversion.go +++ b/utils/formats/conversion.go @@ -193,6 +193,18 @@ func ConvertToIacOrSastTableRow(rows []SourceCodeRow) (tableRows []iacOrSastTabl return } +func ConvertToMaliciousTableRow(rows []SourceCodeRow) (tableRows []maliciousTableRow) { + for i := range rows { + tableRows = append(tableRows, maliciousTableRow{ + severity: rows[i].Severity, + file: rows[i].File, + evidence: rows[i].Snippet, + finding: rows[i].Finding, + }) + } + return +} + func convertToComponentTableRow(rows []ComponentRow) (tableRows []directDependenciesTableRow) { for i := range rows { tableRows = append(tableRows, directDependenciesTableRow{ diff --git a/utils/formats/simplejsonapi.go b/utils/formats/simplejsonapi.go index fc79a45b8..18481f404 100644 --- a/utils/formats/simplejsonapi.go +++ b/utils/formats/simplejsonapi.go @@ -22,6 +22,7 @@ type SimpleJsonResults struct { SecretsViolations []SourceCodeRow `json:"secretsViolations"` IacsViolations []SourceCodeRow `json:"iacViolations"` SastViolations []SourceCodeRow `json:"sastViolations"` + MaliciousVulnerabilities []SourceCodeRow `json:"maliciousCode"` Errors []SimpleJsonError `json:"errors"` Statuses ScanStatus `json:"scansStatus"` MultiScanId string `json:"multiScanId,omitempty"` @@ -34,6 +35,7 @@ type ScanStatus struct { IacStatusCode *int `json:"iacScanStatusCode,omitempty"` SecretsStatusCode *int `json:"secretsScanStatusCode,omitempty"` ApplicabilityStatusCode *int `json:"ContextualAnalysisScanStatusCode,omitempty"` + MaliciousStatusCode *int `json:"MaliciousStatusCode,omitempty"` } type ViolationContext struct { diff --git a/utils/formats/summary.go b/utils/formats/summary.go index 3adb4fee6..505e4cff3 100644 --- a/utils/formats/summary.go +++ b/utils/formats/summary.go @@ -36,10 +36,11 @@ type ScanSummary struct { } type ScanResultSummary struct { - ScaResults *ScaScanResultSummary `json:"sca,omitempty"` - IacResults *ResultSummary `json:"iac,omitempty"` - SecretsResults *ResultSummary `json:"secrets,omitempty"` - SastResults *ResultSummary `json:"sast,omitempty"` + ScaResults *ScaScanResultSummary `json:"sca,omitempty"` + IacResults *ResultSummary `json:"iac,omitempty"` + SecretsResults *ResultSummary `json:"secrets,omitempty"` + SastResults *ResultSummary `json:"sast,omitempty"` + MaliciousResults *ResultSummary `json:"maliciousCode,omitempty"` } type ScanViolationsSummary struct { diff --git a/utils/formats/table.go b/utils/formats/table.go index af038aa97..b3ec7e57a 100644 --- a/utils/formats/table.go +++ b/utils/formats/table.go @@ -23,6 +23,8 @@ type ResultsTables struct { // Secrets SecretsVulnerabilitiesTable []secretsTableRow SecretsViolationsTable []secretsTableRow + // Malicious Code + MaliciousVulnerabilitiesTable []maliciousTableRow } // Used for vulnerabilities and security violations @@ -174,3 +176,10 @@ type iacOrSastTableRow struct { finding string `col-name:"Finding"` watch string `col-name:"Watch Name" omitempty:"true"` } + +type maliciousTableRow struct { + severity string `col-name:"Severity"` + file string `col-name:"File"` + evidence string `col-name:"Evidence"` + finding string `col-name:"Finding"` +} diff --git a/utils/jasutils/jasutils.go b/utils/jasutils/jasutils.go index 2f3d90f7f..621e9262a 100644 --- a/utils/jasutils/jasutils.go +++ b/utils/jasutils/jasutils.go @@ -21,6 +21,7 @@ const ( Secrets JasScanType = "Secrets" IaC JasScanType = "IaC" Sast JasScanType = "Sast" + MaliciousCode JasScanType = "MaliciousCode" ) const ( @@ -40,7 +41,7 @@ func (jst JasScanType) String() string { } func GetJasScanTypes() []JasScanType { - return []JasScanType{Applicability, Secrets, IaC, Sast} + return []JasScanType{Applicability, Secrets, IaC, Sast, MaliciousCode} } func (tvs TokenValidationStatus) String() string { return string(tvs) } @@ -97,6 +98,8 @@ func SubScanTypeToJasScanType(subScanType utils.SubScanType) JasScanType { return Secrets case utils.ContextualAnalysisScan: return Applicability + case utils.MaliciousCodeScan: + return MaliciousCode } return "" } diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go index 002e82262..a8c7df141 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -68,6 +68,7 @@ type ResultsStreamFormatParser[T interface{}] interface { ParseSecrets(secrets ...[]*sarif.Run) error ParseIacs(iacs ...[]*sarif.Run) error ParseSast(sast ...[]*sarif.Run) error + ParseMalicious(malicious ...[]*sarif.Run) error // Parse JFrog violations to the format if supported ParseViolations(violations violationutils.Violations) error // When done parsing the stream results, get the converted content @@ -206,7 +207,11 @@ func parseJasResults[T interface{}](params ResultConvertParams, parser ResultsSt return } // Parsing JAS SAST results - return parser.ParseSast(targetResults.JasResults.JasVulnerabilities.SastScanResults) + if err = parser.ParseSast(targetResults.JasResults.JasVulnerabilities.SastScanResults); err != nil { + return + } + // Parsing JAS Malicious Code results + return parser.ParseMalicious(targetResults.JasResults.JasVulnerabilities.MaliciousScanResults) } func parseViolations[T interface{}](parser ResultsStreamFormatParser[T], violations *violationutils.Violations) (err error) { diff --git a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go index 2c6861f69..6e2d2d069 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -223,6 +223,28 @@ func (cdc *CmdResultsCycloneDxConverter) ParseIacs(iacs ...[]*sarif.Run) (err er }) } +func (cdc *CmdResultsCycloneDxConverter) ParseMalicious(malicious ...[]*sarif.Run) (err error) { + if cdc.bom == nil { + return results.ErrResetConvertor + } + source := cdc.addJasService(malicious) + return results.ForEachJasIssue(results.CollectRuns(malicious...), cdc.entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (e error) { + affectedComponent := cdc.getOrCreateFileComponent(getRelativePath(location, cdc.currentTarget)) + // Create a new JAS vulnerability, add it to the BOM and return it + ratings := []cyclonedx.VulnerabilityRating{severityutils.CreateSeverityRating(severity, jasutils.Applicable, source)} + jasIssue := cdc.getOrCreateJasIssue(sarifutils.GetResultRuleId(result), sarifutils.GetRuleScannerId(rule), sarifutils.GetResultMsgText(result), sarifutils.GetRuleShortDescriptionText(rule), source, sarifutils.GetRuleCWE(rule), ratings) + // Add the location to the vulnerability + results.AddFileIssueAffects(jasIssue, *affectedComponent, cyclonedx.Property{ + Name: fmt.Sprintf( + jasIssueLocationPropertyTemplate, "malicious-code", affectedComponent.BOMRef, + sarifutils.GetLocationStartLine(location), sarifutils.GetLocationStartColumn(location), sarifutils.GetLocationEndLine(location), sarifutils.GetLocationEndColumn(location), + ), + Value: sarifutils.GetLocationSnippetText(location), + }) + return + }) +} + func (cdc *CmdResultsCycloneDxConverter) ParseSast(sast ...[]*sarif.Run) (err error) { if cdc.bom == nil { return results.ErrResetConvertor diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index 7e53d7c86..e577f4123 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -74,10 +74,11 @@ type CmdResultsSarifConverter struct { type currentTargetRuns struct { currentTarget results.ScanTarget // Current run cache information - scaCurrentRun *sarif.Run - secretsCurrentRun *sarif.Run - iacCurrentRun *sarif.Run - sastCurrentRun *sarif.Run + scaCurrentRun *sarif.Run + secretsCurrentRun *sarif.Run + iacCurrentRun *sarif.Run + sastCurrentRun *sarif.Run + maliciousCurrentRun *sarif.Run } // Parse parameters for the SCA result @@ -162,6 +163,10 @@ func (sc *CmdResultsSarifConverter) flush() { if sc.currentTargetConvertedRuns.sastCurrentRun != nil { sc.current.Runs = append(sc.current.Runs, sc.currentTargetConvertedRuns.sastCurrentRun) } + // Flush malicious if needed + if sc.currentTargetConvertedRuns.maliciousCurrentRun != nil { + sc.current.Runs = append(sc.current.Runs, sc.currentTargetConvertedRuns.maliciousCurrentRun) + } sc.currentTargetConvertedRuns = nil } @@ -448,6 +453,14 @@ func (sc *CmdResultsSarifConverter) ParseSast(sast ...[]*sarif.Run) (err error) return } +func (sc *CmdResultsSarifConverter) ParseMalicious(malicious ...[]*sarif.Run) (err error) { + if err = sc.validateBeforeParse(); err != nil || !sc.entitledForJas { + return + } + sc.currentTargetConvertedRuns.maliciousCurrentRun = combineJasRunsToCurrentRun(sc.currentTargetConvertedRuns.maliciousCurrentRun, patchSarifRuns(sc.getVulnerabilitiesConvertParams(utils.MaliciousCodeScan), results.CollectRuns(malicious...)...)...) + return +} + func (sc *CmdResultsSarifConverter) addResultsToCurrentRun(runType RunInJfrogReport, rules []*sarif.ReportingDescriptor, results ...*sarif.Result) { var currentRun *sarif.Run switch runType { diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index 536d4a942..1b5e673e4 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -380,6 +380,21 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSast(sast ...[]*sarif.Run) (err e return } +func (sjc *CmdResultsSimpleJsonConverter) ParseMalicious(malicious ...[]*sarif.Run) (err error) { + if !sjc.entitledForJas { + return + } + if sjc.current == nil { + return results.ErrResetConvertor + } + maliciousSimpleJson, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, results.CollectRuns(malicious...)...) + if err != nil || len(maliciousSimpleJson) == 0 { + return + } + sjc.current.MaliciousVulnerabilities = append(sjc.current.MaliciousVulnerabilities, maliciousSimpleJson...) + return +} + func PrepareSimpleJsonVulnerabilities(target results.ScanTarget, descriptors []string, scaResponse services.ScanResponse, pretty, entitledForJas bool, applicabilityRuns ...*sarif.Run) ([]formats.VulnerabilityOrViolationRow, error) { var vulnerabilitiesRows []formats.VulnerabilityOrViolationRow err := results.ForEachScanGraphVulnerability( diff --git a/utils/results/conversion/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go index c08dea9f0..0c7a547be 100644 --- a/utils/results/conversion/summaryparser/summaryparser.go +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -258,6 +258,19 @@ func (sc *CmdResultsSummaryConverter) ParseIacs(iacs ...[]*sarif.Run) (err error return results.ForEachJasIssue(results.CollectRuns(iacs...), sc.entitledForJas, sc.getJasHandler(jasutils.IaC)) } +func (sc *CmdResultsSummaryConverter) ParseMalicious(malicious ...[]*sarif.Run) (err error) { + if !sc.entitledForJas { + return + } + if sc.currentScan == nil { + return results.ErrResetConvertor + } + if sc.currentScan.Vulnerabilities.MaliciousResults == nil { + sc.currentScan.Vulnerabilities.MaliciousResults = &formats.ResultSummary{} + } + return results.ForEachJasIssue(results.CollectRuns(malicious...), sc.entitledForJas, sc.getJasHandler(jasutils.MaliciousCode)) +} + func (sc *CmdResultsSummaryConverter) ParseSast(sast ...[]*sarif.Run) (err error) { if !sc.entitledForJas || sc.currentScan.Vulnerabilities == nil { // JAS results are only supported as vulnerabilities for now @@ -288,6 +301,8 @@ func (sc *CmdResultsSummaryConverter) getJasHandler(scanType jasutils.JasScanTyp count = sc.currentScan.Vulnerabilities.IacResults case jasutils.Sast: count = sc.currentScan.Vulnerabilities.SastResults + case jasutils.MaliciousCode: + count = sc.currentScan.Vulnerabilities.MaliciousResults } countJasIssues(count, location, severity, resultStatus) return diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go index 64ae02934..0785c2d10 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -48,6 +48,7 @@ func (tc *CmdResultsTableConverter) Get() (formats.ResultsTables, error) { IacViolationsTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.IacsViolations), SastVulnerabilitiesTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.SastVulnerabilities), SastViolationsTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.SastViolations), + MaliciousVulnerabilitiesTable: formats.ConvertToMaliciousTableRow(simpleJsonFormat.MaliciousVulnerabilities), }, nil } @@ -91,6 +92,10 @@ func (tc *CmdResultsTableConverter) ParseSast(sast ...[]*sarif.Run) (err error) return tc.simpleJsonConvertor.ParseSast(sast...) } +func (tc *CmdResultsTableConverter) ParseMalicious(malicious ...[]*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseMalicious(malicious...) +} + func (tc *CmdResultsTableConverter) ParseSbom(sbom *cyclonedx.BOM) (err error) { if sbom == nil || sbom.Components == nil { return nil diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go index 2324c2b74..0f1c1b187 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -264,6 +264,9 @@ func (rw *ResultsWriter) printTables() (err error) { if err = rw.printJasTablesIfNeeded(tableContent, utils.SastScan, jasutils.Sast); err != nil { return } + if err = rw.printJasTablesIfNeeded(tableContent, utils.MaliciousCodeScan, jasutils.MaliciousCode); err != nil { + return + } if len(rw.tableNotes) > 0 { printMessages(rw.tableNotes) } @@ -412,6 +415,12 @@ func PrintJasTable(tables formats.ResultsTables, entitledForJas bool, scanType j return coreutils.PrintTable(tables.SastVulnerabilitiesTable, "Static Application Security Testing (SAST)", "✨ No Static Application Security Testing vulnerabilities were found ✨", false) } + case jasutils.MaliciousCode: + if !violations { + return coreutils.PrintTable(tables.MaliciousVulnerabilitiesTable, "Malicious Code Detection", + "✨ No Malicious Code vulnerabilities were found ✨", false) + } + } return nil } diff --git a/utils/results/results.go b/utils/results/results.go index 4e6baedce..9be9038bc 100644 --- a/utils/results/results.go +++ b/utils/results/results.go @@ -28,6 +28,7 @@ const ( CmdStepIaC = "IaC Scan" CmdStepSecrets = "Secret Detection Scan" CmdStepSast = "Static Application Security Testing (SAST)" + CmdStepMaliciousCode = "Malicious Code" CmdStepViolations = "Violations Reporting" ) @@ -98,6 +99,7 @@ type ResultsStatus struct { SecretsScanStatusCode *int `json:"secrets,omitempty"` IacScanStatusCode *int `json:"iac,omitempty"` SastScanStatusCode *int `json:"sast,omitempty"` + MaliciousScanStatusCode *int `json:"malicious_code,omitempty"` ViolationsStatusCode *int `json:"violations,omitempty"` } @@ -115,6 +117,8 @@ func (status *ResultsStatus) IsScanFailed(step SecurityCommandStep) bool { return isScanFailed(status.IacScanStatusCode) case CmdStepSast: return isScanFailed(status.SastScanStatusCode) + case CmdStepMaliciousCode: + return isScanFailed(status.MaliciousScanStatusCode) case CmdStepViolations: return isScanFailed(status.ViolationsStatusCode) } @@ -151,6 +155,10 @@ func (status *ResultsStatus) UpdateStatus(step SecurityCommandStep, statusCode * if shouldUpdateStatus(status.SastScanStatusCode, statusCode) { status.SastScanStatusCode = statusCode } + case CmdStepMaliciousCode: + if shouldUpdateStatus(status.MaliciousScanStatusCode, statusCode) { + status.MaliciousScanStatusCode = statusCode + } case CmdStepViolations: if shouldUpdateStatus(status.ViolationsStatusCode, statusCode) { status.ViolationsStatusCode = statusCode @@ -195,9 +203,10 @@ type JasScansResults struct { } type JasScanResults struct { - SecretsScanResults []*sarif.Run `json:"secrets,omitempty"` - IacScanResults []*sarif.Run `json:"iac,omitempty"` - SastScanResults []*sarif.Run `json:"sast,omitempty"` + SecretsScanResults []*sarif.Run `json:"secrets,omitempty"` + IacScanResults []*sarif.Run `json:"iac,omitempty"` + SastScanResults []*sarif.Run `json:"sast,omitempty"` + MaliciousScanResults []*sarif.Run `json:"malicious_code,omitempty"` } type ScanTarget struct { @@ -420,6 +429,7 @@ func (r *SecurityCommandResults) GetStatusCodes() ResultsStatus { status.UpdateStatus(CmdStepSecrets, targetResults.ResultsStatus.SecretsScanStatusCode) status.UpdateStatus(CmdStepIaC, targetResults.ResultsStatus.IacScanStatusCode) status.UpdateStatus(CmdStepSast, targetResults.ResultsStatus.SastScanStatusCode) + status.UpdateStatus(CmdStepMaliciousCode, targetResults.ResultsStatus.MaliciousScanStatusCode) status.UpdateStatus(CmdStepViolations, targetResults.ResultsStatus.ViolationsStatusCode) } return status @@ -581,6 +591,11 @@ func (sr *TargetResults) AddJasScanResults(scanType jasutils.JasScanType, vulner sr.JasResults.JasVulnerabilities.SastScanResults = append(sr.JasResults.JasVulnerabilities.SastScanResults, vulnerabilitiesRuns...) sr.JasResults.JasViolations.SastScanResults = append(sr.JasResults.JasViolations.SastScanResults, violationsRuns...) } + case jasutils.MaliciousCode: + sr.ResultsStatus.UpdateStatus(CmdStepMaliciousCode, &exitCode) + if sr.JasResults != nil { + sr.JasResults.JasVulnerabilities.MaliciousScanResults = append(sr.JasResults.JasVulnerabilities.MaliciousScanResults, vulnerabilitiesRuns...) + } } } @@ -662,6 +677,8 @@ func (jsr *JasScansResults) GetVulnerabilitiesResults(scanType jasutils.JasScanT return jsr.JasVulnerabilities.IacScanResults case jasutils.Sast: return jsr.JasVulnerabilities.SastScanResults + case jasutils.MaliciousCode: + return jsr.JasVulnerabilities.MaliciousScanResults } return } diff --git a/utils/utils.go b/utils/utils.go index 6c6e91405..5fbb757f9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -69,6 +69,7 @@ const ( SastScan SubScanType = "sast" SecretsScan SubScanType = "secrets" SecretTokenValidationScan SubScanType = "secrets_token_validation" + MaliciousCodeScan SubScanType = "malicious_code" ) var subScanTypeToText = map[SubScanType]string{ @@ -77,6 +78,7 @@ var subScanTypeToText = map[SubScanType]string{ IacScan: "IaC", SastScan: "SAST", SecretsScan: "Secrets", + MaliciousCodeScan: "Malicious Code", } func (subScan SubScanType) ToTextString() string { @@ -108,7 +110,7 @@ func (s CommandType) IsTargetBinary() bool { } func GetAllSupportedScans() []SubScanType { - return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan, SecretTokenValidationScan} + return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan, SecretTokenValidationScan, MaliciousCodeScan} } // IsScanRequested returns true if the scan is requested, otherwise false. If requestedScans is empty, all scans are considered requested. @@ -116,6 +118,10 @@ func IsScanRequested(cmdType CommandType, subScan SubScanType, requestedScans .. if cmdType.IsTargetBinary() && (subScan == IacScan || subScan == SastScan) { return false } + if subScan == MaliciousCodeScan { + // Scan not requested by default, needs to be specified directly to run it + return slices.Contains(requestedScans, subScan) + } return len(requestedScans) == 0 || slices.Contains(requestedScans, subScan) } From c0d1edda52ad7837b9fa7f2c5681b302c8b862b6 Mon Sep 17 00:00:00 2001 From: barv Date: Tue, 23 Dec 2025 12:09:18 +0200 Subject: [PATCH 2/5] Malicious code scanner --- cli/docs/flags.go | 2 +- cli/scancommands.go | 3 ++- commands/maliciousscan/maliciousscan.go | 10 ++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 935ea09d8..1d666b019 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -177,7 +177,7 @@ var commandFlags = map[string][]string{ Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, }, MaliciousScan: { - Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, OutputFormat, MinSeverity, AnalyzerManagerCustomPath, WorkingDirs, + Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, OutputFormat, MinSeverity, AnalyzerManagerCustomPath, WorkingDirs, scanProjectKey, }, BuildScan: { Url, XrayUrl, user, password, accessToken, ServerId, scanProjectKey, BuildVuln, OutputFormat, Fail, ExtendedTable, Rescan, InsecureTls, TriggerScanRetries, diff --git a/cli/scancommands.go b/cli/scancommands.go index 94ce5b27c..b32f45175 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -270,7 +270,8 @@ func MaliciousScanCmd(c *components.Context) error { SetWorkingDirs(workingDirs). SetThreads(threads). SetOutputFormat(format). - SetMinSeverityFilter(minSeverity) + SetMinSeverityFilter(minSeverity). + SetProject(getProject(c)) if c.IsFlagSet(flags.AnalyzerManagerCustomPath) { maliciousScanCmd.SetCustomAnalyzerManagerPath(c.GetStringFlagValue(flags.AnalyzerManagerCustomPath)) } diff --git a/commands/maliciousscan/maliciousscan.go b/commands/maliciousscan/maliciousscan.go index b728e4995..4b00d60ea 100644 --- a/commands/maliciousscan/maliciousscan.go +++ b/commands/maliciousscan/maliciousscan.go @@ -30,6 +30,7 @@ type MaliciousScanCommand struct { minSeverityFilter severityutils.Severity progress ioUtils.ProgressMgr customAnalyzerManagerPath string + project string } func (cmd *MaliciousScanCommand) SetProgress(progress ioUtils.ProgressMgr) { @@ -66,6 +67,11 @@ func (cmd *MaliciousScanCommand) SetCustomAnalyzerManagerPath(path string) *Mali return cmd } +func (cmd *MaliciousScanCommand) SetProject(project string) *MaliciousScanCommand { + cmd.project = project + return cmd +} + func (cmd *MaliciousScanCommand) ServerDetails() (*config.ServerDetails, error) { return cmd.serverDetails, nil } @@ -100,7 +106,7 @@ func (cmd *MaliciousScanCommand) Run() (err error) { } func (cmd *MaliciousScanCommand) validateAndPrepare() (xrayVersion string, entitledForJas bool, workingDirs []string, err error) { - xrayManager, xrayVersion, err := xray.CreateXrayServiceManagerAndGetVersion(cmd.serverDetails) + xrayManager, xrayVersion, err := xray.CreateXrayServiceManagerAndGetVersion(cmd.serverDetails, xray.WithScopedProjectKey(cmd.project)) if err != nil { return "", false, nil, err } @@ -142,7 +148,7 @@ func (cmd *MaliciousScanCommand) createJasScanner() (*jas.JasScanner, error) { jas.GetAnalyzerManagerXscEnvVars( "", "", - "", + cmd.project, nil, ), ), From ee688318ae77cbf2ea22695032d3dcaa0ec08fbf Mon Sep 17 00:00:00 2001 From: barv Date: Tue, 23 Dec 2025 13:36:06 +0200 Subject: [PATCH 3/5] Malicious code scanner --- cli/docs/flags.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 1d666b019..c4d2c7e30 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -130,6 +130,7 @@ const ( ScanVuln = scanPrefix + Vuln SecretValidation = "validate-secrets" StaticSca = "static-sca" + malProjectKey = Project scanProjectKey = scanPrefix + Project uploadProjectKey = UploadCdx + "-" + Project @@ -177,7 +178,7 @@ var commandFlags = map[string][]string{ Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, }, MaliciousScan: { - Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, OutputFormat, MinSeverity, AnalyzerManagerCustomPath, WorkingDirs, scanProjectKey, + Url, XrayUrl, user, password, accessToken, ServerId, Threads, InsecureTls, OutputFormat, MinSeverity, AnalyzerManagerCustomPath, WorkingDirs, malProjectKey, }, BuildScan: { Url, XrayUrl, user, password, accessToken, ServerId, scanProjectKey, BuildVuln, OutputFormat, Fail, ExtendedTable, Rescan, InsecureTls, TriggerScanRetries, @@ -260,6 +261,7 @@ var flagsMap = map[string]components.Flag{ scanRegexp: components.NewBoolFlag(RegexpFlag, "Set to true to use a regular expression instead of wildcards expression to collect files to scan."), scanAnt: components.NewBoolFlag(AntFlag, "Set to true to use an ant pattern instead of wildcards expression to collect files to scan."), scanProjectKey: components.NewStringFlag(Project, "JFrog project key, to enable Xray to determine security violations accordingly. The command accepts this option only if the --repo-path and --watches options are not provided. If none of the three options are provided, the command will show all known vulnerabilities."), + malProjectKey: components.NewStringFlag(Project, "JFrog project key"), uploadProjectKey: components.NewStringFlag(Project, "JFrog project key to upload the file to."), Watches: components.NewStringFlag(Watches, "Comma-separated list of Xray watches to determine violations. Supported violations are CVEs, operational risk, and Licenses. Incompatible with --project and --repo-path."), RepoPath: components.NewStringFlag(RepoPath, "Artifactory repository path, to enable Xray to determine violations accordingly. The command accepts this option only if the --project and --watches options are not provided. If none of the three options are provided, the command will show all known vulnerabilities."), From 3fae385d6fb12c6c1f1ebba1c0068f880cfc5ff1 Mon Sep 17 00:00:00 2001 From: barv Date: Tue, 23 Dec 2025 13:37:38 +0200 Subject: [PATCH 4/5] Malicious code scanner --- jas/analyzermanager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jas/analyzermanager.go b/jas/analyzermanager.go index d93c5ec18..f6ff0fe0e 100644 --- a/jas/analyzermanager.go +++ b/jas/analyzermanager.go @@ -23,7 +23,7 @@ import ( const ( ApplicabilityFeatureId = "contextual_analysis" AnalyzerManagerZipName = "analyzerManager.zip" - defaultAnalyzerManagerVersion = "1.27.0" + defaultAnalyzerManagerVersion = "1.28.0" analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" analyzerManagerDirName = "analyzerManager" analyzerManagerExecutableName = "analyzerManager" From 91e8aed24cd9635ef31bbca67cebe464c0300a0c Mon Sep 17 00:00:00 2001 From: barv Date: Tue, 23 Dec 2025 13:48:11 +0200 Subject: [PATCH 5/5] Malicious code scanner --- jas/analyzermanager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jas/analyzermanager.go b/jas/analyzermanager.go index f6ff0fe0e..d93c5ec18 100644 --- a/jas/analyzermanager.go +++ b/jas/analyzermanager.go @@ -23,7 +23,7 @@ import ( const ( ApplicabilityFeatureId = "contextual_analysis" AnalyzerManagerZipName = "analyzerManager.zip" - defaultAnalyzerManagerVersion = "1.28.0" + defaultAnalyzerManagerVersion = "1.27.0" analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" analyzerManagerDirName = "analyzerManager" analyzerManagerExecutableName = "analyzerManager"