diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 6a7128b74..1d3b8e4e5 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -58,12 +58,14 @@ const ( Iac = "iac" Sast = "sast" Secrets = "secrets" + Services = "services" WithoutCA = "without-contextual-analysis" auditSca = auditPrefix + Sca auditIac = auditPrefix + Iac auditSast = auditPrefix + Sast auditSecrets = auditPrefix + Secrets + auditServices = auditPrefix + Services auditWithoutCA = auditPrefix + WithoutCA binarySca = scanPrefix + Sca binarySecrets = scanPrefix + Secrets @@ -210,7 +212,7 @@ var commandFlags = map[string][]string{ Url, XrayUrl, user, password, accessToken, ServerId, InsecureTls, scanProjectKey, Watches, RepoPath, Snippet, 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, + auditSca, auditIac, auditSast, auditSecrets, auditServices, auditWithoutCA, SecretValidation, ScanVuln, OutputDir, SkipAutoInstall, AllowPartialResults, MaxTreeDepth, StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, UploadRtRepoPath, UseIncludedBuilds, AddSastRules, }, UploadCdx: { @@ -223,7 +225,7 @@ var commandFlags = map[string][]string{ scanProjectKey, Watches, Snippet, ScanVuln, Fail, // Scan params Threads, ExclusionsAudit, WorkingDirs, Workspace, - auditSca, auditIac, auditSast, auditSecrets, auditWithoutCA, SecretValidation, Sbom, UseConfigProfile, + auditSca, auditIac, auditSast, auditSecrets, auditServices, auditWithoutCA, SecretValidation, Sbom, UseConfigProfile, // Output params Licenses, OutputFormat, ExtendedTable, OutputDir, UploadRtRepoPath, // Scan Logic params @@ -359,10 +361,11 @@ var flagsMap = map[string]components.Flag{ binarySca: components.NewBoolFlag(Sca, fmt.Sprintf("Selective scanners mode: Execute SCA (Software Composition Analysis) sub-scan. Use --%s to run both SCA and Contextual Analysis. Use --%s --%s to to run SCA. Can be combined with --%s.", Sca, Sca, WithoutCA, Secrets)), binarySecrets: components.NewBoolFlag(Secrets, fmt.Sprintf("Selective scanners mode: Execute Secrets sub-scan. Can be combined with --%s.", Sca)), binaryWithoutCA: components.NewBoolFlag(WithoutCA, fmt.Sprintf("Selective scanners mode: Disable Contextual Analysis scanner after SCA. Relevant only with --%s flag.", Sca)), - auditSca: components.NewBoolFlag(Sca, fmt.Sprintf("Selective scanners mode: Execute SCA (Software Composition Analysis) sub-scan. Use --%s to run both SCA and Contextual Analysis. Use --%s --%s to to run SCA. Can be combined with --%s, --%s, --%s.", Sca, Sca, WithoutCA, Secrets, Sast, Iac)), - auditIac: components.NewBoolFlag(Iac, fmt.Sprintf("Selective scanners mode: Execute IaC sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Secrets, Sast)), - auditSast: components.NewBoolFlag(Sast, fmt.Sprintf("Selective scanners mode: Execute SAST sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Secrets, Iac)), - auditSecrets: components.NewBoolFlag(Secrets, fmt.Sprintf("Selective scanners mode: Execute Secrets sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Sast, Iac)), + auditSca: components.NewBoolFlag(Sca, fmt.Sprintf("Selective scanners mode: Execute SCA (Software Composition Analysis) sub-scan. Use --%s to run both SCA and Contextual Analysis. Use --%s --%s to to run SCA. Can be combined with --%s, --%s, --%s and --%s.", Sca, Sca, WithoutCA, Secrets, Sast, Iac, Services)), + auditIac: components.NewBoolFlag(Iac, fmt.Sprintf("Selective scanners mode: Execute IaC sub-scan. Can be combined with --%s, --%s, --%s and --%s", Sca, Secrets, Sast, Services)), + auditSast: components.NewBoolFlag(Sast, fmt.Sprintf("Selective scanners mode: Execute SAST sub-scan. Can be combined with --%s, --%s, --%s and --%s", Sca, Secrets, Iac, Services)), + auditSecrets: components.NewBoolFlag(Secrets, fmt.Sprintf("Selective scanners mode: Execute Secrets sub-scan. Can be combined with --%s, --%s, --%s and --%s", Sca, Sast, Iac, Services)), + auditServices: components.NewBoolFlag(Services, fmt.Sprintf("Selective scanners mode: Execute Services sub-scan. Can be combined with --%s, --%s, --%s and --%s", Sca, Secrets, Sast, Iac)), auditWithoutCA: components.NewBoolFlag(WithoutCA, fmt.Sprintf("Selective scanners mode: Disable Contextual Analysis scanner after SCA. Relevant only with --%s flag.", Sca)), SecretValidation: components.NewBoolFlag(SecretValidation, fmt.Sprintf("Selective scanners mode: Triggers token validation on found secrets. Relevant only with --%s flag.", Secrets)), diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index e760c9faa..f086ff383 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -690,6 +690,32 @@ func TestShouldGenerateSbom(t *testing.T) { scanResults: &results.TargetResults{ScanTarget: results.ScanTarget{CentralConfigModules: configProfileModulesWithoutSca}}, 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 { @@ -1320,14 +1346,15 @@ 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 diff --git a/go.mod b/go.mod index 9942dac4e..acfdd4734 100644 --- a/go.mod +++ b/go.mod @@ -157,7 +157,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-20260728132717-7306d020619b // 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 d59ab376e..ccad1a544 100644 --- a/go.sum +++ b/go.sum @@ -173,8 +173,6 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260722060859-50ee96befe5c h1:e github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260722060859-50ee96befe5c/go.mod h1:1vxzqW7jHBSuTNqO2vxEnhbniwq4dj5wveDuCMJX7Yo= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260724083402-361ba4a2ecdb h1:OW2W8ryV0FiCq1DKfRXPLwuB8OB+1wWebVG/sQPaF3Y= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260724083402-361ba4a2ecdb/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260722075451-613a6b6a7603 h1:gwxRPCQRsbwt8rIWQ6LDu09aH4I2F1DFYObBpx1KDHw= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260722075451-613a6b6a7603/go.mod h1:FHpjN1nTDoj96xd6obe27EOgGErqzU0rQgC96L3Ch9E= 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= @@ -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-20260728132717-7306d020619b h1:rh4paVkuikqXHTJ0NpRq/yhbhfW2BXzevV2NWGBCf7w= +github.com/orto17/jfrog-client-go v0.0.0-20260728132717-7306d020619b/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= diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index ab777371a..e047a8917 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -11,6 +11,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" @@ -89,6 +90,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..8d5030565 --- /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-scan" + servicesScanCommand = "svc" + 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, results.ScanTarget{Module: &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 87997ea44..7775c19aa 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)) @@ -253,6 +258,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: @@ -334,6 +341,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 d1c4b569d..3cf1a1c21 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/tests/testdata/other/configProfile/configProfileExample.json b/tests/testdata/other/configProfile/configProfileExample.json index 3596907ce..1fea0298a 100644 --- a/tests/testdata/other/configProfile/configProfileExample.json +++ b/tests/testdata/other/configProfile/configProfileExample.json @@ -42,6 +42,10 @@ "iac_scanner_config": { "enable_iac_scan": true, "exclude_patterns": ["*.tfstate"] + }, + "services_scanner_config": { + "enable_services_scan": true, + "exclude_patterns": ["**/build/**"] } } } diff --git a/tests/testdata/projects/jas/jas/services/workflow.yml b/tests/testdata/projects/jas/jas/services/workflow.yml new file mode 100644 index 000000000..e011b5b77 --- /dev/null +++ b/tests/testdata/projects/jas/jas/services/workflow.yml @@ -0,0 +1,76 @@ +name: Misconfigured CI Pipeline + +on: + pull_request_target: + types: [opened, synchronize] + push: + branches: [main] + +# Overly broad permissions +permissions: write-all + +env: + # Hardcoded credentials + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + DB_PASSWORD: "supersecret123!" + API_TOKEN: "ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + # Dangerous: checks out PR code under pull_request_target (script injection risk) + - name: Checkout PR code + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: true + + # Dangerous: running untrusted input directly in shell (script injection) + - name: Print PR title + run: echo "PR title is ${{ github.event.pull_request.title }}" + + # Dangerous: curl piped to bash from untrusted source + - name: Install dependencies + run: curl -fsSL https://example.com/install.sh | bash + + # Dangerous: unpinned third-party action (supply chain risk) + - name: Run some action + uses: some-org/some-action@main + + # Dangerous: secret exposed as plain env var and echoed + - name: Deploy + env: + SECRET_KEY: ${{ secrets.SECRET_KEY }} + run: | + echo "Deploying with key: $SECRET_KEY" + ./deploy.sh --token $SECRET_KEY + + # Dangerous: GITHUB_TOKEN with unnecessary write permissions used in script + - name: Comment on PR + run: | + curl -X POST \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d '{"body": "Build passed!"}' \ + https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments + + deploy-prod: + runs-on: ubuntu-latest + needs: build + # No environment protection rules + steps: + - name: Deploy to production + env: + # Hardcoded private key + PRIVATE_KEY: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA2a2rwplBQLF29amygykEMmYz0+Kcj3bKBp29Zz0mM9WXDdUk + FwSPBjRMTCCPjJWFRMFCqCwBTOgHzEMqCqMwRDXJFGMbIBEZQpj4l3R7EXAMPLE + -----END RSA PRIVATE KEY----- + DB_HOST: "prod-db.internal.example.com" + DB_USER: "admin" + DB_PASS: "prod_password_123" + run: ./scripts/deploy-prod.sh \ No newline at end of file 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 346f30cc8..050d9fb00 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 c8d80dfba..1adcad209 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"` } @@ -187,6 +189,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() } @@ -232,6 +237,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) } @@ -309,6 +317,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 58fad0e1f..4305f8ba2 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 24371de3b..fec43d4ef 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -68,8 +68,10 @@ 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 + // ParseSecrets(secrets ...[]*sarif.Run) error + // ParseServices(services ...[]*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 @@ -213,8 +215,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 ad3823f16..a3767e18c 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -29,6 +29,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" // Include directories property @@ -166,12 +167,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) @@ -182,7 +193,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), @@ -199,7 +210,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...) @@ -207,6 +218,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 1edd30bbe..aef025474 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) @@ -287,10 +291,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 { @@ -428,11 +432,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 } @@ -928,6 +939,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 cedfc397d..05d8d1389 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -56,6 +56,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, }, @@ -215,12 +216,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) @@ -355,21 +353,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 @@ -489,6 +517,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{ @@ -695,6 +751,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) } @@ -705,6 +764,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 72ef20781..2b2fc12cf 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" @@ -641,3 +642,20 @@ func TestPrepareSimpleJsonVulnerabilities_Technology(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 9fd9ec4c0..7c2613644 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 81bcfcccc..dfc67f5cd 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -45,6 +45,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), @@ -82,8 +84,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 90c9eb0b3..6c02183ba 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -256,6 +256,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.") } @@ -394,14 +397,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", @@ -428,6 +425,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 25a97db1a..9b1b32143 100644 --- a/utils/results/results.go +++ b/utils/results/results.go @@ -30,6 +30,7 @@ const ( CmdStepSecrets = "Secret Detection Scan" CmdStepSast = "Static Application Security Testing (SAST)" CmdStepMaliciousCode = "Malicious Code" + CmdStepServices = "Services Scan" CmdStepViolations = "Violations Reporting" ) @@ -129,6 +130,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"` } @@ -148,6 +150,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) } @@ -188,6 +192,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 @@ -235,6 +243,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 { @@ -673,6 +682,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 @@ -855,6 +865,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...) + } } } @@ -941,6 +957,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 } @@ -953,6 +971,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 d252156f7..0e260b126 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -73,6 +73,7 @@ const ( SecretsScan SubScanType = "secrets" SecretTokenValidationScan SubScanType = "secrets_token_validation" MaliciousCodeScan SubScanType = "malicious_code" + ServicesScan SubScanType = "services" ) var subScanTypeToText = map[SubScanType]string{ @@ -82,6 +83,7 @@ var subScanTypeToText = map[SubScanType]string{ SastScan: "SAST", SecretsScan: "Secrets", MaliciousCodeScan: "Malicious Code", + ServicesScan: "Services", } func (subScan SubScanType) ToTextString() string { @@ -113,7 +115,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. @@ -121,11 +123,10 @@ func IsScanRequested(cmdType CommandType, subScan SubScanType, centralConfigRequ if centralConfigRequestedParam != nil { return *centralConfigRequestedParam } - if cmdType.IsTargetBinary() && (subScan == IacScan || subScan == SastScan) { + if cmdType.IsTargetBinary() && (subScan == IacScan || subScan == SastScan || subScan == ServicesScan) { 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) diff --git a/utils/xsc/configprofile_test.go b/utils/xsc/configprofile_test.go index ed7e04253..d03bb4261 100644 --- a/utils/xsc/configprofile_test.go +++ b/utils/xsc/configprofile_test.go @@ -9,9 +9,10 @@ import ( "testing" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "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 ( @@ -187,6 +188,10 @@ func getComparisonConfigProfile() *services.ConfigProfile { EnableIacScan: true, ExcludePatterns: []string{"*.tfstate"}, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: true, + ExcludePatterns: []string{"**/build/**"}, + }, }, }, },