diff --git a/audit_test.go b/audit_test.go index 752dfafd..49e0df2a 100644 --- a/audit_test.go +++ b/audit_test.go @@ -415,9 +415,9 @@ func TestXrayAuditMultiProjects(t *testing.T) { output := securityTests.PlatformCli.WithoutCredentials().RunCliCmdWithOutput(t, "audit", "--format="+string(format.SimpleJson), workingDirsFlag) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 43}, + Total: &validations.TotalCount{Vulnerabilities: 44}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 27, Sast: 1, Iac: 9, Secrets: 6}, + ValidateScan: &validations.ScanCount{Sca: 27, Sast: 1, Iac: 9, Secrets: 6, Malicious: 1}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 3, NotCovered: 22, NotApplicable: 2}, }, }) diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 54798d29..093d5fdd 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -52,6 +52,7 @@ const ( Iac = "iac" Sast = "sast" Secrets = "secrets" + Malicious = "malicious-code" WithoutCA = "without-contextual-analysis" ) @@ -160,7 +161,7 @@ var commandFlags = map[string][]string{ url, xrayUrl, user, password, accessToken, ServerId, InsecureTls, Project, Watches, RepoPath, Sbom, Licenses, OutputFormat, ExcludeTestDeps, useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm, Pnpm, Yarn, Go, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, Threads, - Sca, Iac, Sast, Secrets, WithoutCA, ScanVuln, SecretValidation, OutputDir, SkipAutoInstall, AllowPartialResults, MaxTreeDepth, + Sca, Iac, Sast, Secrets, Malicious, WithoutCA, ScanVuln, SecretValidation, OutputDir, SkipAutoInstall, AllowPartialResults, MaxTreeDepth, }, GitAudit: { // Connection params @@ -276,13 +277,13 @@ var flagsMap = map[string]components.Flag{ ), RequirementsFile: components.NewStringFlag(RequirementsFile, "[Pip] Defines pip requirements file name. For example: 'requirements.txt'."), CurationOutput: components.NewStringFlag(OutputFormat, "Defines the output format of the command. Acceptable values are: table, json.", components.WithStrDefaultValue("table")), - Sca: components.NewBoolFlag(Sca, fmt.Sprintf("Selective scanners mode: Execute SCA (Software Composition Analysis) sub-scan. Use --%s to run both SCA and Contextual Analysis. Use --%s --%s to to run SCA. Can be combined with --%s, --%s, --%s.", Sca, Sca, WithoutCA, Secrets, Sast, Iac)), - Iac: components.NewBoolFlag(Iac, fmt.Sprintf("Selective scanners mode: Execute IaC sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Secrets, Sast)), - Sast: components.NewBoolFlag(Sast, fmt.Sprintf("Selective scanners mode: Execute SAST sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Secrets, Iac)), + Sca: components.NewBoolFlag(Sca, fmt.Sprintf("Selective scanners mode: Execute SCA (Software Composition Analysis) sub-scan. Use --%s to run both SCA and Contextual Analysis. Use --%s --%s to to run SCA. Can be combined with --%s, --%s, --%s, --%s.", Sca, Sca, WithoutCA, Secrets, Malicious, Sast, Iac)), + Iac: components.NewBoolFlag(Iac, fmt.Sprintf("Selective scanners mode: Execute IaC sub-scan. Can be combined with --%s, --%s, --%s and --%s.", Sca, Secrets, Malicious, Sast)), + Sast: components.NewBoolFlag(Sast, fmt.Sprintf("Selective scanners mode: Execute SAST sub-scan. Can be combined with --%s, --%s, --%s and --%s.", Sca, Secrets, Malicious, Iac)), Secrets: components.NewBoolFlag(Secrets, fmt.Sprintf("Selective scanners mode: Execute Secrets sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Sast, Iac)), WithoutCA: components.NewBoolFlag(WithoutCA, fmt.Sprintf("Selective scanners mode: Disable Contextual Analysis scanner after SCA. Relevant only with --%s flag.", Sca)), SecretValidation: components.NewBoolFlag(SecretValidation, fmt.Sprintf("Selective scanners mode: Triggers token validation on found secrets. Relevant only with --%s flag.", Secrets)), - + Malicious: components.NewBoolFlag(Malicious, fmt.Sprintf("Selective scanners mode: Executes Malicious code sub scan. Relevant only with --%s flag.", Malicious)), // Git flags InputFile: components.NewStringFlag(InputFile, "Path to an input file in YAML format contains multiple git providers. With this option, all other scm flags will be ignored and only git servers mentioned in the file will be examined.."), ScmType: components.NewStringFlag(ScmType, fmt.Sprintf("SCM type. Possible values are: %s.", contributors.NewScmType().GetValidScmTypeString()), components.SetMandatory()), diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 45150f37..2dbbdd54 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -3,6 +3,7 @@ package audit import ( "errors" "fmt" + "github.com/jfrog/jfrog-cli-security/jas/maliciouscode" "strings" "github.com/jfrog/gofrog/parallel" @@ -203,7 +204,7 @@ func (auditCmd *AuditCommand) Run() (err error) { func (auditCmd *AuditCommand) getResultWriter(cmdResults *results.SecurityCommandResults) *output.ResultsWriter { var messages []string if !cmdResults.EntitledForJas { - messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'IaC Scan' and ‘SAST’.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink(utils.JasInfoURL)} + messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'Malicious Code Scan', 'IaC Scan' and ‘SAST’.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink(utils.JasInfoURL)} } return output.NewResultsWriter(cmdResults). SetOutputFormat(auditCmd.OutputFormat()). @@ -352,6 +353,7 @@ func createJasScansTasks(auditParallelRunner *utils.SecurityParallelRunner, scan ConfigProfile: auditParams.configProfile, ScansToPerform: auditParams.ScansToPerform(), SecretsScanType: secrets.SecretsScannerType, + MaliciousScanType: maliciouscode.MaliciousScannerType, DirectDependencies: auditParams.DirectDependencies(), ThirdPartyApplicabilityScan: auditParams.thirdPartyApplicabilityScan, ApplicableScanType: applicability.ApplicabilityScannerType, diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 5e2e5a3c..41360102 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -457,7 +457,7 @@ func TestAuditWithConfigProfile(t *testing.T) { // This test tests audit flow when providing --output-dir flag func TestAuditWithScansOutputDir(t *testing.T) { - mockServer, serverDetails := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion}) + mockServer, serverDetails := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.NewJasBinaryMinVersion}) defer mockServer.Close() outputDirPath, removeOutputDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) @@ -471,7 +471,7 @@ func TestAuditWithScansOutputDir(t *testing.T) { auditBasicParams := (&utils.AuditBasicParams{}). SetServerDetails(serverDetails). SetOutputFormat(format.Table). - SetXrayVersion(utils.EntitlementsMinVersion). + SetXrayVersion(utils.NewJasBinaryMinVersion). SetUseJas(true) auditParams := NewAuditParams(). @@ -487,13 +487,15 @@ func TestAuditWithScansOutputDir(t *testing.T) { filesList, err := fileutils.ListFiles(outputDirPath, false) assert.NoError(t, err) - assert.Len(t, filesList, 5) + assert.Len(t, filesList, 6) searchForStrWithSubString(t, filesList, "sca_results") searchForStrWithSubString(t, filesList, "iac_results") searchForStrWithSubString(t, filesList, "sast_results") searchForStrWithSubString(t, filesList, "secrets_results") searchForStrWithSubString(t, filesList, "applicability_results") + searchForStrWithSubString(t, filesList, "maliciouscode_results") + } func searchForStrWithSubString(t *testing.T, filesList []string, subString string) { diff --git a/go.mod b/go.mod index b0764dc8..c9a02a7d 100644 --- a/go.mod +++ b/go.mod @@ -111,7 +111,7 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect ) -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go dev +replace github.com/jfrog/jfrog-client-go => github.com/barv-jfrog/jfrog-client-go v0.0.0-20250227080554-3796e0126a92 // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev @@ -120,3 +120,5 @@ require ( // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go dev // replace github.com/jfrog/froggit-go => github.com/jfrog/froggit-go dev + +replace github.com/jfrog/jfrog-apps-config => github.com/barv-jfrog/jfrog-apps-config v0.0.0-20250128142442-6fd49006bb85 diff --git a/go.sum b/go.sum index d3b299ad..6a6ba944 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,10 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/barv-jfrog/jfrog-apps-config v0.0.0-20250128142442-6fd49006bb85 h1:kNdHaJdeD1QBQ0czgrfKqrcND3T+6dInSI8s23lW4Cw= +github.com/barv-jfrog/jfrog-apps-config v0.0.0-20250128142442-6fd49006bb85/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= +github.com/barv-jfrog/jfrog-client-go v0.0.0-20250227080554-3796e0126a92 h1:XueIjtY5Q95SuqHUNhRfDdrIKqCALzNAPaqz306vGzg= +github.com/barv-jfrog/jfrog-client-go v0.0.0-20250227080554-3796e0126a92/go.mod h1:2tQPwRhGS/F357BOKFfZrQbjd4XbzHPYUQm/OFNwLHg= github.com/beevik/etree v1.4.0 h1:oz1UedHRepuY3p4N5OjE0nK1WLCqtzHf25bxplKOHLs= github.com/beevik/etree v1.4.0/go.mod h1:cyWiXwGoasx60gHvtnEh5x8+uIjUVnjWqBvEnhnqKDA= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -126,14 +130,10 @@ github.com/jfrog/froggit-go v1.16.2 h1:F//S83iXH14qsCwYzv0zB2JtjS2pJVEsUoEmYA+37 github.com/jfrog/froggit-go v1.16.2/go.mod h1:5VpdQfAcbuyFl9x/x8HGm7kVk719kEtW/8YJFvKcHPA= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= -github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= -github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-artifactory v0.2.0 h1:4jEbIpJIeu8HsduZHr8L6e0bKQrhn6BLyq/aCRoKPQk= github.com/jfrog/jfrog-cli-artifactory v0.2.0/go.mod h1:U9gkQhxSPv6tXYEdj0kdsCrmFUjcvYmizrh+DztDxXc= github.com/jfrog/jfrog-cli-core/v2 v2.58.1 h1:ZktHuEVDBkM21JNp/0V3HGcMAMt7DLl1iQlbyBNKucE= github.com/jfrog/jfrog-cli-core/v2 v2.58.1/go.mod h1:75J6/Z5sMuRAloMAqJtMJIXqNTC1eFh/SulgLGm2fIY= -github.com/jfrog/jfrog-client-go v1.51.0 h1:O9sgpgEDBW9t05brGYwNR/NMqJ/e3WZY9G8Wge2xR+Q= -github.com/jfrog/jfrog-client-go v1.51.0/go.mod h1:2tQPwRhGS/F357BOKFfZrQbjd4XbzHPYUQm/OFNwLHg= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/jas/analyzermanager.go b/jas/analyzermanager.go index cd897b4a..7b916d51 100644 --- a/jas/analyzermanager.go +++ b/jas/analyzermanager.go @@ -24,7 +24,7 @@ import ( const ( ApplicabilityFeatureId = "contextual_analysis" AnalyzerManagerZipName = "analyzerManager.zip" - defaultAnalyzerManagerVersion = "1.14.1" + defaultAnalyzerManagerVersion = "1.15.0" analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" analyzerManagerDirName = "analyzerManager" analyzerManagerExecutableName = "analyzerManager" diff --git a/jas/maliciouscode/maliciouscodescanner.go b/jas/maliciouscode/maliciouscodescanner.go new file mode 100644 index 00000000..8169f354 --- /dev/null +++ b/jas/maliciouscode/maliciouscodescanner.go @@ -0,0 +1,99 @@ +package maliciouscode + +import ( + clientutils "github.com/jfrog/jfrog-client-go/utils" + "path/filepath" + + jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" + "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/v2/sarif" +) + +const ( + maliciousScanCommand = "mal" + maliciousDocsUrlSuffix = "malicious" + MaliciousScannerType MaliciousScanType = "malicious-scan" // #nosec +) + +type MaliciousScanType string + +type MaliciousScanManager struct { + scanner *jas.JasScanner + scanType MaliciousScanType + configFileName string + resultsFileName string +} + +// The getMaliciousScanResults function runs the malicious code scan flow, which includes the following steps: +// Creating an MaliciousSecretManager object. +// Running the analyzer manager executable. +// Parsing the analyzer manager results. +func RunMaliciousScan(scanner *jas.JasScanner, scanType MaliciousScanType, module jfrogappsconfig.Module, threadId int) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + var scannerTempDir string + if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.MaliciousCode.String()); err != nil { + return + } + maliciousScanManager := newMaliciousScanManager(scanner, scanType, scannerTempDir) + log.Info(clientutils.GetLogMsgPrefix(threadId, false) + "Running Malicious code scan...") + if vulnerabilitiesResults, violationsResults, err = maliciousScanManager.scanner.Run(maliciousScanManager, module); err != nil { + return + } + log.Info(utils.GetScanFindingsLog(utils.MaliciousCodeScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), sarifutils.GetResultsLocationCount(violationsResults...), threadId)) + return +} + +func newMaliciousScanManager(scanner *jas.JasScanner, scanType MaliciousScanType, scannerTempDir string) (manager *MaliciousScanManager) { + return &MaliciousScanManager{ + scanner: scanner, + scanType: scanType, + configFileName: filepath.Join(scannerTempDir, "config.yaml"), + resultsFileName: filepath.Join(scannerTempDir, "results.sarif"), + } +} + +func (msm *MaliciousScanManager) Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = msm.createConfigFile(module, msm.scanner.Exclusions...); err != nil { + return + } + if err = msm.runAnalyzerManager(); err != nil { + return + } + return jas.ReadJasScanRunsFromFile(msm.resultsFileName, module.SourceRoot, maliciousDocsUrlSuffix, msm.scanner.MinSeverity) +} + +type maliciousScanConfig struct { + Scans []maliciousScanConfiguration `yaml:"scans"` +} + +type maliciousScanConfiguration struct { + Roots []string `yaml:"roots"` + Output string `yaml:"output"` + Type string `yaml:"type"` + SkippedDirs []string `yaml:"skipped-folders"` +} + +func (m *MaliciousScanManager) createConfigFile(module jfrogappsconfig.Module, exclusions ...string) error { + roots, err := jas.GetSourceRoots(module, module.Scanners.MaliciousCode) + if err != nil { + return err + } + configFileContent := maliciousScanConfig{ + Scans: []maliciousScanConfiguration{ + { + Roots: roots, + Output: m.resultsFileName, + Type: string(m.scanType), + SkippedDirs: jas.GetExcludePatterns(module, module.Scanners.MaliciousCode, exclusions...), + }, + }, + } + return jas.CreateScannersConfigFile(m.configFileName, configFileContent, jasutils.MaliciousCode) +} + +func (m *MaliciousScanManager) runAnalyzerManager() error { + return m.scanner.AnalyzerManager.Exec(m.configFileName, maliciousScanCommand, filepath.Dir(m.scanner.AnalyzerManager.AnalyzerManagerFullPath), m.scanner.ServerDetails, m.scanner.EnvVars) +} diff --git a/jas/maliciouscode/maliciouscodescanner_test.go b/jas/maliciouscode/maliciouscodescanner_test.go new file mode 100644 index 00000000..2ab49328 --- /dev/null +++ b/jas/maliciouscode/maliciouscodescanner_test.go @@ -0,0 +1,118 @@ +package maliciouscode + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/stretchr/testify/require" + + jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" + "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 := newMaliciousScanManager(scanner, MaliciousScannerType, "temoDirPath") + + assert.NotEmpty(t, maliciousScanManager) + assert.NotEmpty(t, maliciousScanManager.configFileName) + assert.NotEmpty(t, maliciousScanManager.resultsFileName) + assert.Equal(t, &jas.FakeServerDetails, maliciousScanManager.scanner.ServerDetails) +} + +func TestMaliciousScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.MaliciousCode.String()) + require.NoError(t, err) + MaliciouscanManager := newMaliciousScanManager(scanner, MaliciousScannerType, scannerTempDir) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = MaliciouscanManager.createConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}) + assert.NoError(t, err) + + defer func() { + err = os.Remove(MaliciouscanManager.configFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(MaliciouscanManager.configFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(MaliciouscanManager.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() + + MaliciouscanManager := newMaliciousScanManager(scanner, MaliciousScannerType, "temoDirPath") + assert.Error(t, MaliciouscanManager.runAnalyzerManager()) +} + +func TestParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) + // Arrange + MaliciouscanManager := newMaliciousScanManager(scanner, MaliciousScannerType, "temoDirPath") + MaliciouscanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "malicious-scan", "no-malicious.sarif") + + // Act + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(MaliciouscanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, maliciousDocsUrlSuffix, 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() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) + + MaliciouscanManager := newMaliciousScanManager(scanner, MaliciousScannerType, "temoDirPath") + MaliciouscanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "malicious-scan", "contain-malicious.sarif") + + // Act + vulnerabilitiesResults, _, err := jas.ReadJasScanRunsFromFile(MaliciouscanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, maliciousDocsUrlSuffix, severityutils.Medium) + + // Assert + if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { + assert.Len(t, vulnerabilitiesResults, 1) + assert.NotEmpty(t, vulnerabilitiesResults[0].Results) + } + assert.NoError(t, err) + +} + +func TestGetMaliciousScanResults_AnalyzerManagerReturnsError(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) + vulnerabilitiesResults, _, err := RunMaliciousScan(scanner, MaliciousScannerType, jfrogAppsConfigForTest.Modules[0], 0) + assert.Error(t, err) + 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 534a2053..c5126360 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -3,6 +3,7 @@ package runner import ( "errors" "fmt" + "github.com/jfrog/jfrog-cli-security/jas/maliciouscode" "github.com/jfrog/gofrog/parallel" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" @@ -33,6 +34,8 @@ type JasRunnerParams struct { ScansToPerform []utils.SubScanType + // Malicious code scan flags + MaliciousScanType maliciouscode.MaliciousScanType // Secret scan flags SecretsScanType secrets.SecretsScanType // Contextual Analysis scan flags @@ -51,7 +54,7 @@ func AddJasScannersTasks(params JasRunnerParams) (generalError error) { if params.Scanner.AnalyzerManager.AnalyzerManagerFullPath, generalError = jas.GetAnalyzerManagerExecutable(); generalError != nil { return fmt.Errorf("failed to set analyzer manager executable path: %s", generalError.Error()) } - // For docker scan we support only secrets and contextual scans. + // For docker scan we support only secrets, malicious code, and contextual scans. runAllScanners := false if params.ApplicableScanType == applicability.ApplicabilityScannerType || params.SecretsScanType == secrets.SecretsScannerType { runAllScanners = true @@ -66,6 +69,11 @@ func AddJasScannersTasks(params JasRunnerParams) (generalError error) { if generalError = addJasScanTaskForModuleIfNeeded(params, utils.SecretsScan, runSecretsScan(params.Runner, params.Scanner, params.ScanResults.JasResults, params.Module, params.SecretsScanType, params.TargetOutputDir)); generalError != nil { return } + if params.MaliciousScanType != "" { + if generalError = addJasScanTaskForModuleIfNeeded(params, utils.MaliciousCodeScan, runMaliciousScan(params.Runner, params.Scanner, params.ScanResults.JasResults, params.Module, params.MaliciousScanType, params.TargetOutputDir)); generalError != nil { + return + } + } if !runAllScanners { return } @@ -102,6 +110,8 @@ func addJasScanTaskForModuleIfNeeded(params JasRunnerParams, subScan utils.SubSc enabled = params.ConfigProfile.Modules[0].ScanConfig.IacScannerConfig.EnableIacScan case jasutils.Applicability: enabled = params.ConfigProfile.Modules[0].ScanConfig.EnableContextualAnalysisScan + case jasutils.MaliciousCode: + enabled = params.ConfigProfile.Modules[0].ScanConfig.MaliciousScannerConfig.EnableMaliciousScan } if enabled { generalError = addModuleJasScanTask(jasType, params.Runner, task, params.ScanResults, params.AllowPartialResults) @@ -145,6 +155,24 @@ func runSecretsScan(securityParallelRunner *utils.SecurityParallelRunner, scanne } } +func runMaliciousScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *results.JasScansResults, + module jfrogappsconfig.Module, maliciousScanType maliciouscode.MaliciousScanType, scansOutputDir string) parallel.TaskFunc { + return func(threadId int) (err error) { + defer func() { + securityParallelRunner.JasScannersWg.Done() + }() + vulnerabilitiesResults, violationsResults, err := maliciouscode.RunMaliciousScan(scanner, maliciousScanType, module, threadId) + securityParallelRunner.ResultsMu.Lock() + defer securityParallelRunner.ResultsMu.Unlock() + // We first add the scan results and only then check for errors, so we can store the exit code in order to report it in the end + extendedScanResults.AddJasScanResults(jasutils.MaliciousCode, vulnerabilitiesResults, violationsResults, jas.GetAnalyzerManagerExitCode(err)) + if err = jas.ParseAnalyzerManagerError(jasutils.MaliciousCode, err); err != nil { + return fmt.Errorf("%s%s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) + } + return dumpSarifRunToFileIfNeeded(scansOutputDir, jasutils.MaliciousCode, vulnerabilitiesResults, violationsResults) + } +} + func runIacScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *results.JasScansResults, module jfrogappsconfig.Module, scansOutputDir string) parallel.TaskFunc { return func(threadId int) (err error) { 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 00000000..fb4350bd --- /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 00000000..ceeb4b29 --- /dev/null +++ b/tests/testdata/other/malicious-scan/no-malicious.sarif @@ -0,0 +1,29 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Malicious scanner", + "rules": [], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "mac_arm/secrets_scanner/secrets_scanner", + "scan", + "sec_config_example.yaml" + ], + "workingDirectory": { + "uri": "file:///am_versions_for_leap" + } + } + ], + "results": [] + } + ], + "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/projects/jas/jas/malicious/malicious1_v0.pkl b/tests/testdata/projects/jas/jas/malicious/malicious1_v0.pkl new file mode 100644 index 00000000..b7d3d3f2 --- /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/utils/formats/conversion.go b/utils/formats/conversion.go index 20cfb208..5828f567 100644 --- a/utils/formats/conversion.go +++ b/utils/formats/conversion.go @@ -179,6 +179,20 @@ func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow return } +func ConvertToMaliciousTableRow(rows []SourceCodeRow) (tableRows []maliciousTableRow) { + for i := range rows { + tableRows = append(tableRows, maliciousTableRow{ + severity: rows[i].Severity, + file: rows[i].File, + lineColumn: strconv.Itoa(rows[i].StartLine) + ":" + strconv.Itoa(rows[i].StartColumn), + evidence: rows[i].Snippet, + maliciousType: rows[i].Finding, + }) + + } + return +} + func ConvertToIacOrSastTableRow(rows []SourceCodeRow) (tableRows []iacOrSastTableRow) { for i := range rows { tableRows = append(tableRows, iacOrSastTableRow{ diff --git a/utils/formats/simplejsonapi.go b/utils/formats/simplejsonapi.go index 68d38f4e..058c2ff0 100644 --- a/utils/formats/simplejsonapi.go +++ b/utils/formats/simplejsonapi.go @@ -22,6 +22,8 @@ type SimpleJsonResults struct { SecretsViolations []SourceCodeRow `json:"secretsViolations"` IacsViolations []SourceCodeRow `json:"iacViolations"` SastViolations []SourceCodeRow `json:"sastViolations"` + MaliciousVulnerabilities []SourceCodeRow `json:"maliciousCode"` + MaliciousViolations []SourceCodeRow `json:"maliciousViolations"` Errors []SimpleJsonError `json:"errors"` Statuses ScanStatus `json:"scansStatus"` MultiScanId string `json:"multiScanId,omitempty"` @@ -34,6 +36,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 3adb4fee..f6df479e 100644 --- a/utils/formats/summary.go +++ b/utils/formats/summary.go @@ -9,6 +9,7 @@ const ( IacResult SummaryResultType = "IAC" SecretsResult SummaryResultType = "Secrets" SastResult SummaryResultType = "SAST" + MaliciousResult SummaryResultType = "MaliciousCode" ScaResult SummaryResultType = "SCA" ScaSecurityResult SummaryResultType = "Security" ScaLicenseResult SummaryResultType = "License" @@ -36,10 +37,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:"malicious_code,omitempty"` } type ScanViolationsSummary struct { @@ -184,6 +186,9 @@ func (srs *ScanResultSummary) GetTotal(filterTypes ...SummaryResultType) (total if srs.SastResults != nil && isFilterApply(SastResult, filterTypes) { total += srs.SastResults.GetTotal() } + if srs.MaliciousResults != nil && isFilterApply(MaliciousResult, filterTypes) { + total += srs.MaliciousResults.GetTotal() + } if srs.ScaResults == nil { return } @@ -229,6 +234,9 @@ func (ss *ScanResultSummary) GetSummaryBySeverity() (summary ResultSummary) { if ss.SastResults != nil { summary = MergeResultSummaries(summary, *ss.SastResults) } + if ss.MaliciousResults != nil { + summary = MergeResultSummaries(summary, *ss.MaliciousResults) + } return } @@ -306,6 +314,9 @@ func extractIssuesToSummary(issues *ScanResultSummary, destination *ScanResultSu if issues.SastResults != nil { destination.SastResults = mergeResultSummariesPointers(destination.SastResults, issues.SastResults) } + if issues.MaliciousResults != nil { + destination.MaliciousResults = mergeResultSummariesPointers(destination.MaliciousResults, issues.MaliciousResults) + } } func mergeResultSummariesPointers(summaries ...*ResultSummary) (merged *ResultSummary) { diff --git a/utils/formats/table.go b/utils/formats/table.go index cb3b82d8..856617be 100644 --- a/utils/formats/table.go +++ b/utils/formats/table.go @@ -23,6 +23,9 @@ type ResultsTables struct { // Secrets SecretsVulnerabilitiesTable []secretsTableRow SecretsViolationsTable []secretsTableRow + + MaliciousVulnerabilitiesTable []maliciousTableRow + MaliciousViolationsTable []maliciousTableRow } // Used for vulnerabilities and security violations @@ -166,6 +169,14 @@ type secretsTableRow struct { watch string `col-name:"Watch Name" omitempty:"true"` } +type maliciousTableRow struct { + severity string `col-name:"Severity"` + file string `col-name:"File"` + lineColumn string `col-name:"Line:Column"` + evidence string `col-name:"Evidence"` + maliciousType string `col-name:"Malicious Code Type"` +} + type iacOrSastTableRow struct { severity string `col-name:"Severity"` file string `col-name:"File"` diff --git a/utils/jasutils/jasutils.go b/utils/jasutils/jasutils.go index 3c2d11c8..425a722e 100644 --- a/utils/jasutils/jasutils.go +++ b/utils/jasutils/jasutils.go @@ -20,6 +20,7 @@ const ( Secrets JasScanType = "Secrets" IaC JasScanType = "IaC" Sast JasScanType = "Sast" + MaliciousCode JasScanType = "MaliciousCode" ) const ( @@ -39,7 +40,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) } @@ -96,6 +97,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 586209be..0182d1b1 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -59,6 +59,7 @@ type ResultsStreamFormatParser[T interface{}] interface { ParseSbom(target results.ScanTarget, sbom results.Sbom) error // Parse JAS content to the current scan target ParseSecrets(target results.ScanTarget, violations bool, secrets []results.ScanResult[[]*sarif.Run]) error + ParseMalicious(target results.ScanTarget, violations bool, maliciousFindings []results.ScanResult[[]*sarif.Run]) error ParseIacs(target results.ScanTarget, violations bool, iacs []results.ScanResult[[]*sarif.Run]) error ParseSast(target results.ScanTarget, violations bool, sast []results.ScanResult[[]*sarif.Run]) error // When done parsing the stream results, get the converted content @@ -198,6 +199,16 @@ func parseJasResults[T interface{}](params ResultConvertParams, parser ResultsSt }); err != nil { return } + // Parsing JAS Malicious code results + if err = parseJasScanResults(params, targetResults, cmdType, utils.MaliciousCodeScan, func(violations bool) error { + scanResults := targetResults.JasResults.JasVulnerabilities.MaliciousScanResults + if violations { + scanResults = targetResults.JasResults.JasViolations.MaliciousScanResults + } + return parser.ParseMalicious(targetResults.ScanTarget, violations, scanResults) + }); err != nil { + return + } // Parsing JAS SAST results return parseJasScanResults(params, targetResults, cmdType, utils.SastScan, func(violations bool) error { scanResults := targetResults.JasResults.JasVulnerabilities.SastScanResults diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index 26a55c54..7e5caf69 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -62,10 +62,11 @@ type CmdResultsSarifConverter struct { type currentTargetRuns struct { currentTarget results.ScanTarget // Current run cache information, we combine vulnerabilities and violations in the same run - 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 @@ -155,6 +156,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 } @@ -240,6 +245,14 @@ func (sc *CmdResultsSarifConverter) ParseSecrets(target results.ScanTarget, viol return } +func (sc *CmdResultsSarifConverter) ParseMalicious(target results.ScanTarget, violations bool, maliciousFindings []results.ScanResult[[]*sarif.Run]) (err error) { + if err = sc.validateBeforeParse(); err != nil || !sc.entitledForJas { + return + } + sc.currentTargetConvertedRuns.maliciousCurrentRun = combineJasRunsToCurrentRun(sc.currentTargetConvertedRuns.maliciousCurrentRun, patchRunsToPassIngestionRules(sc.baseJfrogUrl, sc.currentCmdType, utils.MaliciousCodeScan, sc.patchBinaryPaths, violations, target, results.ScanResultsToRuns(maliciousFindings)...)...) + return +} + func (sc *CmdResultsSarifConverter) ParseIacs(target results.ScanTarget, violations bool, iacs []results.ScanResult[[]*sarif.Run]) (err error) { if err = sc.validateBeforeParse(); err != nil || !sc.entitledForJas { return diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index 9223966b..293ee80d 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -162,6 +162,30 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSecrets(_ results.ScanTarget, isV return } +func (sjc *CmdResultsSimpleJsonConverter) ParseMalicious(_ results.ScanTarget, isViolationsResults bool, maliciousFindings []results.ScanResult[[]*sarif.Run]) (err error) { + if !sjc.entitledForJas { + return + } + if sjc.current == nil { + return results.ErrResetConvertor + } + for i := range maliciousFindings { + if shouldUpdateStatus(sjc.current.Statuses.MaliciousStatusCode, &maliciousFindings[i].StatusCode) { + sjc.current.Statuses.MaliciousStatusCode = &maliciousFindings[i].StatusCode + } + } + maliciousSimpleJson, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, results.ScanResultsToRuns(maliciousFindings)...) + if err != nil || len(maliciousSimpleJson) == 0 { + return + } + if isViolationsResults { + sjc.current.MaliciousViolations = append(sjc.current.MaliciousViolations, maliciousSimpleJson...) + } else { + sjc.current.MaliciousVulnerabilities = append(sjc.current.MaliciousVulnerabilities, maliciousSimpleJson...) + } + return +} + func (sjc *CmdResultsSimpleJsonConverter) ParseIacs(_ results.ScanTarget, isViolationsResults bool, iacs []results.ScanResult[[]*sarif.Run]) (err error) { if !sjc.entitledForJas { return diff --git a/utils/results/conversion/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go index aff94ff4..70cb6a97 100644 --- a/utils/results/conversion/summaryparser/summaryparser.go +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -279,6 +279,25 @@ func (sc *CmdResultsSummaryConverter) ParseSecrets(_ results.ScanTarget, isViola return results.ApplyHandlerToJasIssues(results.ScanResultsToRuns(secrets), sc.entitledForJas, sc.getJasHandler(jasutils.Secrets, isViolationsResults)) } +func (sc *CmdResultsSummaryConverter) ParseMalicious(_ results.ScanTarget, isViolationsResults bool, maliciousFindings []results.ScanResult[[]*sarif.Run]) (err error) { + if !sc.entitledForJas || sc.currentScan.Vulnerabilities == nil { + // JAS results are only supported as vulnerabilities for now + return + } + if err = sc.validateBeforeParse(); err != nil { + return + } + if !isViolationsResults && sc.currentScan.Vulnerabilities.MaliciousResults == nil { + sc.currentScan.Vulnerabilities.MaliciousResults = &formats.ResultSummary{} + } + if isViolationsResults { + if sc.currentScan.Violations.MaliciousResults == nil { + sc.currentScan.Violations.MaliciousResults = &formats.ResultSummary{} + } + } + return results.ApplyHandlerToJasIssues(results.ScanResultsToRuns(maliciousFindings), sc.entitledForJas, sc.getJasHandler(jasutils.MaliciousCode, isViolationsResults)) +} + func (sc *CmdResultsSummaryConverter) ParseIacs(_ results.ScanTarget, isViolationsResults bool, iacs []results.ScanResult[[]*sarif.Run]) (err error) { if !sc.entitledForJas || sc.currentScan.Vulnerabilities == nil { // JAS results are only supported as vulnerabilities for now diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go index aa09deac..04967530 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -42,6 +42,8 @@ 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), + MaliciousViolationsTable: formats.ConvertToMaliciousTableRow(simpleJsonFormat.MaliciousViolations), }, nil } @@ -65,6 +67,10 @@ func (tc *CmdResultsTableConverter) ParseSecrets(target results.ScanTarget, isVi return tc.simpleJsonConvertor.ParseSecrets(target, isViolationsResults, secrets) } +func (tc *CmdResultsTableConverter) ParseMalicious(target results.ScanTarget, isViolationsResults bool, maliciousFindings []results.ScanResult[[]*sarif.Run]) (err error) { + return tc.simpleJsonConvertor.ParseMalicious(target, isViolationsResults, maliciousFindings) +} + func (tc *CmdResultsTableConverter) ParseIacs(target results.ScanTarget, isViolationsResults bool, iacs []results.ScanResult[[]*sarif.Run]) (err error) { return tc.simpleJsonConvertor.ParseIacs(target, isViolationsResults, iacs) } diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go index ff1c3618..4fbc34d8 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -196,6 +196,9 @@ func (rw *ResultsWriter) printTables() (err error) { if rw.shouldPrintSecretValidationExtraMessage() { log.Output("This table contains multiple secret types, such as tokens, generic password, ssh keys and more, token validation is only supported on tokens.") } + if err = rw.printJasTablesIfNeeded(tableContent, utils.MaliciousCodeScan, jasutils.MaliciousCode); err != nil { + return + } if err = rw.printJasTablesIfNeeded(tableContent, utils.IacScan, jasutils.IaC); err != nil { return } @@ -345,6 +348,14 @@ 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.MaliciousViolationsTable, "Malicious Code Violations", + "✨ No Malicious Code violations were found ✨", false) + } else { + 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 99965ba3..aef6232c 100644 --- a/utils/results/results.go +++ b/utils/results/results.go @@ -118,9 +118,10 @@ type JasScansResults struct { } type JasScanResults struct { - SecretsScanResults []ScanResult[[]*sarif.Run] `json:"secrets,omitempty"` - IacScanResults []ScanResult[[]*sarif.Run] `json:"iac,omitempty"` - SastScanResults []ScanResult[[]*sarif.Run] `json:"sast,omitempty"` + SecretsScanResults []ScanResult[[]*sarif.Run] `json:"secrets,omitempty"` + IacScanResults []ScanResult[[]*sarif.Run] `json:"iac,omitempty"` + SastScanResults []ScanResult[[]*sarif.Run] `json:"sast,omitempty"` + MaliciousScanResults []ScanResult[[]*sarif.Run] `json:"malicious_code,omitempty"` } type ScanTarget struct { @@ -482,6 +483,9 @@ func (jsr *JasScansResults) AddJasScanResults(scanType jasutils.JasScanType, vul case jasutils.Sast: jsr.JasVulnerabilities.SastScanResults = append(jsr.JasVulnerabilities.SastScanResults, ScanResult[[]*sarif.Run]{Scan: vulnerabilitiesRuns, StatusCode: exitCode}) jsr.JasViolations.SastScanResults = append(jsr.JasViolations.SastScanResults, ScanResult[[]*sarif.Run]{Scan: violationsRuns, StatusCode: exitCode}) + case jasutils.MaliciousCode: + jsr.JasVulnerabilities.MaliciousScanResults = append(jsr.JasVulnerabilities.MaliciousScanResults, ScanResult[[]*sarif.Run]{Scan: vulnerabilitiesRuns, StatusCode: exitCode}) + jsr.JasViolations.MaliciousScanResults = append(jsr.JasViolations.MaliciousScanResults, ScanResult[[]*sarif.Run]{Scan: violationsRuns, StatusCode: exitCode}) } } @@ -515,6 +519,13 @@ func (jsr *JasScansResults) GetVulnerabilitiesResults(scanType jasutils.JasScanT } results = append(results, scan.Scan...) } + case jasutils.MaliciousCode: + for _, scan := range jsr.JasVulnerabilities.MaliciousScanResults { + if scan.IsScanFailed() { + continue + } + results = append(results, scan.Scan...) + } } return } @@ -542,6 +553,13 @@ func (jsr *JasScansResults) GetViolationsResults(scanType jasutils.JasScanType) } results = append(results, scan.Scan...) } + case jasutils.MaliciousCode: + for _, scan := range jsr.JasViolations.MaliciousScanResults { + if scan.IsScanFailed() { + continue + } + results = append(results, scan.Scan...) + } } return } diff --git a/utils/utils.go b/utils/utils.go index 4d6536fa..af189cab 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -30,6 +30,7 @@ const ( BaseDocumentationURL = "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/" JasInfoURL = "https://jfrog.com/xray/" EntitlementsMinVersion = "3.66.5" + NewJasBinaryMinVersion = "3.85.7" GitRepoKeyAnalyticsMinVersion = "3.114.0" JfrogExternalRunIdEnv = "JFROG_CLI_USAGE_RUN_ID" @@ -50,6 +51,7 @@ const ( IacScan SubScanType = "iac" SastScan SubScanType = "sast" SecretsScan SubScanType = "secrets" + MaliciousCodeScan SubScanType = "malicious-code" SecretTokenValidationScan SubScanType = "secrets_token_validation" ViolationTypeSecurity ViolationIssueType = "security" ViolationTypeLicense ViolationIssueType = "license" @@ -84,7 +86,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. @@ -99,7 +101,8 @@ func IsJASRequested(cmdType CommandType, requestedScans ...SubScanType) bool { return IsScanRequested(cmdType, ContextualAnalysisScan, requestedScans...) || IsScanRequested(cmdType, SecretsScan, requestedScans...) || IsScanRequested(cmdType, IacScan, requestedScans...) || - IsScanRequested(cmdType, SastScan, requestedScans...) + IsScanRequested(cmdType, SastScan, requestedScans...) || + IsScanRequested(cmdType, MaliciousCodeScan, requestedScans...) } func GetScanFindingsLog(scanType SubScanType, vulnerabilitiesCount, violationsCount, threadId int) string { diff --git a/utils/validations/test_mocks.go b/utils/validations/test_mocks.go index 767868e7..01d780bd 100644 --- a/utils/validations/test_mocks.go +++ b/utils/validations/test_mocks.go @@ -148,6 +148,18 @@ func XrayServer(t *testing.T, params MockServerParams) (*httptest.Server, *confi return } } + if r.RequestURI == "/xray/api/v1/internal/cve_applicability_input_vulnerabilities" { + _, err := w.Write([]byte(`{"api_version": "1", "data": [], "scanners": []}`)) + if !assert.NoError(t, err) { + return + } + } + if r.RequestURI == "/xray/api/v1/system/version" { + _, err := w.Write([]byte(fmt.Sprintf(`{"xray_version": "%s", "xray_revision": "xxx"}`, params.XrayVersion))) + if !assert.NoError(t, err) { + return + } + } if r.RequestURI == "/xray/api/v1/entitlements/feature/contextual_analysis" { if r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) diff --git a/utils/validations/test_validate_simple_json.go b/utils/validations/test_validate_simple_json.go index 9cf90b3d..f12147b2 100644 --- a/utils/validations/test_validate_simple_json.go +++ b/utils/validations/test_validate_simple_json.go @@ -46,17 +46,19 @@ func ValidateCommandSimpleJsonOutput(t *testing.T, params ValidationParams) { func ValidateSimpleJsonIssuesCount(t *testing.T, params ValidationParams, results formats.SimpleJsonResults) { actualValues := validationCountActualValues{ // Total - Vulnerabilities: len(results.Vulnerabilities) + len(results.SecretsVulnerabilities) + len(results.SastVulnerabilities) + len(results.IacsVulnerabilities), - Violations: len(results.SecurityViolations) + len(results.LicensesViolations) + len(results.OperationalRiskViolations) + len(results.SecretsViolations) + len(results.SastViolations) + len(results.IacsViolations), + Vulnerabilities: len(results.Vulnerabilities) + len(results.SecretsVulnerabilities) + len(results.SastVulnerabilities) + len(results.IacsVulnerabilities) + len(results.MaliciousVulnerabilities), + Violations: len(results.SecurityViolations) + len(results.LicensesViolations) + len(results.OperationalRiskViolations) + len(results.SecretsViolations) + len(results.SastViolations) + len(results.IacsViolations) + len(results.MaliciousViolations), Licenses: len(results.Licenses), // Jas vulnerabilities - SastVulnerabilities: len(results.SastVulnerabilities), - SecretsVulnerabilities: len(results.SecretsVulnerabilities), - IacVulnerabilities: len(results.IacsVulnerabilities), + SastVulnerabilities: len(results.SastVulnerabilities), + SecretsVulnerabilities: len(results.SecretsVulnerabilities), + IacVulnerabilities: len(results.IacsVulnerabilities), + MaliciousVulnerabilities: len(results.MaliciousVulnerabilities), // Jas violations - SastViolations: len(results.SastViolations), - SecretsViolations: len(results.SecretsViolations), - IacViolations: len(results.IacsViolations), + SastViolations: len(results.SastViolations), + SecretsViolations: len(results.SecretsViolations), + IacViolations: len(results.IacsViolations), + MaliciousViolations: len(results.MaliciousViolations), // Sca vulnerabilities ScaVulnerabilities: len(results.Vulnerabilities), // Sca violations diff --git a/utils/validations/test_validation.go b/utils/validations/test_validation.go index a7db721e..db89c861 100644 --- a/utils/validations/test_validation.go +++ b/utils/validations/test_validation.go @@ -59,6 +59,8 @@ type ScanCount struct { Iac int // Expected number of Secrets issues Secrets int + // Expected number of Malicious code Issues + Malicious int } type SbomCount struct { @@ -280,10 +282,10 @@ type validationCountActualValues struct { // Total counts Vulnerabilities, Violations, Licenses, SbomComponents int // Vulnerabilities counts - SastVulnerabilities, SecretsVulnerabilities, IacVulnerabilities, ScaVulnerabilities int + SastVulnerabilities, SecretsVulnerabilities, IacVulnerabilities, ScaVulnerabilities, MaliciousVulnerabilities int ApplicableVulnerabilities, UndeterminedVulnerabilities, NotCoveredVulnerabilities, NotApplicableVulnerabilities, MissingContextVulnerabilities, InactiveSecretsVulnerabilities int // Violations counts - SastViolations, SecretsViolations, IacViolations, ScaViolations int + SastViolations, SecretsViolations, IacViolations, ScaViolations, MaliciousViolations int SecurityViolations, LicenseViolations, OperationalViolations int ApplicableViolations, UndeterminedViolations, NotCoveredViolations, NotApplicableViolations, MissingContextViolations, InactiveSecretsViolations int // Sbom counts @@ -313,7 +315,7 @@ func ValidateVulnerabilitiesCount(t *testing.T, outputType string, exactMatch bo if params == nil { return } - ValidateScanTypeCount(t, outputType, false, exactMatch, params.ValidateScan, actual.ScaVulnerabilities, actual.SastVulnerabilities, actual.SecretsVulnerabilities, actual.IacVulnerabilities) + ValidateScanTypeCount(t, outputType, false, exactMatch, params.ValidateScan, actual.ScaVulnerabilities, actual.SastVulnerabilities, actual.SecretsVulnerabilities, actual.IacVulnerabilities, actual.MaliciousVulnerabilities) ValidateApplicabilityStatusCount(t, outputType, false, exactMatch, params.ValidateApplicabilityStatus, actual.ApplicableVulnerabilities, actual.UndeterminedVulnerabilities, actual.NotCoveredVulnerabilities, actual.NotApplicableVulnerabilities, actual.MissingContextVulnerabilities, actual.InactiveSecretsVulnerabilities) } @@ -321,12 +323,12 @@ func ValidateViolationCount(t *testing.T, outputType string, exactMatch bool, pa if params == nil { return } - ValidateScanTypeCount(t, outputType, true, exactMatch, params.ValidateScan, actual.ScaViolations, actual.SastViolations, actual.SecretsViolations, actual.IacViolations) + ValidateScanTypeCount(t, outputType, true, exactMatch, params.ValidateScan, actual.ScaViolations, actual.SastViolations, actual.SecretsViolations, actual.IacViolations, actual.MaliciousViolations) ValidateApplicabilityStatusCount(t, outputType, true, exactMatch, params.ValidateApplicabilityStatus, actual.ApplicableViolations, actual.UndeterminedViolations, actual.NotCoveredViolations, actual.NotApplicableViolations, actual.MissingContextViolations, actual.InactiveSecretsViolations) ValidateScaViolationCount(t, outputType, exactMatch, params.ValidateType, actual.SecurityViolations, actual.LicenseViolations, actual.OperationalViolations) } -func ValidateScanTypeCount(t *testing.T, outputType string, violation, exactMatch bool, params *ScanCount, scaViolations, sastViolations, secretsViolations, iacViolations int) { +func ValidateScanTypeCount(t *testing.T, outputType string, violation, exactMatch bool, params *ScanCount, scaViolations, sastViolations, secretsViolations, iacViolations, maliciousViolations int) { if params == nil { return } @@ -339,6 +341,7 @@ func ValidateScanTypeCount(t *testing.T, outputType string, violation, exactMatc CountValidation[int]{Expected: params.Secrets, Actual: secretsViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("secrets %s", suffix), outputType, exactMatch, params.Secrets, secretsViolations)}, CountValidation[int]{Expected: params.Iac, Actual: iacViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("IaC %s", suffix), outputType, exactMatch, params.Iac, iacViolations)}, CountValidation[int]{Expected: params.Sca, Actual: scaViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("Sca %s", suffix), outputType, exactMatch, params.Sca, scaViolations)}, + CountValidation[int]{Expected: params.Malicious, Actual: maliciousViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("malicious %s", suffix), outputType, exactMatch, params.Malicious, maliciousViolations)}, ) }