diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 5d358adc2..8a9c32a3b 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -123,6 +123,7 @@ const ( Licenses = "licenses" Sbom = "sbom" Snippet = "snippet" + Services = "services-scan" Fail = "fail" ExtendedTable = "extended-table" MinSeverity = "min-severity" @@ -202,7 +203,7 @@ var commandFlags = map[string][]string{ binarySca, binarySecrets, binaryWithoutCA, SecretValidation, OutputDir, AnalyzerManagerCustomPath, }, Audit: { - Url, XrayUrl, user, password, accessToken, ServerId, InsecureTls, scanProjectKey, Watches, RepoPath, Snippet, Sbom, Licenses, OutputFormat, ExcludeTestDeps, + Url, XrayUrl, user, password, accessToken, ServerId, InsecureTls, scanProjectKey, Watches, RepoPath, Snippet, Services, Sbom, Licenses, OutputFormat, ExcludeTestDeps, useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm, Pnpm, Yarn, Go, Swift, Cocoapods, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, Threads, auditSca, auditIac, auditSast, auditSecrets, auditWithoutCA, SecretValidation, ScanVuln, OutputDir, SkipAutoInstall, AllowPartialResults, MaxTreeDepth, @@ -215,7 +216,7 @@ var commandFlags = map[string][]string{ // Connection params Url, XrayUrl, user, password, accessToken, ServerId, InsecureTls, // Violations params - scanProjectKey, Watches, Snippet, ScanVuln, Fail, + scanProjectKey, Watches, Snippet, Services, ScanVuln, Fail, // Scan params Threads, ExclusionsAudit, auditSca, auditIac, auditSast, auditSecrets, auditWithoutCA, SecretValidation, Sbom, @@ -283,6 +284,7 @@ var flagsMap = map[string]components.Flag{ 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."), Snippet: components.NewBoolFlag(Snippet, "Set to true if you'd like to enables snippet-level detection to identify copied code from third-party components and surface related license violations.", components.SetHiddenBoolFlag()), + Services: components.NewBoolFlag(Services, "Set to true to enable services detection.", components.SetHiddenBoolFlag()), Licenses: components.NewBoolFlag(Licenses, "Set if you'd also like the list of licenses to be displayed."), Sbom: components.NewBoolFlag(Sbom, "Set if you'd like all the SBOM (Software Bill of Materials) components to be displayed and not only the affected. Ignored if provided 'format' is not 'table' or 'cyclonedx'."), OutputFormat: components.NewStringFlag( diff --git a/cli/gitcommands.go b/cli/gitcommands.go index cc2aff8bc..6dda31c1a 100644 --- a/cli/gitcommands.go +++ b/cli/gitcommands.go @@ -98,12 +98,17 @@ func GitAuditCmd(c *components.Context) error { if err != nil { return err } + includeServicesDetection, err := validateServicesDetection(c) + if err != nil { + return err + } gitAuditCmd.SetSbomGenerator(sbomGenerator).SetScaScanStrategy(scaScanStrategy) gitAuditCmd.SetViolationGenerator(violationGenerator) gitAuditCmd.SetUploadCdxResults(uploadResults).SetRtResultRepository(c.GetStringFlagValue(flags.UploadRtRepoPath)) gitAuditCmd.SetCustomBomGenBinaryPath(c.GetStringFlagValue(flags.XrayLibPluginBinaryCustomPath)) gitAuditCmd.SetCustomAnalyzerManagerBinaryPath(c.GetStringFlagValue(flags.AnalyzerManagerCustomPath)) gitAuditCmd.SetIncludeSnippetDetection(includeSnippetDetection) + gitAuditCmd.SetIncludeServicesDetection(includeServicesDetection) // Run the command with progress bar if needed, Reporting error if Xsc service is enabled err = reportErrorIfExists(xrayVersion, xscVersion, serverDetails, gitAuditCmd.GetProjectKey(), progressbar.ExecWithProgress(gitAuditCmd)) return err diff --git a/cli/scancommands.go b/cli/scancommands.go index f74abfa44..b99738ed0 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -555,6 +555,10 @@ func CreateAuditCmd(c *components.Context) (string, string, *coreConfig.ServerDe if err != nil { return "", "", nil, nil, err } + includeServicesDetection, err := validateServicesDetection(c) + if err != nil { + return "", "", nil, nil, err + } auditCmd.SetBomGenerator(sbomGenerator).SetCustomBomGenBinaryPath(c.GetStringFlagValue(flags.XrayLibPluginBinaryCustomPath)) auditCmd.SetScaScanStrategy(scaScanStrategy) auditCmd.SetViolationGenerator(violationGenerator) @@ -565,6 +569,7 @@ func CreateAuditCmd(c *components.Context) (string, string, *coreConfig.ServerDe SetIncludeLicenses(c.GetBoolFlagValue(flags.Licenses)). SetIncludeSbom(shouldIncludeSbom(c, format)). SetIncludeSnippetDetection(includeSnippetDetection). + SetIncludeServicesDetection(includeServicesDetection). SetFail(c.GetBoolFlagValue(flags.Fail)). SetPrintExtendedTable(c.GetBoolFlagValue(flags.ExtendedTable)). SetMinSeverityFilter(minSeverity). diff --git a/cli/utils.go b/cli/utils.go index 7772e1b28..c9803ab14 100644 --- a/cli/utils.go +++ b/cli/utils.go @@ -155,6 +155,17 @@ func validateSnippetDetection(c *components.Context) (bool, error) { return true, nil } +func isServicesDetectionEnabled(c *components.Context) bool { + if !c.IsFlagSet(flags.Services) { + return false + } + return c.GetBoolFlagValue(flags.Services) +} + +func validateServicesDetection(c *components.Context) (bool, error) { + return isServicesDetectionEnabled(c), nil +} + func getSubScansToPreform(c *components.Context) (subScans []utils.SubScanType, err error) { if c.GetBoolFlagValue(flags.WithoutCA) && !c.GetBoolFlagValue(flags.Sca) { // No CA flag provided but sca flag is not provided, error diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 921602e28..f00d5385b 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -49,17 +49,18 @@ import ( ) type AuditCommand struct { - watches []string - gitRepoHttpsCloneUrl string - projectKey string - targetRepoPath string - IncludeVulnerabilities bool - IncludeLicenses bool - IncludeSbom bool - IncludeSnippetDetection bool - Fail bool - PrintExtendedTable bool - Threads int + watches []string + gitRepoHttpsCloneUrl string + projectKey string + targetRepoPath string + IncludeVulnerabilities bool + IncludeLicenses bool + IncludeSbom bool + IncludeSnippetDetection bool + IncludeServicesDetection bool + Fail bool + PrintExtendedTable bool + Threads int AuditParams } @@ -111,6 +112,11 @@ func (auditCmd *AuditCommand) SetIncludeSnippetDetection(include bool) *AuditCom return auditCmd } +func (auditCmd *AuditCommand) SetIncludeServicesDetection(include bool) *AuditCommand { + auditCmd.IncludeServicesDetection = include + return auditCmd +} + func (auditCmd *AuditCommand) SetFail(fail bool) *AuditCommand { auditCmd.Fail = fail return auditCmd @@ -127,15 +133,16 @@ func (auditCmd *AuditCommand) SetThreads(threads int) *AuditCommand { } // Create a results context based on the provided parameters. resolves conflicts between the parameters based on the retrieved platform watches. -func CreateAuditResultsContext(serverDetails *config.ServerDetails, xrayVersion string, watches []string, artifactoryRepoPath, projectKey, gitRepoHttpsCloneUrl string, includeVulnerabilities, includeLicenses, includeSbom, includeSnippetDetection bool) (context results.ResultContext) { +func CreateAuditResultsContext(serverDetails *config.ServerDetails, xrayVersion string, watches []string, artifactoryRepoPath, projectKey, gitRepoHttpsCloneUrl string, includeVulnerabilities, includeLicenses, includeSbom, includeSnippetDetection, includeServicesDetection bool) (context results.ResultContext) { context = results.ResultContext{ - RepoPath: artifactoryRepoPath, - Watches: watches, - ProjectKey: projectKey, - IncludeVulnerabilities: shouldIncludeVulnerabilities(includeVulnerabilities, watches, artifactoryRepoPath, projectKey, ""), - IncludeLicenses: includeLicenses, - IncludeSbom: includeSbom, - IncludeSnippetDetection: includeSnippetDetection, + RepoPath: artifactoryRepoPath, + Watches: watches, + ProjectKey: projectKey, + IncludeVulnerabilities: shouldIncludeVulnerabilities(includeVulnerabilities, watches, artifactoryRepoPath, projectKey, ""), + IncludeLicenses: includeLicenses, + IncludeSbom: includeSbom, + IncludeSnippetDetection: includeSnippetDetection, + IncludeServicesDetection: includeServicesDetection, } if err := clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, services.MinXrayVersionGitRepoKey); err != nil { // Git repo key is not supported by the Xray version. @@ -187,6 +194,29 @@ func shouldIncludeSnippetDetection(params *AuditParams) bool { return strings.ToLower(os.Getenv(plugin.SnippetDetectionEnvVariable)) == "true" } +func configProfileEnablesServicesScan(profile *xscservices.ConfigProfile) bool { + if profile == nil { + return false + } + for _, module := range profile.Modules { + if module.ScanConfig.ServicesScannerConfig.EnableServicesScan { + return true + } + } + return false +} + +func shouldIncludeServicesDetection(params *AuditParams) bool { + if profile := params.GetConfigProfile(); profile != nil { + for _, module := range profile.Modules { + if module.ScanConfig.ServicesScannerConfig.EnableServicesScan { + return true + } + } + } + return params.resultsContext.IncludeServicesDetection +} + func logScanPaths(workingDirs []string, isRecursiveScan bool) { if len(workingDirs) == 0 { return @@ -263,6 +293,7 @@ func (auditCmd *AuditCommand) Run() (err error) { auditCmd.IncludeLicenses, auditCmd.IncludeSbom, auditCmd.IncludeSnippetDetection, + auditCmd.IncludeServicesDetection, )). SetGitContext(auditCmd.GitContext()). SetThirdPartyApplicabilityScan(auditCmd.thirdPartyApplicabilityScan). @@ -444,6 +475,16 @@ func initAuditCmdResults(params *AuditParams) (cmdResults *results.SecurityComma } cmdResults.SetEntitledForSnippetDetection(entitledForSnippetDetection) } + if shouldIncludeServicesDetection(params) { + entitledForServicesDetection, err := isEntitledForServicesDetection(entitledForJas, xrayManager, params) + if err != nil { + return cmdResults.AddGeneralError(err, false) + } + if !entitledForServicesDetection { + return cmdResults.AddGeneralError(fmt.Errorf("services detection is requested but the JFrog instance is not entitled for it"), false) + } + cmdResults.SetEntitledForServicesDetection(entitledForServicesDetection) + } return } @@ -463,6 +504,13 @@ func isEntitledForSnippetDetection(isEntitledForJas bool, xrayManager *xray.Xray return xrayutils.IsEntitled(xrayManager, auditParams.GetXrayVersion(), xrayplugin.SnippetDetectionFeatureId) } +func isEntitledForServicesDetection(isEntitledForJas bool, xrayManager *xray.XrayServicesManager, auditParams *AuditParams) (entitled bool, err error) { + if !isEntitledForJas { + return false, nil + } + return xrayutils.IsEntitled(xrayManager, auditParams.GetXrayVersion(), xrayplugin.ServicesDetectionFeatureId) +} + func populateScanTargets(cmdResults *results.SecurityCommandResults, params *AuditParams) { // Populate the scan targets based on the provided parameters. detectScanTargets(cmdResults, params) @@ -485,6 +533,7 @@ func populateScanTargets(cmdResults *results.SecurityCommandResults, params *Aud bom.GenerateSbomForTarget(params.BomGenerator().WithOptions( buildinfo.WithDescriptors(targetResult.GetDescriptors()), xrayplugin.WithSnippetDetection(shouldIncludeSnippetDetection(params)), + xrayplugin.WithServicesDetection(shouldIncludeServicesDetection(params)), ), bom.SbomGeneratorParams{ Target: targetResult, @@ -503,6 +552,9 @@ func shouldGenerateSbom(params *AuditParams) bool { if params.resultsContext.IncludeSbom { return true } + if shouldIncludeServicesDetection(params) { + return true + } scansToPerform := params.ScansToPerform() if slices.Contains(scansToPerform, utils.ScaScan) { return true diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 40ecf4012..8dc1c3097 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -309,6 +309,32 @@ func TestShouldGenerateSbom(t *testing.T) { }(), expectSbom: false, }, + { + name: "services detection only", + params: func() *AuditParams { + params := NewAuditParams().SetResultsContext(results.ResultContext{IncludeServicesDetection: true}) + params.SetScansToPerform([]utils.SubScanType{utils.SastScan}) + return params + }(), + expectSbom: true, + }, + { + name: "services enabled in config profile without sca", + params: func() *AuditParams { + params := NewAuditParams().SetResultsContext(results.ResultContext{}) + params.SetScansToPerform([]utils.SubScanType{utils.SastScan}) + params.SetConfigProfile(&services.ConfigProfile{ + Modules: []services.Module{{ + ScanConfig: services.ScanConfig{ + ScaScannerConfig: services.ScaScannerConfig{EnableScaScan: false}, + ServicesScannerConfig: services.ServicesScannerConfig{EnableServicesScan: true}, + }, + }}, + }) + return params + }(), + expectSbom: true, + }, } for _, testCase := range testCases { @@ -939,23 +965,25 @@ func TestCreateResultsContext(t *testing.T) { testCases := []struct { name string - artifactoryRepoPath string - httpCloneUrl string - watches []string - jfrogProjectKey string - includeVulnerabilities bool - includeLicenses bool - includeSbom bool - includeSnippetDetection bool + artifactoryRepoPath string + httpCloneUrl string + watches []string + jfrogProjectKey string + includeVulnerabilities bool + includeLicenses bool + includeSbom bool + includeSnippetDetection bool + includeServicesDetection bool - expectedArtifactoryRepoPath string - expectedHttpCloneUrl string - expectedWatches []string - expectedJfrogProjectKey string - expectedIncludeVulnerabilities bool - expectedIncludeLicenses bool - expectedIncludeSbom bool - expectedIncludeSnippetDetection bool + expectedArtifactoryRepoPath string + expectedHttpCloneUrl string + expectedWatches []string + expectedJfrogProjectKey string + expectedIncludeVulnerabilities bool + expectedIncludeLicenses bool + expectedIncludeSbom bool + expectedIncludeSnippetDetection bool + expectedIncludeServicesDetection bool }{ { name: "Only Vulnerabilities", @@ -995,6 +1023,12 @@ func TestCreateResultsContext(t *testing.T) { expectedIncludeVulnerabilities: true, expectedIncludeSnippetDetection: true, }, + { + name: "Services Detection - no violation context", + includeServicesDetection: true, + expectedIncludeVulnerabilities: true, + expectedIncludeServicesDetection: true, + }, { name: "All", httpCloneUrl: validations.TestMockGitInfo.Source.GitRepoHttpsCloneUrl, @@ -1018,7 +1052,7 @@ func TestCreateResultsContext(t *testing.T) { t.Run(fmt.Sprintf("%s - %s", test.name, testCase.name), func(t *testing.T) { mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: test.xrayVersion, ReturnMockPlatformWatches: test.expectedPlatformWatches}) defer mockServer.Close() - context := CreateAuditResultsContext(serverDetails, test.xrayVersion, testCase.watches, testCase.artifactoryRepoPath, testCase.jfrogProjectKey, testCase.httpCloneUrl, testCase.includeVulnerabilities, testCase.includeLicenses, testCase.includeSbom, testCase.includeSnippetDetection) + context := CreateAuditResultsContext(serverDetails, test.xrayVersion, testCase.watches, testCase.artifactoryRepoPath, testCase.jfrogProjectKey, testCase.httpCloneUrl, testCase.includeVulnerabilities, testCase.includeLicenses, testCase.includeSbom, testCase.includeSnippetDetection, testCase.includeServicesDetection) assert.Equal(t, testCase.expectedArtifactoryRepoPath, context.RepoPath) assert.Equal(t, testCase.expectedHttpCloneUrl, context.GitRepoHttpsCloneUrl) assert.Equal(t, testCase.expectedWatches, context.Watches) @@ -1027,6 +1061,7 @@ func TestCreateResultsContext(t *testing.T) { assert.Equal(t, testCase.expectedIncludeLicenses, context.IncludeLicenses) assert.Equal(t, testCase.expectedIncludeSbom, context.IncludeSbom) assert.Equal(t, testCase.expectedIncludeSnippetDetection, context.IncludeSnippetDetection) + assert.Equal(t, testCase.expectedIncludeServicesDetection, context.IncludeServicesDetection) }) } } diff --git a/commands/git/audit/gitaudit.go b/commands/git/audit/gitaudit.go index c7dc333ed..32b1d8c17 100644 --- a/commands/git/audit/gitaudit.go +++ b/commands/git/audit/gitaudit.go @@ -90,6 +90,7 @@ func toAuditParams(params GitAuditParams) *sourceAudit.AuditParams { params.resultsContext.IncludeLicenses, params.includeSbom, params.resultsContext.IncludeSnippetDetection, + params.resultsContext.IncludeServicesDetection, ) auditParams.SetResultsContext(resultContext) log.Debug(fmt.Sprintf("Results context: %+v", resultContext)) diff --git a/commands/git/audit/gitauditparams.go b/commands/git/audit/gitauditparams.go index 9a0180bc7..6c9bc3045 100644 --- a/commands/git/audit/gitauditparams.go +++ b/commands/git/audit/gitauditparams.go @@ -89,6 +89,11 @@ func (gap *GitAuditParams) SetIncludeSnippetDetection(includeSnippetDetection bo return gap } +func (gap *GitAuditParams) SetIncludeServicesDetection(includeServicesDetection bool) *GitAuditParams { + gap.resultsContext.IncludeServicesDetection = includeServicesDetection + return gap +} + func (gap *GitAuditParams) SetScansToPerform(scansToPerform []utils.SubScanType) *GitAuditParams { gap.scansToPerform = scansToPerform return gap diff --git a/go.mod b/go.mod index 4f7c6980e..8aa0444df 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/jfrog/jfrog-cli-security go 1.26.3 require ( - github.com/CycloneDX/cyclonedx-go v0.10.0 + github.com/CycloneDX/cyclonedx-go v0.11.0 github.com/beevik/etree v1.6.0 github.com/go-git/go-git/v5 v5.19.1 github.com/google/go-github/v56 v56.0.0 @@ -24,9 +24,9 @@ require ( github.com/stretchr/testify v1.11.1 github.com/urfave/cli v1.22.17 github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 - golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f + golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.44.0 + golang.org/x/sys v0.45.0 golang.org/x/text v0.37.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -38,7 +38,7 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/buger/jsonparser v1.2.0 // indirect github.com/c-bata/go-prompt v0.2.6 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -85,7 +85,7 @@ require ( github.com/jedib0t/go-pretty/v6 v6.7.10 // indirect github.com/jfrog/archiver/v3 v3.6.3 // indirect github.com/kevinburke/ssh_config v1.6.0 // indirect - github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/ktrysmt/go-bitbucket v0.9.88 // indirect @@ -101,7 +101,7 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/moby/api v1.54.0 // indirect github.com/moby/moby/client v0.3.0 // indirect - github.com/nwaples/rardecode/v2 v2.2.2 // indirect + github.com/nwaples/rardecode/v2 v2.2.3 // indirect github.com/oklog/run v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -138,9 +138,9 @@ require ( go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.50.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/time v0.15.0 // indirect @@ -151,7 +151,8 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect ) -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go master +//orto17:missconfiguration-service +replace github.com/jfrog/jfrog-client-go => github.com/orto17/jfrog-client-go v0.0.0-20260601083500-656c0e8d801d // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 master diff --git a/go.sum b/go.sum index 66e20d7b9..d63702d8b 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y= -github.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= +github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI= +github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -15,8 +15,8 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -173,15 +173,13 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260522105851-297d2027a72e h1:w github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260522105851-297d2027a72e/go.mod h1:QTAlyhazt1yITTf72eiEfwAdM2xsbE26LmOqaN4wFJc= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260522091649-43f236276873 h1:6X1Hwu0st7c9gbFoIj1fc8qjoQ3wAHWX2qo7K9IxWgU= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260522091649-43f236276873/go.mod h1:D9afcOJmauUYcQZ3WGDg7HejyoBmCQr2XrwXHeN1YY8= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260522071027-8b60a715d6e4 h1:ujVu255rk51l9Uz1t75DdsVoa2MH+lYNV2cB2xDWjPM= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260522071027-8b60a715d6e4/go.mod h1:k3PqoFpS6XDt9/4xg3pS8J8JUvxtaz1w2vdTdodknGk= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -234,8 +232,8 @@ github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= -github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU= -github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.3 h1:qaVuy3ChZDbAQZshPLjHeNJKF3Cru8uo9jmgveKIy2A= +github.com/nwaples/rardecode/v2 v2.2.3/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= @@ -244,6 +242,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/orto17/jfrog-client-go v0.0.0-20260601083500-656c0e8d801d h1:rrT1bx+DqF4SrKHtsy0lx/mnSvSsRzYx9DLZhDmGsDM= +github.com/orto17/jfrog-client-go v0.0.0-20260601083500-656c0e8d801d/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= github.com/owenrumney/go-sarif/v3 v3.2.3 h1:n6mdX5ugKwCrZInvBsf6WumXmpAe3mbmQXgkXlIq34U= github.com/owenrumney/go-sarif/v3 v3.2.3/go.mod h1:1bV7t8SZg7pX41spaDkEUs8/yEjzk9JapztMoX1XNjg= github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= @@ -354,14 +354,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3 h1:VHEvKbpgPXcPXn40t9cDTGK3JZwMikIEyF/CTrFfu7k= +golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -372,8 +372,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -411,8 +411,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -436,8 +436,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/jas/common.go b/jas/common.go index 9892c7e4b..cfdb5631e 100644 --- a/jas/common.go +++ b/jas/common.go @@ -61,6 +61,7 @@ type SpecificScannersExcludePatterns struct { SecretsExcludePatterns []string IacExcludePatterns []string MaliciousCodeExcludePatterns []string + ServicesExcludePatterns []string } type JasScannerOption func(f *JasScanner) error @@ -523,6 +524,7 @@ func UpdateJasScannerWithExcludePatternsFromProfile(scanner *JasScanner, profile scanner.ScannersExclusions.SastExcludePatterns = profile.Modules[0].ScanConfig.SastScannerConfig.ExcludePatterns scanner.ScannersExclusions.SecretsExcludePatterns = profile.Modules[0].ScanConfig.SecretsScannerConfig.ExcludePatterns scanner.ScannersExclusions.IacExcludePatterns = profile.Modules[0].ScanConfig.IacScannerConfig.ExcludePatterns + scanner.ScannersExclusions.ServicesExcludePatterns = profile.Modules[0].ScanConfig.ServicesScannerConfig.ExcludePatterns } func GetStartJasScanLog(scanType utils.SubScanType, threadId int, module jfrogappsconfig.Module, targetCount int) string { diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index 0e8ec9d69..c08bbdf7a 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -12,6 +12,7 @@ import ( "github.com/jfrog/jfrog-cli-security/jas/iac" "github.com/jfrog/jfrog-cli-security/jas/sast" "github.com/jfrog/jfrog-cli-security/jas/secrets" + jfrogServices "github.com/jfrog/jfrog-cli-security/jas/services" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-cli-security/utils/results" @@ -88,6 +89,10 @@ func AddJasScannersTasks(params JasRunnerParams) error { // Scan task addition failure should not impact the other scanners tasks addition, therefore we accumulate the errors and return the overall error at the end. errorsCollection = errors.Join(errorsCollection, generalError) } + + if generalError := addJasScanTaskForModuleIfNeeded(params, utils.ServicesScan, runServicesScan(¶ms)); generalError != nil { + errorsCollection = errors.Join(errorsCollection, generalError) + } return errorsCollection } @@ -201,6 +206,22 @@ func runSastScan(params *JasRunnerParams) parallel.TaskFunc { } } +func runServicesScan(params *JasRunnerParams) parallel.TaskFunc { + return func(threadId int) (err error) { + defer func() { + params.Runner.JasScannersWg.Done() + }() + vulnerabilitiesResults, violationsResults, err := jfrogServices.RunServicesScan(params.Scanner, params.Module, params.TargetCount, threadId, getSourceRunsToCompare(params, jasutils.Services)...) + params.Runner.ResultsMu.Lock() + defer params.Runner.ResultsMu.Unlock() + params.ScanResults.AddJasScanResults(jasutils.Services, vulnerabilitiesResults, violationsResults, jas.GetAnalyzerManagerExitCode(err)) + if err = jas.ParseAnalyzerManagerError(jasutils.Services, err); err != nil { + return fmt.Errorf("%s%s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) + } + return dumpSarifRunToFileIfNeeded(params.TargetOutputDir, jasutils.Services, threadId, vulnerabilitiesResults, violationsResults) + } +} + func runContextualScan(params *JasRunnerParams) parallel.TaskFunc { return func(threadId int) (err error) { defer func() { diff --git a/jas/services/servicesscanner.go b/jas/services/servicesscanner.go new file mode 100644 index 000000000..c5f53f87c --- /dev/null +++ b/jas/services/servicesscanner.go @@ -0,0 +1,108 @@ +package services + +import ( + "path/filepath" + "time" + + 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/v3/pkg/report/v210/sarif" +) + +const ( + servicesScannerType = "services" + servicesScanCommand = "serve" + servicesDocsUrlSuffix = "services-scans" +) + +type ServicesScanManager struct { + scanner *jas.JasScanner + + resultsToCompareFileName string + configFileName string + resultsFileName string +} + +func RunServicesScan(scanner *jas.JasScanner, module jfrogappsconfig.Module, targetCount, threadId int, resultsToCompare ...*sarif.Run) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + var scannerTempDir string + if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Services.String(), threadId); err != nil { + return + } + servicesScanManager, err := newServicesScanManager(scanner, scannerTempDir, resultsToCompare...) + if err != nil { + return + } + startTime := time.Now() + log.Info(jas.GetStartJasScanLog(utils.ServicesScan, threadId, module, targetCount)) + if vulnerabilitiesResults, violationsResults, err = servicesScanManager.scanner.Run(servicesScanManager, module); err != nil { + return + } + log.Info(utils.GetScanFindingsLog(utils.ServicesScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, threadId)) + return +} + +func newServicesScanManager(scanner *jas.JasScanner, scannerTempDir string, resultsToCompare ...*sarif.Run) (manager *ServicesScanManager, err error) { + manager = &ServicesScanManager{ + scanner: scanner, + configFileName: filepath.Join(scannerTempDir, "config.yaml"), + resultsFileName: filepath.Join(scannerTempDir, "results.sarif"), + } + if len(resultsToCompare) == 0 { + return + } + log.Debug("Diff mode - Services results to compare provided") + manager.resultsToCompareFileName = filepath.Join(scannerTempDir, "target.sarif") + if err = jas.SaveScanResultsToCompareAsReport(manager.resultsToCompareFileName, resultsToCompare...); err != nil { + return + } + return +} + +func (ssm *ServicesScanManager) Run(module jfrogappsconfig.Module) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = ssm.createConfigFile(module, ssm.scanner.ScannersExclusions.ServicesExcludePatterns, ssm.scanner.Exclusions...); err != nil { + return + } + if err = ssm.runAnalyzerManager(); err != nil { + return + } + return jas.ReadJasScanRunsFromFile(ssm.resultsFileName, module.SourceRoot, servicesDocsUrlSuffix, ssm.scanner.MinSeverity) +} + +type servicesScanConfig struct { + Scans []servicesScanConfiguration `yaml:"scans"` +} + +type servicesScanConfiguration 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 (ssm *ServicesScanManager) createConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { + roots, err := jas.GetSourceRoots(module, nil) + if err != nil { + return err + } + configFileContent := servicesScanConfig{ + Scans: []servicesScanConfiguration{ + { + Roots: roots, + Output: ssm.resultsFileName, + PathToResultsToCompare: ssm.resultsToCompareFileName, + Type: servicesScannerType, + SkippedDirs: jas.GetExcludePatterns(module, nil, centralConfigExclusions, exclusions...), + }, + }, + } + return jas.CreateScannersConfigFile(ssm.configFileName, configFileContent, jasutils.Services) +} + +func (ssm *ServicesScanManager) runAnalyzerManager() error { + return ssm.scanner.AnalyzerManager.Exec(ssm.configFileName, servicesScanCommand, filepath.Dir(ssm.scanner.AnalyzerManager.AnalyzerManagerFullPath), ssm.scanner.ServerDetails, ssm.scanner.EnvVars) +} diff --git a/policy/enforcer/policyenforcer.go b/policy/enforcer/policyenforcer.go index 43c68cb61..3da6a5e42 100644 --- a/policy/enforcer/policyenforcer.go +++ b/policy/enforcer/policyenforcer.go @@ -148,7 +148,8 @@ func dumpViolationsResponseToFileIfNeeded(generatedViolations *services.Violatio func convertToViolations(cmdResults *results.SecurityCommandResults, generatedViolations []services.XrayViolation) (convertedViolations violationutils.Violations, err error) { convertedViolations = violationutils.Violations{} for _, violation := range generatedViolations { - switch getViolationType(violation) { + violationScanType := getViolationType(violation) + switch violationScanType { case utils.ScaScan: switch violation.Type { case xrayUtils.SecurityViolation: @@ -164,9 +165,13 @@ func convertToViolations(cmdResults *results.SecurityCommandResults, generatedVi if sastViolation := convertToJasViolation(cmdResults, jasutils.Sast, violation); sastViolation != nil { convertedViolations.Sast = append(convertedViolations.Sast, *sastViolation) } - case utils.SecretsScan: - if secretsViolation := convertToJasViolation(cmdResults, jasutils.Secrets, violation); secretsViolation != nil { - convertedViolations.Secrets = append(convertedViolations.Secrets, *secretsViolation) + case utils.SecretsScan, utils.ServicesScan: + if exposuresViolation := convertToExposuresViolation(cmdResults, violationScanType, violation); exposuresViolation != nil { + if violationScanType == utils.SecretsScan { + convertedViolations.Secrets = append(convertedViolations.Secrets, *exposuresViolation) + } else { + convertedViolations.Services = append(convertedViolations.Services, *exposuresViolation) + } } default: log.Warn(fmt.Sprintf("Skipping violation with unknown scan type for violation ID %s", violation.Id)) @@ -207,6 +212,8 @@ func getJasViolationType(jasType jasutils.JasScanType) violationutils.ViolationI return violationutils.SastViolationType case jasutils.Secrets: return violationutils.SecretsViolationType + case jasutils.Services: + return violationutils.ServicesViolationType case jasutils.IaC: return violationutils.IacViolationType default: @@ -280,6 +287,10 @@ func locateBomVulnerabilityInfo(cmdResults *results.SecurityCommandResults, issu return } +func convertToExposuresViolation(cmdResults *results.SecurityCommandResults, scanType utils.SubScanType, violation services.XrayViolation) *violationutils.JasViolation { + return convertToJasViolation(cmdResults, jasutils.SubScanTypeToJasScanType(scanType), violation) +} + func convertToJasViolation(cmdResults *results.SecurityCommandResults, jasType jasutils.JasScanType, violation services.XrayViolation) (jasViolations *violationutils.JasViolation) { match := locateJasVulnerabilityInfo(cmdResults, jasType, violation) if match.rule == nil || match.result == nil || match.location == nil { diff --git a/policy/local/localconvertor.go b/policy/local/localconvertor.go index 874c79dc2..53b4267e2 100644 --- a/policy/local/localconvertor.go +++ b/policy/local/localconvertor.go @@ -56,9 +56,19 @@ func (d *DeprecatedViolationGenerator) GenerateViolations(cmdResults *results.Se } // JAS violations (from JasResults) if target.JasResults != nil { - if len(target.JasResults.JasViolations.SecretsScanResults) > 0 { - if e := results.ForEachJasIssue(target.JasResults.JasViolations.SecretsScanResults, cmdResults.Entitlements.Jas, convertJasViolationsToPolicyViolations(&convertedViolations, jasutils.Secrets)); e != nil { - err = errors.Join(err, fmt.Errorf("failed to convert JAS Secret violations for target %s: %w", target.Target, e)) + for _, exposuresScan := range []struct { + runs []*sarif.Run + violationType violationutils.ViolationIssueType + label string + }{ + {target.JasResults.JasViolations.SecretsScanResults, violationutils.SecretsViolationType, "Secret"}, + {target.JasResults.JasViolations.ServicesScanResults, violationutils.ServicesViolationType, "Services"}, + } { + if len(exposuresScan.runs) == 0 { + continue + } + if e := results.ForEachJasIssue(exposuresScan.runs, cmdResults.Entitlements.Jas, convertExposuresViolationsToPolicyViolations(&convertedViolations, exposuresScan.violationType)); e != nil { + err = errors.Join(err, fmt.Errorf("failed to convert JAS %s violations for target %s: %w", exposuresScan.label, target.Target, e)) } } if len(target.JasResults.JasViolations.IacScanResults) > 0 { @@ -157,6 +167,26 @@ func convertToBasicJasViolation(violationType violationutils.ViolationIssueType, return violation } +func convertExposuresViolationsToPolicyViolations(convertedViolations *violationutils.Violations, violationType violationutils.ViolationIssueType) results.ParseJasIssueFunc { + return func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (err error) { + violation := violationutils.JasViolation{ + Violation: convertToBasicJasViolation(violationType, result, severity), + Rule: rule, + Result: result, + Location: location, + } + switch violationType { + case violationutils.SecretsViolationType: + convertedViolations.Secrets = append(convertedViolations.Secrets, violation) + case violationutils.ServicesViolationType: + convertedViolations.Services = append(convertedViolations.Services, violation) + default: + return fmt.Errorf("unsupported exposures violation type: %s", violationType) + } + return nil + } +} + func convertJasViolationsToPolicyViolations(convertedViolations *violationutils.Violations, jasType jasutils.JasScanType) results.ParseJasIssueFunc { return func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (err error) { switch jasType { diff --git a/sca/bom/xrayplugin/plugin/config.go b/sca/bom/xrayplugin/plugin/config.go index b21bc0003..0a6215bc2 100644 --- a/sca/bom/xrayplugin/plugin/config.go +++ b/sca/bom/xrayplugin/plugin/config.go @@ -20,4 +20,6 @@ type Config struct { IgnorePatterns []string `json:"ignorePatterns,omitempty" yaml:"ignorePatterns,omitempty"` // [Optional] Ecosystems to scan. Ecosystems []techutils.Technology `json:"ecosystems,omitempty" yaml:"ecosystems,omitempty"` + // [Optional] Enable services. + EnableServicesScan bool `json:"enableServicesScan,omitempty" yaml:"enableServicesScan,omitempty"` } diff --git a/sca/bom/xrayplugin/xraylibbom.go b/sca/bom/xrayplugin/xraylibbom.go index 442d922e0..657aced8c 100644 --- a/sca/bom/xrayplugin/xraylibbom.go +++ b/sca/bom/xrayplugin/xraylibbom.go @@ -19,12 +19,16 @@ import ( // SnippetDetectionFeatureId is "curation" because snippet detection is gated by the curation entitlement on the Xray server. const SnippetDetectionFeatureId = "curation" +// ServicesDetectionFeatureId is the Xray entitlement feature id for services detection. +const ServicesDetectionFeatureId = "services" + type XrayLibBomGenerator struct { - binaryPath string - snippetDetection bool - ignorePatterns []string - specificTechs []techutils.Technology - totalTargets int + binaryPath string + snippetDetection bool + servicesDetection bool + ignorePatterns []string + specificTechs []techutils.Technology + totalTargets int } func NewXrayLibBomGenerator() *XrayLibBomGenerator { @@ -74,6 +78,14 @@ func WithSnippetDetection(snippetDetection bool) bom.SbomGeneratorOption { } } +func WithServicesDetection(servicesDetection bool) bom.SbomGeneratorOption { + return func(sg bom.SbomGenerator) { + if sbg, ok := sg.(*XrayLibBomGenerator); ok { + sbg.servicesDetection = servicesDetection + } + } +} + func (sbg *XrayLibBomGenerator) WithOptions(options ...bom.SbomGeneratorOption) bom.SbomGenerator { for _, option := range options { option(sbg) @@ -142,11 +154,12 @@ func (sbg *XrayLibBomGenerator) getXrayLibExecutablePath() (xrayLibPath string, func (sbg *XrayLibBomGenerator) executeScanner(scanner plugin.Scanner, target results.ScanTarget) (output *cyclonedx.BOM, err error) { scanConfig := plugin.Config{ - BomRef: cdxutils.GetFileRef(target.Target), - Type: string(cyclonedx.ComponentTypeFile), - Name: target.Target, - IgnorePatterns: sbg.ignorePatterns, - Ecosystems: sbg.specificTechs, + BomRef: cdxutils.GetFileRef(target.Target), + Type: string(cyclonedx.ComponentTypeFile), + Name: target.Target, + IgnorePatterns: sbg.ignorePatterns, + Ecosystems: sbg.specificTechs, + EnableServicesScan: sbg.servicesDetection, } if scanConfigStr, err := utils.GetAsJsonString(scanConfig, false, true); err == nil { log.Debug(fmt.Sprintf("Scan configuration: %s", scanConfigStr)) diff --git a/tests/testdata/other/configProfile/configProfileExample.json b/tests/testdata/other/configProfile/configProfileExample.json index 3596907ce..2e2e1b406 100644 --- a/tests/testdata/other/configProfile/configProfileExample.json +++ b/tests/testdata/other/configProfile/configProfileExample.json @@ -42,6 +42,9 @@ "iac_scanner_config": { "enable_iac_scan": true, "exclude_patterns": ["*.tfstate"] + }, + "services_scanner_config": { + "enable_services_scan": false } } } diff --git a/utils/formats/conversion.go b/utils/formats/conversion.go index fa180a6b1..8950c7ef9 100644 --- a/utils/formats/conversion.go +++ b/utils/formats/conversion.go @@ -157,6 +157,21 @@ func ConvertToOperationalRiskViolationTableRow(rows []OperationalRiskViolationRo return } +func ConvertToServicesTableRow(rows []SourceCodeRow) (tableRows []servicesTableRow) { + for i := range rows { + tableRows = append(tableRows, servicesTableRow{ + severity: rows[i].Severity, + file: rows[i].File, + lineColumn: strconv.Itoa(rows[i].StartLine) + ":" + strconv.Itoa(rows[i].StartColumn), + finding: rows[i].Finding, + cwe: strings.Join(rows[i].Cwe, ", "), + outcomes: rows[i].Outcomes, + watch: rows[i].Watch, + }) + } + return +} + func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow) { for i := range rows { var status string diff --git a/utils/formats/sarifutils/sarifutils.go b/utils/formats/sarifutils/sarifutils.go index 35badac87..abc9fde9d 100644 --- a/utils/formats/sarifutils/sarifutils.go +++ b/utils/formats/sarifutils/sarifutils.go @@ -21,6 +21,7 @@ const ( JasScannerIdSarifPropertyKey = "scanner_id" FailPrSarifPropertyKey = "failPullRequest" CWEPropertyKey = "CWE" + OutcomesPropertyKey = "Outcomes" SarifImpactPathsRulePropertyKey = "impactPaths" TokenValidationStatusSarifPropertyKey = "tokenValidation" TokenValidationMetadataSarifPropertyKey = "metadata" @@ -132,16 +133,38 @@ func GetRuleUndeterminedReason(rule *sarif.ReportingDescriptor) string { func GetRuleCWE(rule *sarif.ReportingDescriptor) (cwe []string) { if rule == nil || rule.DefaultConfiguration == nil || rule.DefaultConfiguration.Parameters == nil || rule.DefaultConfiguration.Parameters.Properties == nil { - // No CWE property return } if cweProperty, ok := rule.DefaultConfiguration.Parameters.Properties[CWEPropertyKey]; ok { if cweValue, ok := cweProperty.(string); ok { - split := strings.Split(cweValue, ",") - for _, policy := range split { - cwe = append(cwe, strings.TrimSpace(policy)) - } - return + return ParseCWEValue(cweValue) + } + } + return +} + +func GetResultCWE(result *sarif.Result) []string { + return ParseCWEValue(GetResultProperty(CWEPropertyKey, result)) +} + +func GetResultOutcomes(result *sarif.Result) string { + return GetResultProperty(OutcomesPropertyKey, result) +} + +func GetServicesCWE(rule *sarif.ReportingDescriptor, result *sarif.Result) []string { + if cwe := GetResultCWE(result); len(cwe) > 0 { + return cwe + } + return GetRuleCWE(rule) +} + +func ParseCWEValue(cweValue string) (cwe []string) { + if cweValue == "" { + return + } + for _, part := range strings.Split(cweValue, ",") { + if trimmed := strings.TrimSpace(part); trimmed != "" { + cwe = append(cwe, trimmed) } } return diff --git a/utils/formats/sarifutils/sarifutils_test.go b/utils/formats/sarifutils/sarifutils_test.go index d1451a7c9..0cef88050 100644 --- a/utils/formats/sarifutils/sarifutils_test.go +++ b/utils/formats/sarifutils/sarifutils_test.go @@ -620,3 +620,28 @@ func TestGetResultFingerprint(t *testing.T) { assert.Equal(t, test.expectedOutput, GetResultFingerprint(test.result)) } } + +func createRuleWithCWE(ruleId, cwe string) *sarif.ReportingDescriptor { + return sarif.NewRule(ruleId).WithDefaultConfiguration( + sarif.NewReportingConfiguration().WithParameters( + sarif.NewPropertyBag().Add(CWEPropertyKey, cwe), + ), + ) +} + +func TestGetServicesCWEAndOutcomes(t *testing.T) { + rule := createRuleWithCWE("service-rule", "CWE-200") + result := CreateResultWithProperties("msg", "service-rule", "warning", map[string]string{ + CWEPropertyKey: "CWE-918", + OutcomesPropertyKey: "exposed-endpoint", + }, CreateLocation("config.yml", 1, 1, 1, 10, "")) + + assert.Equal(t, []string{"CWE-918"}, GetServicesCWE(rule, result)) + assert.Equal(t, "exposed-endpoint", GetResultOutcomes(result)) + + resultWithoutCWE := CreateResultWithProperties("msg", "service-rule", "warning", map[string]string{ + OutcomesPropertyKey: "misconfigured-port", + }, CreateLocation("config.yml", 2, 1, 2, 10, "")) + assert.Equal(t, []string{"CWE-200"}, GetServicesCWE(rule, resultWithoutCWE)) + assert.Equal(t, "misconfigured-port", GetResultOutcomes(resultWithoutCWE)) +} diff --git a/utils/formats/simplejsonapi.go b/utils/formats/simplejsonapi.go index 2e029276f..4e789cd2d 100644 --- a/utils/formats/simplejsonapi.go +++ b/utils/formats/simplejsonapi.go @@ -17,9 +17,11 @@ type SimpleJsonResults struct { Licenses []LicenseRow `json:"licenses"` OperationalRiskViolations []OperationalRiskViolationRow `json:"operationalRiskViolations"` SecretsVulnerabilities []SourceCodeRow `json:"secrets"` + ServicesVulnerabilities []SourceCodeRow `json:"services"` IacsVulnerabilities []SourceCodeRow `json:"iac"` SastVulnerabilities []SourceCodeRow `json:"sast"` SecretsViolations []SourceCodeRow `json:"secretsViolations"` + ServicesViolations []SourceCodeRow `json:"servicesViolations"` IacsViolations []SourceCodeRow `json:"iacViolations"` SastViolations []SourceCodeRow `json:"sastViolations"` MaliciousVulnerabilities []SourceCodeRow `json:"maliciousCode"` @@ -34,6 +36,7 @@ type ScanStatus struct { SastStatusCode *int `json:"sastScanStatusCode,omitempty"` IacStatusCode *int `json:"iacScanStatusCode,omitempty"` SecretsStatusCode *int `json:"secretsScanStatusCode,omitempty"` + ServicesStatusCode *int `json:"servicesScanStatusCode,omitempty"` ApplicabilityStatusCode *int `json:"ContextualAnalysisScanStatusCode,omitempty"` MaliciousStatusCode *int `json:"MaliciousStatusCode,omitempty"` } @@ -111,6 +114,7 @@ type SourceCodeRow struct { Location Finding string `json:"finding,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` + Outcomes string `json:"outcomes,omitempty"` Applicability *Applicability `json:"applicability,omitempty"` CodeFlow [][]Location `json:"codeFlow,omitempty"` } diff --git a/utils/formats/summary.go b/utils/formats/summary.go index 505e4cff3..85b53b8f8 100644 --- a/utils/formats/summary.go +++ b/utils/formats/summary.go @@ -8,6 +8,7 @@ import ( const ( IacResult SummaryResultType = "IAC" SecretsResult SummaryResultType = "Secrets" + ServicesResult SummaryResultType = "Services" SastResult SummaryResultType = "SAST" ScaResult SummaryResultType = "SCA" ScaSecurityResult SummaryResultType = "Security" @@ -39,6 +40,7 @@ type ScanResultSummary struct { ScaResults *ScaScanResultSummary `json:"sca,omitempty"` IacResults *ResultSummary `json:"iac,omitempty"` SecretsResults *ResultSummary `json:"secrets,omitempty"` + ServicesResults *ResultSummary `json:"services,omitempty"` SastResults *ResultSummary `json:"sast,omitempty"` MaliciousResults *ResultSummary `json:"maliciousCode,omitempty"` } @@ -182,6 +184,9 @@ func (srs *ScanResultSummary) GetTotal(filterTypes ...SummaryResultType) (total if srs.SecretsResults != nil && isFilterApply(SecretsResult, filterTypes) { total += srs.SecretsResults.GetTotal() } + if srs.ServicesResults != nil && isFilterApply(ServicesResult, filterTypes) { + total += srs.ServicesResults.GetTotal() + } if srs.SastResults != nil && isFilterApply(SastResult, filterTypes) { total += srs.SastResults.GetTotal() } @@ -227,6 +232,9 @@ func (ss *ScanResultSummary) GetSummaryBySeverity() (summary ResultSummary) { if ss.SecretsResults != nil { summary = MergeResultSummaries(summary, *ss.SecretsResults) } + if ss.ServicesResults != nil { + summary = MergeResultSummaries(summary, *ss.ServicesResults) + } if ss.SastResults != nil { summary = MergeResultSummaries(summary, *ss.SastResults) } @@ -304,6 +312,9 @@ func extractIssuesToSummary(issues *ScanResultSummary, destination *ScanResultSu if issues.SecretsResults != nil { destination.SecretsResults = mergeResultSummariesPointers(destination.SecretsResults, issues.SecretsResults) } + if issues.ServicesResults != nil { + destination.ServicesResults = mergeResultSummariesPointers(destination.ServicesResults, issues.ServicesResults) + } if issues.SastResults != nil { destination.SastResults = mergeResultSummariesPointers(destination.SastResults, issues.SastResults) } diff --git a/utils/formats/table.go b/utils/formats/table.go index b3ec7e57a..5da4ab12d 100644 --- a/utils/formats/table.go +++ b/utils/formats/table.go @@ -23,6 +23,9 @@ type ResultsTables struct { // Secrets SecretsVulnerabilitiesTable []secretsTableRow SecretsViolationsTable []secretsTableRow + // Services + ServicesVulnerabilitiesTable []servicesTableRow + ServicesViolationsTable []servicesTableRow // Malicious Code MaliciousVulnerabilitiesTable []maliciousTableRow } @@ -169,6 +172,16 @@ type secretsTableRow struct { watch string `col-name:"Watch Name" omitempty:"true"` } +type servicesTableRow struct { + severity string `col-name:"Severity"` + file string `col-name:"File"` + lineColumn string `col-name:"Line:Column"` + finding string `col-name:"Finding"` + cwe string `col-name:"CWE" omitempty:"true"` + outcomes string `col-name:"Outcomes" omitempty:"true"` + watch string `col-name:"Watch Name" omitempty:"true"` +} + type iacOrSastTableRow struct { severity string `col-name:"Severity"` file string `col-name:"File"` diff --git a/utils/formats/violationutils/violations.go b/utils/formats/violationutils/violations.go index eaa67c1c2..fca259629 100644 --- a/utils/formats/violationutils/violations.go +++ b/utils/formats/violationutils/violations.go @@ -14,12 +14,13 @@ import ( ) const ( - LicenseViolationType ViolationIssueType = "license" - OperationalRiskType ViolationIssueType = "operational_risk" - CveViolationType ViolationIssueType = "cve" - SecretsViolationType ViolationIssueType = "secrets" - IacViolationType ViolationIssueType = "iac" - SastViolationType ViolationIssueType = "sast" + LicenseViolationType ViolationIssueType = "license" + OperationalRiskType ViolationIssueType = "operational_risk" + CveViolationType ViolationIssueType = "cve" + SecretsViolationType ViolationIssueType = "secrets" + ServicesViolationType ViolationIssueType = "services" + IacViolationType ViolationIssueType = "iac" + SastViolationType ViolationIssueType = "sast" ) type ViolationIssueType string @@ -43,20 +44,21 @@ func (v ScaViolationIssueType) String() string { } type Violations struct { - Sca []CveViolation `json:"sca,omitempty"` - License []LicenseViolation `json:"license,omitempty"` - OpRisk []OperationalRiskViolation `json:"operational_risk,omitempty"` - Secrets []JasViolation `json:"secrets,omitempty"` - Iac []JasViolation `json:"iac,omitempty"` - Sast []JasViolation `json:"sast,omitempty"` + Sca []CveViolation `json:"sca,omitempty"` + License []LicenseViolation `json:"license,omitempty"` + OpRisk []OperationalRiskViolation `json:"operational_risk,omitempty"` + Secrets []JasViolation `json:"secrets,omitempty"` + Services []JasViolation `json:"services,omitempty"` + Iac []JasViolation `json:"iac,omitempty"` + Sast []JasViolation `json:"sast,omitempty"` } func (vs *Violations) HasViolations() bool { - return len(vs.Sca) > 0 || len(vs.License) > 0 || len(vs.OpRisk) > 0 || len(vs.Secrets) > 0 || len(vs.Iac) > 0 || len(vs.Sast) > 0 + return len(vs.Sca) > 0 || len(vs.License) > 0 || len(vs.OpRisk) > 0 || len(vs.Secrets) > 0 || len(vs.Services) > 0 || len(vs.Iac) > 0 || len(vs.Sast) > 0 } func (vs *Violations) Count() int { - return len(vs.Sca) + len(vs.License) + len(vs.OpRisk) + len(vs.Secrets) + len(vs.Iac) + len(vs.Sast) + return len(vs.Sca) + len(vs.License) + len(vs.OpRisk) + len(vs.Secrets) + len(vs.Services) + len(vs.Iac) + len(vs.Sast) } func (vs *Violations) String() string { @@ -76,6 +78,9 @@ func (vs *Violations) String() string { if len(vs.Secrets) > 0 { out = append(out, fmt.Sprintf("%d Secrets", len(vs.Secrets))) } + if len(vs.Services) > 0 { + out = append(out, fmt.Sprintf("%d Services", len(vs.Services))) + } if len(vs.Iac) > 0 { out = append(out, fmt.Sprintf("%d IaC", len(vs.Iac))) } @@ -106,6 +111,11 @@ func (vs *Violations) ShouldFailBuild() bool { return true } } + for _, v := range vs.Services { + if v.ShouldFailBuild() { + return true + } + } for _, v := range vs.Iac { if v.ShouldFailBuild() { return true @@ -140,6 +150,11 @@ func (vs *Violations) ShouldFailPR() bool { return true } } + for _, v := range vs.Services { + if v.ShouldFailPR() { + return true + } + } for _, v := range vs.Iac { if v.ShouldFailPR() { return true diff --git a/utils/jasutils/jasutils.go b/utils/jasutils/jasutils.go index 621e9262a..c4bb44e45 100644 --- a/utils/jasutils/jasutils.go +++ b/utils/jasutils/jasutils.go @@ -22,6 +22,7 @@ const ( IaC JasScanType = "IaC" Sast JasScanType = "Sast" MaliciousCode JasScanType = "MaliciousCode" + Services JasScanType = "Services" ) const ( @@ -41,7 +42,7 @@ func (jst JasScanType) String() string { } func GetJasScanTypes() []JasScanType { - return []JasScanType{Applicability, Secrets, IaC, Sast, MaliciousCode} + return []JasScanType{Applicability, Secrets, IaC, Sast, MaliciousCode, Services} } func (tvs TokenValidationStatus) String() string { return string(tvs) } @@ -100,6 +101,8 @@ func SubScanTypeToJasScanType(subScanType utils.SubScanType) JasScanType { return Applicability case utils.MaliciousCodeScan: return MaliciousCode + case utils.ServicesScan: + return Services } return "" } diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go index 04fbf2d88..31760b74c 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -68,8 +68,8 @@ type ResultsStreamFormatParser[T interface{}] interface { ParseSbom(sbom *cyclonedx.BOM) error ParseSbomLicenses(sbom *cyclonedx.BOM) error ParseCVEs(enrichedSbom *cyclonedx.BOM, applicableScan ...[]*sarif.Run) error - // Parse JAS content to the current scan target - ParseSecrets(secrets ...[]*sarif.Run) error + // Parse JAS exposures scans that share the secrets-like output shape (secrets + services). + ParseExposuresScans(secrets, services []*sarif.Run) error ParseIacs(iacs ...[]*sarif.Run) error ParseSast(sast ...[]*sarif.Run) error ParseMalicious(malicious ...[]*sarif.Run) error @@ -206,8 +206,10 @@ func parseJasResults[T interface{}](params ResultConvertParams, parser ResultsSt if targetResults.JasResults == nil || !params.IncludeVulnerabilities { return } - // Parsing JAS Secrets results - if err = parser.ParseSecrets(targetResults.JasResults.JasVulnerabilities.SecretsScanResults); err != nil { + if err = parser.ParseExposuresScans( + targetResults.JasResults.JasVulnerabilities.SecretsScanResults, + targetResults.JasResults.JasVulnerabilities.ServicesScanResults, + ); err != nil { return } // Parsing JAS IAC results diff --git a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go index 4d47c2771..d71829eb5 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -28,6 +28,7 @@ const ( // Properties for secret validation secretValidationPropertyTemplate = "jfrog:secret-validation:status:" + results.LocationIdTemplate secretValidationMetadataPropertyTemplate = "jfrog:secret-validation:metadata:" + results.LocationIdTemplate + servicesOutcomesPropertyTemplate = "jfrog:services:outcomes:" + results.LocationIdTemplate // Git context property gitContextProperty = "jfrog:git:context" ) @@ -154,12 +155,22 @@ func (cdc *CmdResultsCycloneDxConverter) ParseCVEs(enrichedSbom *cyclonedx.BOM, ) } -func (cdc *CmdResultsCycloneDxConverter) ParseSecrets(secrets ...[]*sarif.Run) (err error) { +func (cdc *CmdResultsCycloneDxConverter) ParseExposuresScans(secrets, services []*sarif.Run) (err error) { + if err = cdc.parseExposuresScan("secret", secrets); err != nil { + return + } + return cdc.parseServicesScan(services) +} + +func (cdc *CmdResultsCycloneDxConverter) parseExposuresScan(locationLabel string, runs []*sarif.Run) (err error) { if cdc.bom == nil { return results.ErrResetConvertor } - source := cdc.addJasService(secrets) - return results.ForEachJasIssue(results.CollectRuns(secrets...), cdc.entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (e error) { + if len(runs) == 0 { + return + } + source := cdc.addJasService([][]*sarif.Run{runs}) + return results.ForEachJasIssue(runs, cdc.entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (e error) { startLine := sarifutils.GetLocationStartLine(location) startColumn := sarifutils.GetLocationStartColumn(location) endLine := sarifutils.GetLocationEndLine(location) @@ -170,7 +181,7 @@ func (cdc *CmdResultsCycloneDxConverter) ParseSecrets(secrets ...[]*sarif.Run) ( properties := []cyclonedx.Property{} applicabilityStatus := jasutils.NotScanned if secretValidation := results.GetJasResultApplicability(result); secretValidation != nil { - // Secret validation results exist + // Exposure validation results exist applicabilityStatus = jasutils.ConvertToApplicabilityStatus(secretValidation.Status) properties = append(properties, cyclonedx.Property{ Name: fmt.Sprintf(secretValidationPropertyTemplate, affectedComponent.BOMRef, startLine, startColumn, endLine, endColumn), @@ -187,7 +198,7 @@ func (cdc *CmdResultsCycloneDxConverter) ParseSecrets(secrets ...[]*sarif.Run) ( jasIssue := cdc.getOrCreateJasIssue(sarifutils.GetResultRuleId(result), sarifutils.GetSecretScannerRuleId(rule), sarifutils.GetResultMsgText(result), sarifutils.GetRuleShortDescriptionText(rule), source, sarifutils.GetRuleCWE(rule), ratings) // Add the location to the vulnerability properties = append(properties, cyclonedx.Property{ - Name: fmt.Sprintf(jasIssueLocationPropertyTemplate, "secret", affectedComponent.BOMRef, startLine, startColumn, endLine, endColumn), + Name: fmt.Sprintf(jasIssueLocationPropertyTemplate, locationLabel, affectedComponent.BOMRef, startLine, startColumn, endLine, endColumn), Value: sarifutils.GetLocationSnippetText(location), }) results.AddFileIssueAffects(jasIssue, *affectedComponent, properties...) @@ -195,6 +206,45 @@ func (cdc *CmdResultsCycloneDxConverter) ParseSecrets(secrets ...[]*sarif.Run) ( }) } +func (cdc *CmdResultsCycloneDxConverter) parseServicesScan(runs []*sarif.Run) (err error) { + if cdc.bom == nil { + return results.ErrResetConvertor + } + if len(runs) == 0 { + return + } + source := cdc.addJasService([][]*sarif.Run{runs}) + return results.ForEachJasIssue(runs, cdc.entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (e error) { + startLine := sarifutils.GetLocationStartLine(location) + startColumn := sarifutils.GetLocationStartColumn(location) + endLine := sarifutils.GetLocationEndLine(location) + endColumn := sarifutils.GetLocationEndColumn(location) + affectedComponent := cdc.getOrCreateFileComponent(getRelativePath(location, cdc.currentTarget)) + 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.GetServicesCWE(rule, result), + ratings, + ) + properties := []cyclonedx.Property{{ + Name: fmt.Sprintf(jasIssueLocationPropertyTemplate, "services", affectedComponent.BOMRef, startLine, startColumn, endLine, endColumn), + Value: sarifutils.GetLocationSnippetText(location), + }} + if outcomes := sarifutils.GetResultOutcomes(result); outcomes != "" { + properties = append(properties, cyclonedx.Property{ + Name: fmt.Sprintf(servicesOutcomesPropertyTemplate, affectedComponent.BOMRef, startLine, startColumn, endLine, endColumn), + Value: outcomes, + }) + } + results.AddFileIssueAffects(jasIssue, *affectedComponent, properties...) + return + }) +} + func (cdc *CmdResultsCycloneDxConverter) ParseIacs(iacs ...[]*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 923a0e5a2..26149968f 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -77,6 +77,7 @@ type currentTargetRuns struct { // Current run cache information scaCurrentRun *sarif.Run secretsCurrentRun *sarif.Run + servicesCurrentRun *sarif.Run iacCurrentRun *sarif.Run sastCurrentRun *sarif.Run maliciousCurrentRun *sarif.Run @@ -156,6 +157,9 @@ func (sc *CmdResultsSarifConverter) flush() { if sc.currentTargetConvertedRuns.secretsCurrentRun != nil { sc.current.Runs = append(sc.current.Runs, sc.currentTargetConvertedRuns.secretsCurrentRun) } + if sc.currentTargetConvertedRuns.servicesCurrentRun != nil { + sc.current.Runs = append(sc.current.Runs, sc.currentTargetConvertedRuns.servicesCurrentRun) + } // Flush iac if needed if sc.currentTargetConvertedRuns.iacCurrentRun != nil { sc.current.Runs = append(sc.current.Runs, sc.currentTargetConvertedRuns.iacCurrentRun) @@ -282,10 +286,10 @@ func (sc *CmdResultsSarifConverter) ParseViolations(violationsScanResults violat if len(scaRules) > 0 && len(scaSarifResults) > 0 { sc.addResultsToCurrentRun(ViolationsRun, maps.Values(scaRules), scaSarifResults...) } - // Secrets violations - for _, secretViolation := range violationsScanResults.Secrets { - secretResult, secretRule := createJasViolation(secretViolation) - sc.addResultsToCurrentRun(ViolationsRun, []*sarif.ReportingDescriptor{secretRule}, secretResult) + // Exposures scan violations (secrets + services) + for _, violation := range append(violationsScanResults.Secrets, violationsScanResults.Services...) { + jasResult, jasRule := createJasViolation(violation) + sc.addResultsToCurrentRun(ViolationsRun, []*sarif.ReportingDescriptor{jasRule}, jasResult) } // IaC violations for _, iacViolation := range violationsScanResults.Iac { @@ -418,11 +422,18 @@ func (sc *CmdResultsSarifConverter) getVulnerabilitiesConvertParams(scanType uti return getSarifConvertParams(sc.currentCmdType, scanType, &sc.currentTargetConvertedRuns.currentTarget, false, sc.patchBinaryPaths, sc.baseJfrogUrl) } -func (sc *CmdResultsSarifConverter) ParseSecrets(secrets ...[]*sarif.Run) (err error) { - if err = sc.validateBeforeParse(); err != nil || !sc.entitledForJas { +func (sc *CmdResultsSarifConverter) ParseExposuresScans(secrets, services []*sarif.Run) (err error) { + if err = sc.appendExposuresScan(utils.SecretsScan, secrets, &sc.currentTargetConvertedRuns.secretsCurrentRun); err != nil { + return + } + return sc.appendExposuresScan(utils.ServicesScan, services, &sc.currentTargetConvertedRuns.servicesCurrentRun) +} + +func (sc *CmdResultsSarifConverter) appendExposuresScan(scanType utils.SubScanType, runs []*sarif.Run, destination **sarif.Run) (err error) { + if err = sc.validateBeforeParse(); err != nil || !sc.entitledForJas || len(runs) == 0 { return } - sc.currentTargetConvertedRuns.secretsCurrentRun = combineJasRunsToCurrentRun(sc.currentTargetConvertedRuns.secretsCurrentRun, patchSarifRuns(sc.getVulnerabilitiesConvertParams(utils.SecretsScan), results.CollectRuns(secrets...)...)...) + *destination = combineJasRunsToCurrentRun(*destination, patchSarifRuns(sc.getVulnerabilitiesConvertParams(scanType), runs...)...) return } @@ -904,6 +915,8 @@ func getResultViolationType(violationType string) utils.SubScanType { switch violationutils.ViolationIssueType(violationType) { case violationutils.SecretsViolationType: return utils.SecretsScan + case violationutils.ServicesViolationType: + return utils.ServicesScan case violationutils.IacViolationType: return utils.IacScan case violationutils.SastViolationType: diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index 84280b41c..af08c54b1 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -54,6 +54,7 @@ func (sjc *CmdResultsSimpleJsonConverter) Reset(metadata results.ResultsMetaData ScaStatusCode: statusCodes.ScaScanStatusCode, ApplicabilityStatusCode: statusCodes.ContextualAnalysisStatusCode, SecretsStatusCode: statusCodes.SecretsScanStatusCode, + ServicesStatusCode: statusCodes.ServicesScanStatusCode, IacStatusCode: statusCodes.IacScanStatusCode, SastStatusCode: statusCodes.SastScanStatusCode, }, @@ -202,12 +203,9 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseViolations(violationsScanResults for _, opRiskViolation := range violationsScanResults.OpRisk { sjc.current.OperationalRiskViolations = append(sjc.current.OperationalRiskViolations, sjc.createOpRiskViolationRow(opRiskViolation)) } - // Secrets Violations - for _, jasViolation := range violationsScanResults.Secrets { - violation := createSourceCodeRow(jasViolation.Rule, jasViolation.Severity, jasViolation.Result, jasViolation.Location, []*sarif.Invocation{}, sjc.pretty) - violation.ViolationContext = convertToViolationContext(jasViolation.Violation) - sjc.current.SecretsViolations = append(sjc.current.SecretsViolations, violation) - } + // Exposures scan violations (secrets + services share the same row shape) + sjc.appendExposuresViolations(violationsScanResults.Secrets, &sjc.current.SecretsViolations) + sjc.appendServicesExposuresViolations(violationsScanResults.Services, &sjc.current.ServicesViolations) // IaC Violations for _, jasViolation := range violationsScanResults.Iac { violation := createSourceCodeRow(jasViolation.Rule, jasViolation.Severity, jasViolation.Result, jasViolation.Location, []*sarif.Invocation{}, sjc.pretty) @@ -333,21 +331,51 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSbom(_ *cyclonedx.BOM) (err error return } -func (sjc *CmdResultsSimpleJsonConverter) ParseSecrets(secrets ...[]*sarif.Run) (err error) { +func (sjc *CmdResultsSimpleJsonConverter) ParseExposuresScans(secrets, services []*sarif.Run) (err error) { if !sjc.entitledForJas { return } if sjc.current == nil { return results.ErrResetConvertor } - secretsSimpleJson, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, results.CollectRuns(secrets...)...) - if err != nil || len(secretsSimpleJson) == 0 { + if err = sjc.appendExposuresVulnerabilities(secrets, func(rows []formats.SourceCodeRow) { + sjc.current.SecretsVulnerabilities = append(sjc.current.SecretsVulnerabilities, rows...) + }); err != nil { + return + } + return sjc.appendServicesExposuresVulnerabilities(services, func(rows []formats.SourceCodeRow) { + sjc.current.ServicesVulnerabilities = append(sjc.current.ServicesVulnerabilities, rows...) + }) +} + +func (sjc *CmdResultsSimpleJsonConverter) appendExposuresVulnerabilities(runs []*sarif.Run, appendRows func([]formats.SourceCodeRow)) (err error) { + if len(runs) == 0 { + return + } + rows, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, runs...) + if err != nil || len(rows) == 0 { return } - sjc.current.SecretsVulnerabilities = append(sjc.current.SecretsVulnerabilities, secretsSimpleJson...) + appendRows(rows) return } +func (sjc *CmdResultsSimpleJsonConverter) appendExposuresViolations(violations []violationutils.JasViolation, destination *[]formats.SourceCodeRow) { + for _, jasViolation := range violations { + violation := createSourceCodeRow(jasViolation.Rule, jasViolation.Severity, jasViolation.Result, jasViolation.Location, []*sarif.Invocation{}, sjc.pretty) + violation.ViolationContext = convertToViolationContext(jasViolation.Violation) + *destination = append(*destination, violation) + } +} + +func (sjc *CmdResultsSimpleJsonConverter) appendServicesExposuresViolations(violations []violationutils.JasViolation, destination *[]formats.SourceCodeRow) { + for _, jasViolation := range violations { + violation := createServicesSourceCodeRow(jasViolation.Rule, jasViolation.Severity, jasViolation.Result, jasViolation.Location, []*sarif.Invocation{}, sjc.pretty) + violation.ViolationContext = convertToViolationContext(jasViolation.Violation) + *destination = append(*destination, violation) + } +} + func (sjc *CmdResultsSimpleJsonConverter) ParseIacs(iacs ...[]*sarif.Run) (err error) { if !sjc.entitledForJas { return @@ -467,6 +495,34 @@ func PrepareSimpleJsonJasIssues(entitledForJas, pretty bool, jasIssues ...*sarif return rows, err } +func (sjc *CmdResultsSimpleJsonConverter) appendServicesExposuresVulnerabilities(runs []*sarif.Run, appendRows func([]formats.SourceCodeRow)) (err error) { + if len(runs) == 0 { + return + } + rows, err := PrepareSimpleJsonServicesIssues(sjc.entitledForJas, sjc.pretty, runs...) + if err != nil || len(rows) == 0 { + return + } + appendRows(rows) + return +} + +func PrepareSimpleJsonServicesIssues(entitledForJas, pretty bool, jasIssues ...*sarif.Run) ([]formats.SourceCodeRow, error) { + var rows []formats.SourceCodeRow + err := results.ForEachJasIssue(jasIssues, entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) error { + rows = append(rows, createServicesSourceCodeRow(rule, severity, result, location, run.Invocations, pretty)) + return nil + }) + return rows, err +} + +func createServicesSourceCodeRow(rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location, invocations []*sarif.Invocation, pretty bool) formats.SourceCodeRow { + row := createSourceCodeRow(rule, severity, result, location, invocations, pretty) + row.Cwe = sarifutils.GetServicesCWE(rule, result) + row.Outcomes = sarifutils.GetResultOutcomes(result) + return row +} + func createSourceCodeRow(rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location, invocations []*sarif.Invocation, pretty bool) formats.SourceCodeRow { return formats.SourceCodeRow{ ScannerInfo: formats.ScannerInfo{ @@ -673,6 +729,9 @@ func sortResults(simpleJsonResults *formats.SimpleJsonResults) { if len(simpleJsonResults.SecretsVulnerabilities) > 0 { sortSourceCodeRow(simpleJsonResults.SecretsVulnerabilities) } + if len(simpleJsonResults.ServicesVulnerabilities) > 0 { + sortSourceCodeRow(simpleJsonResults.ServicesVulnerabilities) + } if len(simpleJsonResults.IacsVulnerabilities) > 0 { sortSourceCodeRow(simpleJsonResults.IacsVulnerabilities) } @@ -683,6 +742,9 @@ func sortResults(simpleJsonResults *formats.SimpleJsonResults) { if len(simpleJsonResults.SecretsViolations) > 0 { sortSourceCodeRow(simpleJsonResults.SecretsViolations) } + if len(simpleJsonResults.ServicesViolations) > 0 { + sortSourceCodeRow(simpleJsonResults.ServicesViolations) + } if len(simpleJsonResults.IacsViolations) > 0 { sortSourceCodeRow(simpleJsonResults.IacsViolations) } diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go index 9a84fd14b..d16f663da 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go @@ -6,6 +6,7 @@ import ( "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" @@ -487,3 +488,20 @@ func TestPrepareSimpleJsonJasIssues(t *testing.T) { }) } } + +func TestPrepareSimpleJsonServicesIssues(t *testing.T) { + rule := sarif.NewRule("service-rule") + result := sarifutils.CreateResultWithProperties("service finding", "service-rule", "warning", map[string]string{ + sarifutils.CWEPropertyKey: "CWE-918", + sarifutils.OutcomesPropertyKey: "publicly-exposed", + }, sarifutils.CreateLocation("service.yaml", 3, 4, 5, 6, "snippet")) + run := sarifutils.CreateRunWithDummyResults(result) + run.Tool.Driver.Rules = []*sarif.ReportingDescriptor{rule} + + out, err := PrepareSimpleJsonServicesIssues(true, false, run) + assert.NoError(t, err) + require.Len(t, out, 1) + assert.Equal(t, []string{"CWE-918"}, out[0].Cwe) + assert.Equal(t, "publicly-exposed", out[0].Outcomes) + assert.Equal(t, "service finding", out[0].Finding) +} diff --git a/utils/results/conversion/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go index a8b4ac9e9..c61645d44 100644 --- a/utils/results/conversion/summaryparser/summaryparser.go +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -5,6 +5,9 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" + "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" @@ -12,8 +15,6 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/severityutils" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/owenrumney/go-sarif/v3/pkg/report/v210/sarif" ) type CmdResultsSummaryConverter struct { @@ -222,25 +223,66 @@ func (sc *CmdResultsSummaryConverter) ParseViolations(violations violationutils. } return errors.Join(err, sc.parseScaViolations(violations), - sc.parseSecretsViolations(violations.Secrets), + sc.parseExposuresViolations(violations.Secrets, sc.ensureViolationsSecretsResults), + sc.parseExposuresViolations(violations.Services, sc.ensureViolationsServicesResults), sc.parseIacViolations(violations.Iac), sc.parseSastViolations(violations.Sast), ) } -func (sc *CmdResultsSummaryConverter) ParseSecrets(secrets ...[]*sarif.Run) (err error) { +func (sc *CmdResultsSummaryConverter) ParseExposuresScans(secrets, services []*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 err = sc.parseExposuresVulnerabilities(secrets, sc.ensureVulnerabilitiesSecretsResults); err != nil { + return + } + return sc.parseExposuresVulnerabilities(services, sc.ensureVulnerabilitiesServicesResults) +} +func (sc *CmdResultsSummaryConverter) ensureVulnerabilitiesSecretsResults() *formats.ResultSummary { if sc.currentScan.Vulnerabilities.SecretsResults == nil { sc.currentScan.Vulnerabilities.SecretsResults = &formats.ResultSummary{} } - return results.ForEachJasIssue(results.CollectRuns(secrets...), sc.entitledForJas, sc.getJasHandler(jasutils.Secrets)) + return sc.currentScan.Vulnerabilities.SecretsResults +} + +func (sc *CmdResultsSummaryConverter) ensureVulnerabilitiesServicesResults() *formats.ResultSummary { + if sc.currentScan.Vulnerabilities.ServicesResults == nil { + sc.currentScan.Vulnerabilities.ServicesResults = &formats.ResultSummary{} + } + return sc.currentScan.Vulnerabilities.ServicesResults +} + +func (sc *CmdResultsSummaryConverter) ensureViolationsSecretsResults() *formats.ResultSummary { + if sc.currentScan.Violations.SecretsResults == nil { + sc.currentScan.Violations.SecretsResults = &formats.ResultSummary{} + } + return sc.currentScan.Violations.SecretsResults +} + +func (sc *CmdResultsSummaryConverter) ensureViolationsServicesResults() *formats.ResultSummary { + if sc.currentScan.Violations.ServicesResults == nil { + sc.currentScan.Violations.ServicesResults = &formats.ResultSummary{} + } + return sc.currentScan.Violations.ServicesResults +} + +func (sc *CmdResultsSummaryConverter) parseExposuresVulnerabilities(runs []*sarif.Run, getSummary func() *formats.ResultSummary) (err error) { + if len(runs) == 0 { + return + } + return results.ForEachJasIssue(runs, sc.entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) error { + resultStatus := formats.NoStatus + if tokenStatus := sarifutils.GetResultPropertyTokenValidation(result); tokenStatus != "" { + resultStatus = tokenStatus + } + countJasIssues(getSummary(), location, severity, resultStatus) + return nil + }) } func (sc *CmdResultsSummaryConverter) ParseIacs(iacs ...[]*sarif.Run) (err error) { @@ -321,20 +363,17 @@ func countJasIssues(count *formats.ResultSummary, location *sarif.Location, seve (*count)[severity.String()][resultStatus] += 1 } -func (sc *CmdResultsSummaryConverter) parseSecretsViolations(secretsViolations []violationutils.JasViolation) (err error) { - if err = sc.validateBeforeParse(); err != nil || sc.currentScan.Violations == nil { +func (sc *CmdResultsSummaryConverter) parseExposuresViolations(violations []violationutils.JasViolation, getSummary func() *formats.ResultSummary) (err error) { + if err = sc.validateBeforeParse(); err != nil || sc.currentScan.Violations == nil || len(violations) == 0 { return } - if sc.currentScan.Violations.SecretsResults == nil { - sc.currentScan.Violations.SecretsResults = &formats.ResultSummary{} - } - for _, secretViolation := range secretsViolations { + for _, violation := range violations { status := formats.NoStatus - if tokenStatus := sarifutils.GetResultPropertyTokenValidation(secretViolation.Result); tokenStatus != "" { + if tokenStatus := sarifutils.GetResultPropertyTokenValidation(violation.Result); tokenStatus != "" { status = tokenStatus } - sc.currentScan.Violations.Watches = utils.UniqueUnion(sc.currentScan.Violations.Watches, secretViolation.Watch) - countJasIssues(sc.currentScan.Violations.SecretsResults, secretViolation.Location, secretViolation.Severity, status) + sc.currentScan.Violations.Watches = utils.UniqueUnion(sc.currentScan.Violations.Watches, violation.Watch) + countJasIssues(getSummary(), violation.Location, violation.Severity, status) } return } diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go index 1eb3e3348..126803b1b 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -44,6 +44,8 @@ func (tc *CmdResultsTableConverter) Get() (formats.ResultsTables, error) { OperationalRiskViolationsTable: formats.ConvertToOperationalRiskViolationTableRow(simpleJsonFormat.OperationalRiskViolations), SecretsVulnerabilitiesTable: formats.ConvertToSecretsTableRow(simpleJsonFormat.SecretsVulnerabilities), SecretsViolationsTable: formats.ConvertToSecretsTableRow(simpleJsonFormat.SecretsViolations), + ServicesVulnerabilitiesTable: formats.ConvertToServicesTableRow(simpleJsonFormat.ServicesVulnerabilities), + ServicesViolationsTable: formats.ConvertToServicesTableRow(simpleJsonFormat.ServicesViolations), IacVulnerabilitiesTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.IacsVulnerabilities), IacViolationsTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.IacsViolations), SastVulnerabilitiesTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.SastVulnerabilities), @@ -80,8 +82,8 @@ func (tc *CmdResultsTableConverter) ParseViolations(violations violationutils.Vi return tc.simpleJsonConvertor.ParseViolations(violations) } -func (tc *CmdResultsTableConverter) ParseSecrets(secrets ...[]*sarif.Run) (err error) { - return tc.simpleJsonConvertor.ParseSecrets(secrets...) +func (tc *CmdResultsTableConverter) ParseExposuresScans(secrets, services []*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseExposuresScans(secrets, services) } func (tc *CmdResultsTableConverter) ParseIacs(iacs ...[]*sarif.Run) (err error) { diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go index 8fc815032..7ebafc5f3 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -255,6 +255,9 @@ func (rw *ResultsWriter) printTables() (err error) { if err = rw.printJasTablesIfNeeded(tableContent, utils.SecretsScan, jasutils.Secrets); err != nil { return } + if err = rw.printJasTablesIfNeeded(tableContent, utils.ServicesScan, jasutils.Services); err != nil { + return + } 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.") } @@ -393,14 +396,8 @@ func PrintJasTable(tables formats.ResultsTables, entitledForJas bool, scanType j // Space before the tables log.Output() switch scanType { - case jasutils.Secrets: - if violations { - return coreutils.PrintTable(tables.SecretsViolationsTable, "Secret Violations", - "✨ No violations were found ✨", false) - } else { - return coreutils.PrintTable(tables.SecretsVulnerabilitiesTable, "Secrets Detection", - "✨ No secrets were found ✨", false) - } + case jasutils.Secrets, jasutils.Services: + return printExposuresScanTable(tables, scanType, violations) case jasutils.IaC: if violations { return coreutils.PrintTable(tables.IacViolationsTable, "Infrastructure as Code Violations", @@ -427,6 +424,22 @@ func PrintJasTable(tables formats.ResultsTables, entitledForJas bool, scanType j return nil } +func printExposuresScanTable(tables formats.ResultsTables, scanType jasutils.JasScanType, violations bool) error { + switch scanType { + case jasutils.Secrets: + if violations { + return coreutils.PrintTable(tables.SecretsViolationsTable, "Secret Violations", "✨ No violations were found ✨", false) + } + return coreutils.PrintTable(tables.SecretsVulnerabilitiesTable, "Secrets Detection", "✨ No secrets were found ✨", false) + case jasutils.Services: + if violations { + return coreutils.PrintTable(tables.ServicesViolationsTable, "Services Violations", "✨ No violations were found ✨", false) + } + return coreutils.PrintTable(tables.ServicesVulnerabilitiesTable, "Services Detection", "✨ No services were found ✨", false) + } + return nil +} + func WriteJsonResults(results *results.SecurityCommandResults) (resultsPath string, err error) { out, err := fileutils.CreateTempFile() if errorutils.CheckError(err) != nil { diff --git a/utils/results/output/securityJobSummary.go b/utils/results/output/securityJobSummary.go index 05f742a0c..1eaf286c7 100644 --- a/utils/results/output/securityJobSummary.go +++ b/utils/results/output/securityJobSummary.go @@ -626,6 +626,11 @@ func getResultsTypesSummaryString(index commandsummary.Index, violations bool, s content += TabTag.Format(fmt.Sprintf("%d %s", count, formats.SecretsResult.String())) } } + if summary.ServicesResults != nil { + if count := summary.GetTotal(formats.ServicesResult); count > 0 { + content += TabTag.Format(fmt.Sprintf("%d %s", count, formats.ServicesResult.String())) + } + } if summary.SastResults != nil { if count := summary.GetTotal(formats.SastResult); count > 0 { content += TabTag.Format(fmt.Sprintf("%d %s", count, formats.SastResult.String())) diff --git a/utils/results/results.go b/utils/results/results.go index 8cb83ef0c..25be1e591 100644 --- a/utils/results/results.go +++ b/utils/results/results.go @@ -29,6 +29,7 @@ const ( CmdStepSecrets = "Secret Detection Scan" CmdStepSast = "Static Application Security Testing (SAST)" CmdStepMaliciousCode = "Malicious Code" + CmdStepServices = "Services Scan" CmdStepViolations = "Violations Reporting" ) @@ -64,8 +65,9 @@ type ResultsMetaData struct { } type Entitlements struct { - Jas bool `json:"jas"` - SnippetDetection bool `json:"snippet_detection"` + Jas bool `json:"jas"` + SnippetDetection bool `json:"snippet_detection"` + ServicesDetection bool `json:"services_detection"` } // We have three types of results: vulnerabilities, violations and licenses. @@ -91,6 +93,8 @@ type ResultContext struct { IncludeSbom bool `json:"include_sbom,omitempty"` // If requested, the results will include snippet detection IncludeSnippetDetection bool `json:"include_snippet_detection,omitempty"` + // If requested, the results will include services detection + IncludeServicesDetection bool `json:"include_services_detection,omitempty"` // The active watches defined on the project_key and git_repository values above that were fetched from the platform PlatformWatches *xrayApi.ResourcesWatchesBody `json:"platform_watches,omitempty"` } @@ -107,6 +111,7 @@ type ResultsStatus struct { IacScanStatusCode *int `json:"iac,omitempty"` SastScanStatusCode *int `json:"sast,omitempty"` MaliciousScanStatusCode *int `json:"malicious_code,omitempty"` + ServicesScanStatusCode *int `json:"services,omitempty"` ViolationsStatusCode *int `json:"violations,omitempty"` } @@ -126,6 +131,8 @@ func (status *ResultsStatus) IsScanFailed(step SecurityCommandStep) bool { return isScanFailed(status.SastScanStatusCode) case CmdStepMaliciousCode: return isScanFailed(status.MaliciousScanStatusCode) + case CmdStepServices: + return isScanFailed(status.ServicesScanStatusCode) case CmdStepViolations: return isScanFailed(status.ViolationsStatusCode) } @@ -166,6 +173,10 @@ func (status *ResultsStatus) UpdateStatus(step SecurityCommandStep, statusCode * if shouldUpdateStatus(status.MaliciousScanStatusCode, statusCode) { status.MaliciousScanStatusCode = statusCode } + case CmdStepServices: + if shouldUpdateStatus(status.ServicesScanStatusCode, statusCode) { + status.ServicesScanStatusCode = statusCode + } case CmdStepViolations: if shouldUpdateStatus(status.ViolationsStatusCode, statusCode) { status.ViolationsStatusCode = statusCode @@ -214,6 +225,7 @@ type JasScanResults struct { IacScanResults []*sarif.Run `json:"iac,omitempty"` SastScanResults []*sarif.Run `json:"sast,omitempty"` MaliciousScanResults []*sarif.Run `json:"malicious_code,omitempty"` + ServicesScanResults []*sarif.Run `json:"services,omitempty"` } type ScanTarget struct { @@ -271,6 +283,11 @@ func (r *SecurityCommandResults) SetEntitledForSnippetDetection(entitledForSnipp return r } +func (r *SecurityCommandResults) SetEntitledForServicesDetection(entitledForServicesDetection bool) *SecurityCommandResults { + r.Entitlements.ServicesDetection = entitledForServicesDetection + return r +} + func (r *SecurityCommandResults) SetSecretValidation(secretValidation bool) *SecurityCommandResults { r.SecretValidation = secretValidation return r @@ -442,6 +459,7 @@ func (r *SecurityCommandResults) GetStatusCodes() ResultsStatus { status.UpdateStatus(CmdStepIaC, targetResults.ResultsStatus.IacScanStatusCode) status.UpdateStatus(CmdStepSast, targetResults.ResultsStatus.SastScanStatusCode) status.UpdateStatus(CmdStepMaliciousCode, targetResults.ResultsStatus.MaliciousScanStatusCode) + status.UpdateStatus(CmdStepServices, targetResults.ResultsStatus.ServicesScanStatusCode) status.UpdateStatus(CmdStepViolations, targetResults.ResultsStatus.ViolationsStatusCode) } return status @@ -608,6 +626,12 @@ func (sr *TargetResults) AddJasScanResults(scanType jasutils.JasScanType, vulner if sr.JasResults != nil { sr.JasResults.JasVulnerabilities.MaliciousScanResults = append(sr.JasResults.JasVulnerabilities.MaliciousScanResults, vulnerabilitiesRuns...) } + case jasutils.Services: + sr.ResultsStatus.UpdateStatus(CmdStepServices, &exitCode) + if sr.JasResults != nil { + sr.JasResults.JasVulnerabilities.ServicesScanResults = append(sr.JasResults.JasVulnerabilities.ServicesScanResults, vulnerabilitiesRuns...) + sr.JasResults.JasViolations.ServicesScanResults = append(sr.JasResults.JasViolations.ServicesScanResults, violationsRuns...) + } } } @@ -694,6 +718,8 @@ func (jsr *JasScansResults) GetVulnerabilitiesResults(scanType jasutils.JasScanT return jsr.JasVulnerabilities.SastScanResults case jasutils.MaliciousCode: return jsr.JasVulnerabilities.MaliciousScanResults + case jasutils.Services: + return jsr.JasVulnerabilities.ServicesScanResults } return } @@ -706,6 +732,8 @@ func (jsr *JasScansResults) GetViolationsResults(scanType jasutils.JasScanType) return jsr.JasViolations.IacScanResults case jasutils.Sast: return jsr.JasViolations.SastScanResults + case jasutils.Services: + return jsr.JasViolations.ServicesScanResults } return } diff --git a/utils/utils.go b/utils/utils.go index 467895696..1746966b9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -71,6 +71,7 @@ const ( SecretsScan SubScanType = "secrets" SecretTokenValidationScan SubScanType = "secrets_token_validation" MaliciousCodeScan SubScanType = "malicious_code" + ServicesScan SubScanType = "services" ) var subScanTypeToText = map[SubScanType]string{ @@ -80,6 +81,7 @@ var subScanTypeToText = map[SubScanType]string{ SastScan: "SAST", SecretsScan: "Secrets", MaliciousCodeScan: "Malicious Code", + ServicesScan: "Services", } func (subScan SubScanType) ToTextString() string { @@ -111,7 +113,7 @@ func (s CommandType) IsTargetBinary() bool { } func GetAllSupportedScans() []SubScanType { - return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan, SecretTokenValidationScan, MaliciousCodeScan} + return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan, SecretTokenValidationScan, MaliciousCodeScan, ServicesScan} } // IsScanRequested returns true if the scan is requested, otherwise false. If requestedScans is empty, all scans are considered requested. @@ -119,8 +121,7 @@ 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 + if subScan == MaliciousCodeScan || subScan == ServicesScan { return slices.Contains(requestedScans, subScan) } return len(requestedScans) == 0 || slices.Contains(requestedScans, subScan) @@ -130,7 +131,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, ServicesScan, requestedScans...) } func getScanFindingName(scanType SubScanType) string { diff --git a/utils/xsc/configprofile_test.go b/utils/xsc/configprofile_test.go index d4950e04d..d44a884d0 100644 --- a/utils/xsc/configprofile_test.go +++ b/utils/xsc/configprofile_test.go @@ -3,9 +3,10 @@ package xsc import ( "testing" - "github.com/jfrog/jfrog-cli-security/tests/validations" "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/stretchr/testify/assert" + + "github.com/jfrog/jfrog-cli-security/tests/validations" ) const ( @@ -87,7 +88,6 @@ func getComparisonConfigProfile() *services.ConfigProfile { ProfileName: "default-profile", GeneralConfig: services.GeneralConfig{ ScannersDownloadPath: "https://repo.example.com/releases", - GeneralExcludePatterns: []string{"*.log*", "*.tmp*"}, FailUponAnyScannerError: true, }, FrogbotConfig: services.FrogbotConfig{ @@ -128,6 +128,9 @@ func getComparisonConfigProfile() *services.ConfigProfile { EnableIacScan: true, ExcludePatterns: []string{"*.tfstate"}, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }, },