diff --git a/audit_test.go b/audit_test.go index 5b7683372..3a2662c30 100644 --- a/audit_test.go +++ b/audit_test.go @@ -513,9 +513,9 @@ func TestXrayAuditMultiProjects(t *testing.T) { } output := securityTests.PlatformCli.WithoutCredentials().RunCliCmdWithOutput(t, getAuditCmdArgs(params)...) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 43}, + Total: &validations.TotalCount{Vulnerabilities: 49}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 27, Sast: 1, Iac: 9, Secrets: 6}, + ValidateScan: &validations.ScanCount{Sca: 27, Sast: 1, Iac: 9, Secrets: 6, Services: 6}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 3, NotCovered: 22, NotApplicable: 2}, }, }) @@ -783,9 +783,9 @@ func TestXrayAuditJasSimpleJson(t *testing.T) { Format: format.SimpleJson, }) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 23}, + Total: &validations.TotalCount{Vulnerabilities: 29}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 7, Sast: 1, Iac: 9, Secrets: 6}, + ValidateScan: &validations.ScanCount{Sca: 7, Sast: 1, Iac: 9, Secrets: 6, Services: 6}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 3, Undetermined: 1, NotCovered: 1, NotApplicable: 2}, }, }) @@ -813,9 +813,9 @@ func TestXrayAuditJasSimpleJsonWithOneThread(t *testing.T) { Format: format.SimpleJson, }) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 23}, + Total: &validations.TotalCount{Vulnerabilities: 29}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 7, Sast: 1, Iac: 9, Secrets: 6}, + ValidateScan: &validations.ScanCount{Sca: 7, Sast: 1, Iac: 9, Secrets: 6, Services: 6}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 3, Undetermined: 1, NotCovered: 1, NotApplicable: 2}, }, }) @@ -922,7 +922,7 @@ func TestXrayAuditNotEntitledForJasWithXrayUrl(t *testing.T) { validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ Total: &validations.TotalCount{Vulnerabilities: 8}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 8, Sast: 0, Iac: 0, Secrets: 0}, + ValidateScan: &validations.ScanCount{Sca: 8, Sast: 0, Iac: 0, Secrets: 0, Services: 0}, }, }) } @@ -934,9 +934,9 @@ func TestXrayAuditJasSimpleJsonWithXrayUrl(t *testing.T) { Format: format.SimpleJson, }) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 24}, + Total: &validations.TotalCount{Vulnerabilities: 29}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 7, Sast: 1, Iac: 9, Secrets: 6}, + ValidateScan: &validations.ScanCount{Sca: 7, Sast: 1, Iac: 9, Secrets: 6, Services: 6}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 3, Undetermined: 1, NotCovered: 1, NotApplicable: 2}, }, }) @@ -951,9 +951,9 @@ func TestXrayAuditJasSimpleJsonWithCustomExclusions(t *testing.T) { Format: format.SimpleJson, }) validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 24}, + Total: &validations.TotalCount{Vulnerabilities: 29}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 7, Sast: 2, Iac: 9, Secrets: 6}, + ValidateScan: &validations.ScanCount{Sca: 7, Sast: 2, Iac: 9, Secrets: 6, Services: 6}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 3, Undetermined: 1, NotCovered: 1, NotApplicable: 2}, }, }) @@ -993,7 +993,7 @@ func testAuditCommandNewSca(t *testing.T, params auditCommandTestParams, project } params.WithStaticSca = true // No **/tests/** exclusion, we are scanning projects in the test resources path - params.CustomExclusion = []string{"*.git*", "*node_modules*", "*target*", "*venv*", "dist"} + params.CustomExclusion = []string{"*.git", "*node_modules*", "*target*", "*venv*", "dist"} // Configure a new server named "default" cleanUpHome := securityIntegrationTestUtils.UseTestHomeWithDefaultXrayConfig(t) if params.Threads <= 0 { @@ -1182,10 +1182,10 @@ func TestAuditNewScaCycloneDxPip(t *testing.T) { ) assert.NoError(t, err) validations.VerifyCycloneDxResults(t, output, validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 22, BomComponents: 1 /*root*/ + 2 /*components*/ + 5 /*files (secrets)*/}, + Total: &validations.TotalCount{Vulnerabilities: 28, BomComponents: 1 /*root*/ + 2 /*components*/ + 5 /*files (secrets)*/}, SbomComponents: &validations.SbomCount{Root: 1, Direct: 2}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sast: 2, Iac: 9, Secrets: 11}, + ValidateScan: &validations.ScanCount{Sast: 2, Iac: 9, Secrets: 11, Services: 6}, }, }) } diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 6a7128b74..a871cf017 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..cd367302d 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -711,6 +711,7 @@ func TestAuditWithConfigProfile(t *testing.T) { expectedCaNotCovered int expectedCaNotApplicable int expectedSastIssues int + expectedServicesIssues int expectedSecretsIssues int expectedIacIssues int }{ @@ -739,6 +740,9 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: false, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, @@ -770,6 +774,9 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: false, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, @@ -800,6 +807,9 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: false, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, @@ -831,10 +841,13 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: false, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, - expectedSecretsIssues: 16, + expectedSecretsIssues: 17, }, { name: "Secrets scanner is enabled with exclusions", @@ -862,10 +875,13 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: false, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, - expectedSecretsIssues: 7, + expectedSecretsIssues: 8, }, { name: "Enable only Sast scanner", @@ -892,6 +908,9 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: false, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, @@ -923,6 +942,9 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: false, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, @@ -953,6 +975,9 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: true, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, @@ -984,11 +1009,78 @@ func TestAuditWithConfigProfile(t *testing.T) { EnableIacScan: true, ExcludePatterns: []string{"**/*iac/gcp*/**"}, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: false, + }, }, }}, }, expectedIacIssues: 0, }, + { + name: "Enable only Services scanner", + testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "jas", "jas"), + configProfile: services.ConfigProfile{ + ProfileName: "only-services", + Modules: []services.Module{{ + ModuleId: 1, + ModuleName: "only-services-module", + PathFromRoot: ".", + ScanConfig: services.ScanConfig{ + ScaScannerConfig: services.ScaScannerConfig{ + EnableScaScan: false, + }, + ContextualAnalysisScannerConfig: services.CaScannerConfig{ + EnableCaScan: false, + }, + SastScannerConfig: services.SastScannerConfig{ + EnableSastScan: false, + }, + IacScannerConfig: services.IacScannerConfig{ + EnableIacScan: false, + }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: true, + }, + }, + }}, + }, + expectedServicesIssues: 6, + }, + { + name: "Services scanner is enabled with exclusions", + testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "jas", "jas"), + configProfile: services.ConfigProfile{ + ProfileName: "services-with-exclusions", + Modules: []services.Module{{ + ModuleId: 1, + ModuleName: "services-with-exclusions-module", + PathFromRoot: ".", + ScanConfig: services.ScanConfig{ + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: true, + ExcludePatterns: []string{"**/*.git*/**"}, + }, + ScaScannerConfig: services.ScaScannerConfig{ + EnableScaScan: false, + }, + ContextualAnalysisScannerConfig: services.CaScannerConfig{ + EnableCaScan: false, + }, + SastScannerConfig: services.SastScannerConfig{ + EnableSastScan: false, + }, + SecretsScannerConfig: services.SecretsScannerConfig{ + EnableSecretsScan: false, + }, + IacScannerConfig: services.IacScannerConfig{ + EnableIacScan: false, + }, + }, + }}, + }, + expectedServicesIssues: 0, + }, { name: "Enable All Scanners", testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "jas", "jas"), @@ -1014,13 +1106,17 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: true, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: true, + }, }, }}, }, - expectedSastIssues: 2, - expectedSecretsIssues: 16, - expectedIacIssues: 9, - expectedCaNotCovered: 15, + expectedServicesIssues: 6, + expectedSastIssues: 2, + expectedSecretsIssues: 17, + expectedIacIssues: 9, + expectedCaNotCovered: 15, }, { name: "All scanners enabled but some with exclude patterns", @@ -1049,19 +1145,23 @@ func TestAuditWithConfigProfile(t *testing.T) { IacScannerConfig: services.IacScannerConfig{ EnableIacScan: true, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: true, + }, }, }}, }, - expectedSastIssues: 0, - expectedSecretsIssues: 7, - expectedIacIssues: 9, - expectedCaNotCovered: 15, + expectedServicesIssues: 6, + expectedSastIssues: 0, + expectedSecretsIssues: 8, + expectedIacIssues: 9, + expectedCaNotCovered: 15, }, } assert.NoError(t, securityTestUtils.PrepareAnalyzerManagerResource()) for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { - mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion, XscVersion: services.ConfigProfileMinXscVersion}) + mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: services.ConfigProfileNewSchemaMinXrayVersion, XscVersion: services.ConfigProfileMinXscVersion}) defer mockServer.Close() tempProjectPath, cleanUp := securityTestUtils.CreateTestProjectInTempDir(t, testcase.testDirPath) @@ -1101,9 +1201,9 @@ func TestAuditWithConfigProfile(t *testing.T) { validations.ValidateCommandSummaryOutput(t, validations.ValidationParams{ Actual: summary, ExactResultsMatch: true, - Total: &validations.TotalCount{Vulnerabilities: testcase.expectedSastIssues + testcase.expectedSecretsIssues + testcase.expectedIacIssues + scaResultsCount}, + Total: &validations.TotalCount{Vulnerabilities: testcase.expectedSastIssues + testcase.expectedSecretsIssues + testcase.expectedIacIssues + scaResultsCount + testcase.expectedServicesIssues}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: scaResultsCount, Sast: testcase.expectedSastIssues, Secrets: testcase.expectedSecretsIssues, Iac: testcase.expectedIacIssues}, + ValidateScan: &validations.ScanCount{Sca: scaResultsCount, Sast: testcase.expectedSastIssues, Secrets: testcase.expectedSecretsIssues, Iac: testcase.expectedIacIssues, Services: testcase.expectedServicesIssues}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: testcase.expectedCaApplicable, NotApplicable: testcase.expectedCaNotApplicable, NotCovered: testcase.expectedCaNotCovered, Undetermined: testcase.expectedCaUndetermined}, }, }) diff --git a/git_test.go b/git_test.go index 524f9ee7e..bbf85ab61 100644 --- a/git_test.go +++ b/git_test.go @@ -169,12 +169,12 @@ func TestGitAuditStaticScaSimpleJson(t *testing.T) { }, xrayVersion, "", "One or more of the detected violations are configured to fail the build that including them", validations.ValidationParams{ - Total: &validations.TotalCount{Licenses: 85, Violations: 12, Vulnerabilities: 16}, + Total: &validations.TotalCount{Licenses: 85, Violations: 18, Vulnerabilities: 22}, Vulnerabilities: &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 8, Sast: 2, Iac: 4, Secrets: 2}, + ValidateScan: &validations.ScanCount{Sca: 8, Sast: 2, Iac: 4, Secrets: 2, Services: 6}, }, // Check that we have at least one violation for each scan type. (IAC is not supported yet) - Violations: &validations.ViolationCount{ValidateScan: &validations.ScanCount{Sca: 8, Sast: 2, Secrets: 2}}, + Violations: &validations.ViolationCount{ValidateScan: &validations.ScanCount{Sca: 8, Sast: 2, Secrets: 2, Services: 6}}, }, ) } @@ -202,9 +202,9 @@ func TestGitAuditViolationsWithIgnoreRule(t *testing.T) { }, xrayVersion, xscVersion, "One or more of the detected violations are configured to fail the build that including them", validations.ValidationParams{ - Total: &validations.TotalCount{Licenses: 3, Violations: 12, Vulnerabilities: 12}, + Total: &validations.TotalCount{Licenses: 3, Violations: 18, Vulnerabilities: 18}, // Check that we have at least one violation for each scan type. (IAC is not supported yet) - Violations: &validations.ViolationCount{ValidateScan: &validations.ScanCount{Sca: 1, Sast: 1, Secrets: 1}}, + Violations: &validations.ViolationCount{ValidateScan: &validations.ScanCount{Sca: 1, Sast: 1, Secrets: 1, Services: 6}}, }, ) @@ -217,7 +217,7 @@ func TestGitAuditViolationsWithIgnoreRule(t *testing.T) { defer cleanUpCveIgnoreRule() cleanUpExposureIgnoreRule := securityTestUtils.CreateTestIgnoreRules(t, "security cli tests - Exposure ignore rule", xrayUtils.IgnoreFilters{ GitRepositories: []string{xscutils.GetGitRepoUrlKey(dummyCloneUrl)}, - Exposures: &xrayUtils.ExposuresFilterName{Categories: []xrayUtils.ExposureType{xrayUtils.SecretExposureType, xrayUtils.IacExposureType}}, + Exposures: &xrayUtils.ExposuresFilterName{Categories: []xrayUtils.ExposureType{xrayUtils.SecretExposureType, xrayUtils.IacExposureType, xrayUtils.ServicesExposureType}}, Watches: []string{watchName}, }) defer cleanUpExposureIgnoreRule() @@ -260,11 +260,11 @@ func TestGitAuditJasViolationsProjectKeySimpleJson(t *testing.T) { gitAuditCommandTestParams{auditCommandTestParams: auditCommandTestParams{Format: format.SimpleJson, ProjectKey: *securityTests.JfrogTestProjectKey, WithVuln: true}}, xrayVersion, xscVersion, policy.NewFailBuildError().Error(), validations.ValidationParams{ - Total: &validations.TotalCount{Vulnerabilities: 12, Violations: 12}, + Total: &validations.TotalCount{Vulnerabilities: 18, Violations: 18}, // Validate we have vulnerabilities for each scan type (to make sure if violations are issue when fail or not related and issue from other places before) - Vulnerabilities: &validations.VulnerabilityCount{ValidateScan: &validations.ScanCount{Sca: 1, Sast: 1, Secrets: 1}}, + Vulnerabilities: &validations.VulnerabilityCount{ValidateScan: &validations.ScanCount{Sca: 1, Sast: 1, Secrets: 1, Services: 6}}, // Check that we have at least one violation for each scan type. (IAC is not supported yet) - Violations: &validations.ViolationCount{ValidateScan: &validations.ScanCount{Sca: 1, Sast: 1, Secrets: 1}}, + Violations: &validations.ViolationCount{ValidateScan: &validations.ScanCount{Sca: 1, Sast: 1, Secrets: 1, Services: 6}}, }, ) } @@ -292,6 +292,8 @@ func TestGitAuditJasSkipNotApplicableCvesViolations(t *testing.T) { } }() + onlyScan := []securityUtils.SubScanType{securityUtils.SecretsScan, securityUtils.ScaScan, securityUtils.SastScan, securityUtils.IacScan, securityUtils.ServicesScan} + // Run the git audit command and verify violations are reported to the platform. createTestProjectRunGitAuditAndValidate(t, projectPath, gitAuditCommandTestParams{ @@ -299,7 +301,7 @@ func TestGitAuditJasSkipNotApplicableCvesViolations(t *testing.T) { Format: format.SimpleJson, Watches: []string{watchName}, DisableFailOnFailedBuildFlag: true, - OnlyScan: []securityUtils.SubScanType{securityUtils.SecretsScan, securityUtils.ScaScan, securityUtils.SastScan, securityUtils.IacScan}, + OnlyScan: onlyScan, ValidateSecrets: true, }, OverrideRepoCloneUrl: dummyCloneUrl, @@ -308,7 +310,7 @@ func TestGitAuditJasSkipNotApplicableCvesViolations(t *testing.T) { xrayVersion, xscVersion, "", validations.ValidationParams{ Violations: &validations.ViolationCount{ - ValidateScan: &validations.ScanCount{Sca: 70, Sast: 5, Secrets: 2}, + ValidateScan: &validations.ScanCount{Sca: 70, Sast: 5, Secrets: 6, Services: 6}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{NotApplicable: 61, NotCovered: 8, MissingContext: 1, Inactive: 1}, }, ExactResultsMatch: true, @@ -335,7 +337,7 @@ func TestGitAuditJasSkipNotApplicableCvesViolations(t *testing.T) { Watches: []string{skipWatchName}, DisableFailOnFailedBuildFlag: true, ValidateSecrets: true, - OnlyScan: []securityUtils.SubScanType{securityUtils.SecretsScan, securityUtils.ScaScan, securityUtils.SastScan, securityUtils.IacScan}, + OnlyScan: onlyScan, }, OverrideRepoCloneUrl: dummyCloneUrl, OverrideCommitMsg: getDummyCommitMsg("git-audit-jas-skip-not-applicable-cves-violations-after"), @@ -343,7 +345,7 @@ func TestGitAuditJasSkipNotApplicableCvesViolations(t *testing.T) { xrayVersion, xscVersion, "", validations.ValidationParams{ Violations: &validations.ViolationCount{ - ValidateScan: &validations.ScanCount{Sca: 9, Sast: 5, Secrets: 2}, + ValidateScan: &validations.ScanCount{Sca: 9, Sast: 5, Secrets: 6, Services: 6}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{NotCovered: 8, MissingContext: 1, Inactive: 1}, }, ExactResultsMatch: true, diff --git a/go.mod b/go.mod index 50cc60101..47897a8d3 100644 --- a/go.mod +++ b/go.mod @@ -158,7 +158,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-20260729095022-f0072c4ddd4b // 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 ff78e6031..d8c09f090 100644 --- a/go.sum +++ b/go.sum @@ -173,8 +173,6 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0 h1:D github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260728121041-2227ac7420a0/go.mod h1:1vxzqW7jHBSuTNqO2vxEnhbniwq4dj5wveDuCMJX7Yo= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e h1:K0IK3w5a5h6SIi9yoOJ6a7DL+kuFjs5acypOxKyT2OM= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260728123939-34b27f070f2e/go.mod h1:MygQx8pekgPCXyXnejIAVG9S4ImGcDFmcfRPUug/0d0= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260728135425-4a7f34efb305 h1:D86yACKH94D9sJ0XcBK4ioOVSHDec667y22rXondYN0= -github.com/jfrog/jfrog-client-go v1.55.1-0.20260728135425-4a7f34efb305/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-20260729095022-f0072c4ddd4b h1:sxMPb46s61Wzde900lemIxsIWocktOi9vxmjGeN1ZAc= +github.com/orto17/jfrog-client-go v0.0.0-20260729095022-f0072c4ddd4b/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/common_test.go b/jas/common_test.go index f6949ccb8..758f9024c 100644 --- a/jas/common_test.go +++ b/jas/common_test.go @@ -299,8 +299,8 @@ func TestConvertToFilesExcludePatterns(t *testing.T) { expectedOutput: []string(nil), }, { - excludePatterns: []string{"*.git*", "*node_modules*", "*target*", "*venv*", "*test*"}, - expectedOutput: []string{"**/*.git*/**", "**/*node_modules*/**", "**/*target*/**", "**/*test*/**", "**/*venv*/**"}, + excludePatterns: []string{"*.git", "*node_modules*", "*target*", "*venv*", "*test*"}, + expectedOutput: []string{"**/*.git/**", "**/*node_modules*/**", "**/*target*/**", "**/*test*/**", "**/*venv*/**"}, }, } @@ -715,6 +715,7 @@ func TestShouldSkipScannerByConfigProfile(t *testing.T) { ScaScannerConfig: xscServices.ScaScannerConfig{EnableScaScan: true}, ContextualAnalysisScannerConfig: xscServices.CaScannerConfig{EnableCaScan: true}, IacScannerConfig: xscServices.IacScannerConfig{EnableIacScan: true}, + ServicesScannerConfig: xscServices.ServicesScannerConfig{EnableServicesScan: true}, SecretsScannerConfig: xscServices.SecretsScannerConfig{EnableSecretsScan: true}, SastScannerConfig: xscServices.SastScannerConfig{EnableSastScan: true}, }, @@ -745,6 +746,14 @@ func TestShouldSkipScannerByConfigProfile(t *testing.T) { jasType: jasutils.Secrets, expected: false, }, + { + name: "Services scan enabled - should not skip", + target: results.ScanTarget{Target: "/project", CentralConfigModules: enabledProfile.Modules}, + profile: enabledProfile, + scanType: utils.ServicesScan, + jasType: jasutils.Services, + expected: false, + }, { name: "Scan disabled - should skip", target: results.ScanTarget{Target: "/project", CentralConfigModules: []xscServices.Module{{ @@ -764,6 +773,25 @@ func TestShouldSkipScannerByConfigProfile(t *testing.T) { jasType: jasutils.Secrets, expected: true, }, + { + name: "Services scan disabled - should skip", + target: results.ScanTarget{Target: "/project", CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ServicesScannerConfig: xscServices.ServicesScannerConfig{EnableServicesScan: false}, + }, + }}}, + profile: &xscServices.ConfigProfile{ + ProfileName: "disabled-services-profile", + Modules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ServicesScannerConfig: xscServices.ServicesScannerConfig{EnableServicesScan: false}, + }, + }}, + }, + scanType: utils.ServicesScan, + jasType: jasutils.Services, + expected: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/jas/iac/iacscanner.go b/jas/iac/iacscanner.go index 05f802441..70a701673 100644 --- a/jas/iac/iacscanner.go +++ b/jas/iac/iacscanner.go @@ -18,7 +18,7 @@ import ( const ( iacScannerType = "iac-scan-modules" iacScanCommand = "iac" - iacDocsUrlSuffix = "misconfigurations-scans-1" + iacDocsUrlSuffix = "misconfigurations-scans-1#iac-scans--infrastructure-as-code-iac-security-analysis" ) type IacScanManager struct { diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index ab777371a..ac6d31362 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" + servicesScan "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" @@ -85,6 +86,11 @@ func AddJasScannersTasks(params JasRunnerParams) error { errorsCollection = errors.Join(errorsCollection, generalError) } + if generalError := addJasScanTaskIfNeeded(params, utils.ServicesScan, runServicesScan(¶ms)); generalError != nil { + // 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 := addJasScanTaskIfNeeded(params, utils.SastScan, runSastScan(¶ms)); generalError != nil { // 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) @@ -149,6 +155,29 @@ func runSecretsScan(params *JasRunnerParams) parallel.TaskFunc { } } +func runServicesScan(params *JasRunnerParams) parallel.TaskFunc { + return func(threadId int) (err error) { + defer func() { + params.Runner.JasScannersWg.Done() + }() + servicesScanParams := servicesScan.ServicesScanParams{ + ThreadId: threadId, + TargetCount: params.TargetCount, + ResultsToCompare: getSourceRunsToCompare(params, jasutils.Services), + Target: params.ScanResults.ScanTarget, + } + vulnerabilitiesResults, violationsResults, err := servicesScan.RunServicesScan(params.Scanner, servicesScanParams) + params.Runner.ResultsMu.Lock() + defer params.Runner.ResultsMu.Unlock() + // We first add the scan results and only then check for errors, so we can store the exit code in order to report it in the end + 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 runIacScan(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..23036ac2e --- /dev/null +++ b/jas/services/servicesscanner.go @@ -0,0 +1,155 @@ +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-cli-security/utils/results" + "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 = "misconfigurations-scans-1#services-scans---services-configuration-security" +) + +type ServicesScanManager struct { + scanner *jas.JasScanner + + resultsToCompareFileName string + configFileName string + resultsFileName string +} + +type ServicesScanParams struct { + ThreadId int + TargetCount int + ResultsToCompare []*sarif.Run + Target results.ScanTarget +} + +// The RunServicesScan function runs the services scan flow, which includes the following steps: +// Creating a ServicesScanManager object. +// Running the analyzer manager executable. +// Parsing the analyzer manager results. +func RunServicesScan(scanner *jas.JasScanner, params ServicesScanParams) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + var scannerTempDir string + if scannerTempDir, err = jas.CreateScannerTempDirectory(scanner, jasutils.Services.String(), params.ThreadId); err != nil { + return + } + servicesScanManager, err := newServicesScanManager(scanner, scannerTempDir, params.ResultsToCompare...) + if err != nil { + return + } + startTime := time.Now() + log.Info(jas.GetStartJasScanLog(utils.ServicesScan, params.ThreadId, params.Target.DeprecatedAppsConfigModule, params.TargetCount)) + if vulnerabilitiesResults, violationsResults, err = servicesScanManager.runServicesScan(params); err != nil { + return + } + log.Info(utils.GetScanFindingsLog(utils.ServicesScan, sarifutils.GetResultsLocationCount(vulnerabilitiesResults...), startTime, params.ThreadId)) + return +} + +func (servicesScanManager *ServicesScanManager) runServicesScan(params ServicesScanParams) (vulnerabilitiesResults []*sarif.Run, violationsResults []*sarif.Run, err error) { + if params.Target.DeprecatedAppsConfigModule == nil { + return servicesScanManager.scanner.Run(servicesScanManager, params.Target) + } + return servicesScanManager.scanner.DeprecatedRun(servicesScanManager, *params.Target.DeprecatedAppsConfigModule, params.Target.GetCentralConfigExclusions(utils.ServicesScan)) +} + +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 { + // No scan results to compare + return + } + log.Debug("Diff mode - Services results to compare provided") + manager.resultsToCompareFileName = filepath.Join(scannerTempDir, "target.sarif") + // Save the iac results to compare as a report + if err = jas.SaveScanResultsToCompareAsReport(manager.resultsToCompareFileName, resultsToCompare...); err != nil { + return + } + return +} + +func (servicesScanManager *ServicesScanManager) DeprecatedRun(module jfrogappsconfig.Module, centralConfigExclusions []string) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = servicesScanManager.deprecatedCreateConfigFile(module, centralConfigExclusions, servicesScanManager.scanner.Exclusions...); err != nil { + return + } + if err = servicesScanManager.runAnalyzerManager(); err != nil { + return + } + return jas.ReadJasScanRunsFromFile(servicesScanManager.resultsFileName, servicesDocsUrlSuffix, servicesScanManager.scanner.MinSeverity, module.SourceRoot) +} + +func (servicesScanManager *ServicesScanManager) Run(target results.ScanTarget) (vulnerabilitiesSarifRuns []*sarif.Run, violationsSarifRuns []*sarif.Run, err error) { + if err = servicesScanManager.createConfigFileForTarget(target); err != nil { + return + } + if err = servicesScanManager.runAnalyzerManager(); err != nil { + return + } + return jas.ReadJasScanRunsFromFile(servicesScanManager.resultsFileName, servicesDocsUrlSuffix, servicesScanManager.scanner.MinSeverity, target.Target, target.Include...) +} + +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 (servicesScanManager *ServicesScanManager) deprecatedCreateConfigFile(module jfrogappsconfig.Module, centralConfigExclusions []string, exclusions ...string) error { + // We are not planning on adding support in jfrogappsconfig for services scans since it is deprecated + roots, err := jas.GetSourceRoots(module, nil) + if err != nil { + return err + } + configFileContent := servicesScanConfig{ + Scans: []servicesScanConfiguration{ + { + Roots: roots, + Output: servicesScanManager.resultsFileName, + PathToResultsToCompare: servicesScanManager.resultsToCompareFileName, + Type: servicesScannerType, + SkippedDirs: jas.GetJasExcludePatterns(module, nil, centralConfigExclusions, exclusions...), + }, + }, + } + return jas.CreateScannersConfigFile(servicesScanManager.configFileName, configFileContent, jasutils.Services) +} + +func (servicesScanManager *ServicesScanManager) createConfigFileForTarget(target results.ScanTarget) error { + configFileContent := servicesScanConfig{ + Scans: []servicesScanConfiguration{ + { + Roots: jas.GetRootsFromTarget(target), + Output: servicesScanManager.resultsFileName, + PathToResultsToCompare: servicesScanManager.resultsToCompareFileName, + Type: servicesScannerType, + SkippedDirs: jas.GetJasExcludePatternsForTarget(target, target.GetCentralConfigExclusions(utils.ServicesScan)), + }, + }, + } + return jas.CreateScannersConfigFile(servicesScanManager.configFileName, configFileContent, jasutils.Services) +} + +func (servicesScanManager *ServicesScanManager) runAnalyzerManager() error { + return servicesScanManager.scanner.AnalyzerManager.Exec(servicesScanManager.configFileName, servicesScanCommand, filepath.Dir(servicesScanManager.scanner.AnalyzerManager.AnalyzerManagerFullPath), servicesScanManager.scanner.ServerDetails, servicesScanManager.scanner.EnvVars) +} diff --git a/jas/services/servicesscanner_test.go b/jas/services/servicesscanner_test.go new file mode 100644 index 000000000..c79deca50 --- /dev/null +++ b/jas/services/servicesscanner_test.go @@ -0,0 +1,169 @@ +package services + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/stretchr/testify/require" + + jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" + "github.com/jfrog/jfrog-cli-security/jas" + + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/stretchr/testify/assert" +) + +func TestNewServicesScanManager(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{"currentDir"}) + assert.NoError(t, err) + + servicesScanManager, err := newServicesScanManager(scanner, "temoDirPath") + assert.NoError(t, err) + + if assert.NotNil(t, servicesScanManager) { + assert.NotEmpty(t, servicesScanManager.configFileName) + assert.NotEmpty(t, servicesScanManager.resultsFileName) + assert.NotEmpty(t, jfrogAppsConfigForTest.Modules[0].SourceRoot) + assert.Equal(t, &jas.FakeServerDetails, servicesScanManager.scanner.ServerDetails) + } +} + +func TestNewServicesScanManagerWithFilesToCompare(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + tempDir, cleanUpTempDir := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer cleanUpTempDir() + + scanner.TempDir = tempDir + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Services.String(), 0) + require.NoError(t, err) + + servicesScanManager, err := newServicesScanManager(scanner, scannerTempDir, sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyResult("test-markdown", "test-msg", "test-rule-id", "note"))) + require.NoError(t, err) + + assert.NotEmpty(t, servicesScanManager.resultsToCompareFileName) + assert.True(t, fileutils.IsPathExists(servicesScanManager.resultsToCompareFileName, false)) +} + +func TestServicesScan_CreateDeprecatedConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Services.String(), 0) + require.NoError(t, err) + servicesScanManager, err := newServicesScanManager(scanner, scannerTempDir) + require.NoError(t, err) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = servicesScanManager.deprecatedCreateConfigFile(jfrogappsconfig.Module{SourceRoot: currWd}, []string{}) + + defer func() { + err = os.Remove(servicesScanManager.configFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(servicesScanManager.configFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(servicesScanManager.configFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) + assert.Contains(t, string(fileContent), servicesScannerType) +} + +func TestServicesScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + tempDir, cleanUpTempDir := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer cleanUpTempDir() + + scanner.TempDir = tempDir + scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.Services.String(), 0) + require.NoError(t, err) + + servicesScanManager, err := newServicesScanManager(scanner, scannerTempDir) + require.NoError(t, err) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = servicesScanManager.createConfigFileForTarget(results.ScanTarget{Target: currWd}) + assert.NoError(t, err) + + defer func() { + err = os.Remove(servicesScanManager.configFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(servicesScanManager.configFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(servicesScanManager.configFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) + assert.Contains(t, string(fileContent), servicesScannerType) +} + +func TestServicesParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) + + servicesScanManager, err := newServicesScanManager(scanner, "temoDirPath") + require.NoError(t, err) + servicesScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "services-scan", "no-violations.sarif") + + vulnerabilitiesResults, violationResults, err := jas.ReadJasScanRunsFromFile(servicesScanManager.resultsFileName, servicesDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) + if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { + assert.Len(t, vulnerabilitiesResults, 1) + assert.Empty(t, vulnerabilitiesResults[0].Results) + } + assert.Empty(t, violationResults) +} + +func TestServicesParseResults_ResultsContainServicesIssues(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) + + tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + servicesScanManager, err := newServicesScanManager(scanner, "temoDirPath") + require.NoError(t, err) + assert.NoError(t, biutils.CopyDir(filepath.Join(jas.GetTestDataPath(), "services-scan"), tempDirPath, true, nil)) + servicesScanManager.resultsFileName = filepath.Join(tempDirPath, "contains-services-issues.sarif") + + vulnerabilitiesResults, violationResults, err := jas.ReadJasScanRunsFromFile(servicesScanManager.resultsFileName, servicesDocsUrlSuffix, scanner.MinSeverity, jfrogAppsConfigForTest.Modules[0].SourceRoot) + if assert.NoError(t, err) && assert.NotNil(t, vulnerabilitiesResults) { + assert.Len(t, vulnerabilitiesResults, 1) + assert.Len(t, vulnerabilitiesResults[0].Results, 4) + } + if assert.NotNil(t, violationResults) { + assert.Len(t, violationResults, 1) + assert.Len(t, violationResults[0].Results, 1) + } +} + +func TestGetServicesScanResults_AnalyzerManagerReturnsError(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) + servicesScanParams := ServicesScanParams{ + TargetCount: 1, + Target: results.ScanTarget{Target: jfrogAppsConfigForTest.Modules[0].SourceRoot, DeprecatedAppsConfigModule: &jfrogAppsConfigForTest.Modules[0]}, + } + vulnerabilitiesResults, _, err := RunServicesScan(scanner, servicesScanParams) + assert.Error(t, err) + assert.ErrorContains(t, jas.ParseAnalyzerManagerError(jasutils.Services, err), "failed to run Services scan") + assert.Nil(t, vulnerabilitiesResults) +} diff --git a/policy/enforcer/policyenforcer.go b/policy/enforcer/policyenforcer.go index 87997ea44..d73ca19c9 100644 --- a/policy/enforcer/policyenforcer.go +++ b/policy/enforcer/policyenforcer.go @@ -168,6 +168,10 @@ func convertToViolations(cmdResults *results.SecurityCommandResults, generatedVi if secretsViolation := convertToJasViolation(cmdResults, jasutils.Secrets, violation); secretsViolation != nil { convertedViolations.Secrets = append(convertedViolations.Secrets, *secretsViolation) } + case utils.ServicesScan: + if servicesViolation := convertToJasViolation(cmdResults, jasutils.Services, violation); servicesViolation != nil { + convertedViolations.Services = append(convertedViolations.Services, *servicesViolation) + } default: log.Warn(fmt.Sprintf("Skipping violation with unknown scan type for violation ID %s", violation.Id)) } @@ -183,6 +187,7 @@ func getViolationType(violation services.XrayViolation) utils.SubScanType { if strings.HasPrefix(violation.ExposureDetails.Id, "EXP") { return utils.SecretsScan } + // TODO: add Services support when Xray adds Services details to violations // TODO: add IaC support when Xray adds IaC details to violations return "" } @@ -255,6 +260,8 @@ func getJasViolationType(jasType jasutils.JasScanType) violationutils.ViolationI return violationutils.SecretsViolationType case jasutils.IaC: return violationutils.IacViolationType + case jasutils.Services: + return violationutils.ServicesViolationType default: return "" } diff --git a/policy/handler_test.go b/policy/handler_test.go index 2c619ecf3..1964bff94 100644 --- a/policy/handler_test.go +++ b/policy/handler_test.go @@ -125,6 +125,15 @@ func TestCheckPolicyFailPrError(t *testing.T) { }), expectedErr: NewFailPrError(), }, + { + name: "services violations with fail PR", + resultToTest: createResultsWithViolations(violationutils.Violations{ + Services: []violationutils.JasViolation{ + createJasViolation("JAS-SVC-1", violationutils.ServicesViolationType, createPolicy(false, true, false)), + }, + }), + expectedErr: NewFailPrError(), + }, { name: "violations with no fail PR", resultToTest: createResultsWithViolations(violationutils.Violations{ diff --git a/policy/local/localconvertor.go b/policy/local/localconvertor.go index d1c4b569d..4d99a89b4 100644 --- a/policy/local/localconvertor.go +++ b/policy/local/localconvertor.go @@ -66,6 +66,11 @@ func (d *DeprecatedViolationGenerator) GenerateViolations(cmdResults *results.Se err = errors.Join(err, fmt.Errorf("failed to convert JAS IaC violations for target %s: %w", target.Target, e)) } } + if len(target.JasResults.JasViolations.ServicesScanResults) > 0 { + if e := results.ForEachJasIssue(target.JasResults.JasViolations.ServicesScanResults, cmdResults.Entitlements.Jas, convertJasViolationsToPolicyViolations(&convertedViolations, jasutils.Services)); e != nil { + err = errors.Join(err, fmt.Errorf("failed to convert JAS Services violations for target %s: %w", target.Target, e)) + } + } if len(target.JasResults.JasViolations.SastScanResults) > 0 { if e := results.ForEachJasIssue(target.JasResults.JasViolations.SastScanResults, cmdResults.Entitlements.Jas, convertJasViolationsToPolicyViolations(&convertedViolations, jasutils.Sast)); e != nil { err = errors.Join(err, fmt.Errorf("failed to convert JAS SAST violations for target %s: %w", target.Target, e)) @@ -167,6 +172,13 @@ func convertJasViolationsToPolicyViolations(convertedViolations *violationutils. Result: result, Location: location, }) + case jasutils.Services: + convertedViolations.Services = append(convertedViolations.Services, violationutils.JasViolation{ + Violation: convertToBasicJasViolation(violationutils.ServicesViolationType, result, severity), + Rule: rule, + Result: result, + Location: location, + }) case jasutils.IaC: convertedViolations.Iac = append(convertedViolations.Iac, violationutils.JasViolation{ Violation: convertToBasicJasViolation(violationutils.IacViolationType, result, severity), diff --git a/policy/local/localconvertor_test.go b/policy/local/localconvertor_test.go index a18cce803..739199f3b 100644 --- a/policy/local/localconvertor_test.go +++ b/policy/local/localconvertor_test.go @@ -187,6 +187,7 @@ func TestGenerateViolations(t *testing.T) { assert.ElementsMatch(t, converted.Sast, tc.expected.Sast) assert.ElementsMatch(t, converted.Secrets, tc.expected.Secrets) assert.ElementsMatch(t, converted.Iac, tc.expected.Iac) + assert.ElementsMatch(t, converted.Services, tc.expected.Services) }) } } diff --git a/sca/bom/buildinfo/technologies/common_test.go b/sca/bom/buildinfo/technologies/common_test.go index c69f8021d..f89a6967d 100644 --- a/sca/bom/buildinfo/technologies/common_test.go +++ b/sca/bom/buildinfo/technologies/common_test.go @@ -34,7 +34,7 @@ func TestGetScaExcludePattern(t *testing.T) { { name: "Test no exclude pattern recursive", isRecursiveScan: true, - expected: "(^.*\\.git.*$)|(^.*node_modules.*$)|(^.*target.*$)|(^.*venv.*$)|(^.*test.*$)|(^dist$)", + expected: "(^.*\\.git$)|(^.*node_modules.*$)|(^.*target.*$)|(^.*venv.*$)|(^.*test.*$)|(^dist$)", }, { name: "Test exclude pattern not recursive", @@ -43,7 +43,7 @@ func TestGetScaExcludePattern(t *testing.T) { }, { name: "Test no exclude pattern", - expected: "(^.*\\.git.*$)|(^.*node_modules.*$)|(^.*target.*$)|(^.*venv.*$)|(^.*test.*$)|(^dist$)", + expected: "(^.*\\.git$)|(^.*node_modules.*$)|(^.*target.*$)|(^.*venv.*$)|(^.*test.*$)|(^dist$)", }, { name: "Test exclude patterns from config profile", diff --git a/scans_test.go b/scans_test.go index 1d7958250..352f0a6aa 100644 --- a/scans_test.go +++ b/scans_test.go @@ -65,6 +65,8 @@ func subScansToFlags(subScans []utils.SubScanType) (flags []string) { flags = append(flags, "--secrets") case utils.IacScan: flags = append(flags, "--iac") + case utils.ServicesScan: + flags = append(flags, "--services") case utils.SastScan: flags = append(flags, "--sast") } diff --git a/tests/testdata/git/projects/issues-mvn/issues-mvn.zip b/tests/testdata/git/projects/issues-mvn/issues-mvn.zip index 68a5860e6..aa0e9c2b7 100644 Binary files a/tests/testdata/git/projects/issues-mvn/issues-mvn.zip and b/tests/testdata/git/projects/issues-mvn/issues-mvn.zip differ diff --git a/tests/testdata/git/projects/issues/issues.zip b/tests/testdata/git/projects/issues/issues.zip index 9169ec62f..6bfebdf5b 100644 Binary files a/tests/testdata/git/projects/issues/issues.zip and b/tests/testdata/git/projects/issues/issues.zip differ 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/other/services-scan/contains-services-issues.sarif b/tests/testdata/other/services-scan/contains-services-issues.sarif new file mode 100644 index 000000000..cffadea8a --- /dev/null +++ b/tests/testdata/other/services-scan/contains-services-issues.sarif @@ -0,0 +1,129 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Services scanner", + "rules": [], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./svc_scanner", + "scan", + "scan.yaml" + ], + "workingDirectory": { + "uri": "file:///tmp/services-scanner" + } + } + ], + "results": [ + { + "message": { + "text": "Overly broad permissions write-all detected" + }, + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///tmp/services-scanner/.github/workflows/services.yml" + }, + "region": { + "endColumn": 20, + "endLine": 10, + "snippet": { + "text": "permissions: write-all" + }, + "startColumn": 1, + "startLine": 10 + } + } + } + ], + "ruleId": "GITHUB-ACTIONS-permissions-write-all" + }, + { + "message": { + "text": "Hardcoded credentials detected in workflow env" + }, + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///tmp/services-scanner/.github/workflows/services.yml" + }, + "region": { + "endColumn": 40, + "endLine": 15, + "snippet": { + "text": "AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE" + }, + "startColumn": 3, + "startLine": 15 + } + } + } + ], + "ruleId": "GITHUB-ACTIONS-hardcoded-credentials" + }, + { + "message": { + "text": "Unpinned third-party action detected" + }, + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///tmp/services-scanner/.github/workflows/services.yml" + }, + "region": { + "endColumn": 40, + "endLine": 41, + "snippet": { + "text": "uses: some-org/some-action@main" + }, + "startColumn": 9, + "startLine": 41 + } + } + } + ], + "ruleId": "GITHUB-ACTIONS-unpinned-action" + }, + { + "message": { + "text": "pull_request_target with untrusted checkout detected" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///tmp/services-scanner/.github/workflows/services.yml" + }, + "region": { + "endColumn": 30, + "endLine": 4, + "snippet": { + "text": "pull_request_target:" + }, + "startColumn": 3, + "startLine": 4 + } + } + } + ], + "ruleId": "GITHUB-ACTIONS-pull-request-target" + } + ] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} diff --git a/tests/testdata/other/services-scan/contains-services-issues_violations.sarif b/tests/testdata/other/services-scan/contains-services-issues_violations.sarif new file mode 100644 index 000000000..5391018d4 --- /dev/null +++ b/tests/testdata/other/services-scan/contains-services-issues_violations.sarif @@ -0,0 +1,62 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Services scanner", + "rules": [], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./svc_scanner", + "scan", + "scan.yaml" + ], + "workingDirectory": { + "uri": "file:///tmp/services-scanner" + } + } + ], + "results": [ + { + "properties": { + "issueId": "services-violation-1", + "policies": [ + "policy" + ], + "watch": "watch" + }, + "message": { + "text": "Overly broad permissions write-all detected" + }, + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///tmp/services-scanner/.github/workflows/services.yml" + }, + "region": { + "endColumn": 20, + "endLine": 10, + "snippet": { + "text": "permissions: write-all" + }, + "startColumn": 1, + "startLine": 10 + } + } + } + ], + "ruleId": "GITHUB-ACTIONS-permissions-write-all" + } + ] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} diff --git a/tests/testdata/other/services-scan/no-violations.sarif b/tests/testdata/other/services-scan/no-violations.sarif new file mode 100644 index 000000000..4eb3dc9f5 --- /dev/null +++ b/tests/testdata/other/services-scan/no-violations.sarif @@ -0,0 +1,30 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Services scanner", + "rules": [], + "version": "" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "./svc_scanner", + "scan", + "scan.yaml" + ], + "workingDirectory": { + "uri": "file:///tmp/services-scanner" + } + } + ], + "results": [ + ] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} diff --git a/tests/testdata/projects/jas/jas/.github/workflows/services.yml b/tests/testdata/projects/jas/jas/.github/workflows/services.yml new file mode 100644 index 000000000..e011b5b77 --- /dev/null +++ b/tests/testdata/projects/jas/jas/.github/workflows/services.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/tests/validations/test_validate_cyclonedx.go b/tests/validations/test_validate_cyclonedx.go index 4a362fada..dc02da241 100644 --- a/tests/validations/test_validate_cyclonedx.go +++ b/tests/validations/test_validate_cyclonedx.go @@ -44,8 +44,8 @@ func GetCycloneDxActualValues(t *testing.T, content string) (actualValues Valida func toActualValuesCycloneDx(content *cdxutils.FullBOM) (actualValues ValidationCountActualValues) { actualValues.SbomComponents, actualValues.RootComponents, actualValues.DirectComponents, actualValues.TransitiveComponents, actualValues.Licenses = countSbomComponents(&content.BOM) actualValues.ScaVulnerabilities, actualValues.ApplicableVulnerabilities, actualValues.UndeterminedVulnerabilities, actualValues.NotCoveredVulnerabilities, actualValues.NotApplicableVulnerabilities, actualValues.MissingContextVulnerabilities = countScaVulnerabilities(&content.BOM) - actualValues.SastVulnerabilities, actualValues.SecretsVulnerabilities, actualValues.IacVulnerabilities, actualValues.InactiveSecretsVulnerabilities = countJasVulnerabilities(content) - actualValues.Vulnerabilities = actualValues.ScaVulnerabilities + actualValues.SastVulnerabilities + actualValues.SecretsVulnerabilities + actualValues.IacVulnerabilities + actualValues.SastVulnerabilities, actualValues.SecretsVulnerabilities, actualValues.IacVulnerabilities, actualValues.ServicesVulnerabilities, actualValues.InactiveSecretsVulnerabilities = countJasVulnerabilities(content) + actualValues.Vulnerabilities = actualValues.ScaVulnerabilities + actualValues.SastVulnerabilities + actualValues.SecretsVulnerabilities + actualValues.IacVulnerabilities + actualValues.ServicesVulnerabilities return } @@ -115,7 +115,7 @@ func countScaVulnerabilities(content *cyclonedx.BOM) (scaVulnerabilities, applic return } -func countJasVulnerabilities(content *cdxutils.FullBOM) (sastVulnerabilities, secretsVulnerabilities, iacVulnerabilities, inactiveSecretsVulnerabilities int) { +func countJasVulnerabilities(content *cdxutils.FullBOM) (sastVulnerabilities, secretsVulnerabilities, iacVulnerabilities, servicesVulnerabilities, inactiveSecretsVulnerabilities int) { if content == nil || content.Vulnerabilities == nil { return } @@ -134,6 +134,9 @@ func countJasVulnerabilities(content *cdxutils.FullBOM) (sastVulnerabilities, se if strings.HasPrefix(property.Name, "jfrog:iac:location:") { iacVulnerabilities++ } + if strings.HasPrefix(property.Name, "jfrog:services:location:") { + servicesVulnerabilities++ + } if strings.HasPrefix(property.Name, "jfrog:secret:location:") { secretsVulnerabilities++ } diff --git a/tests/validations/test_validate_sarif.go b/tests/validations/test_validate_sarif.go index 0027defcb..d005d1acb 100644 --- a/tests/validations/test_validate_sarif.go +++ b/tests/validations/test_validate_sarif.go @@ -20,6 +20,7 @@ const ( IacToolName = "JFrog Terraform scanner" // #nosec G101 -- Not credentials. SecretsToolName = "JFrog Secrets scanner" + ServicesToolName = "JFrog Services scanner" ContextualAnalysisToolName = "JFrog Applicability Scanner" ) @@ -62,13 +63,18 @@ func toActualValuesSarif(content *sarif.Report) (actualValues ValidationCountAct actualValues.Vulnerabilities += actualValues.IacVulnerabilities actualValues.Violations += actualValues.IacViolations + // Services + actualValues.ServicesVulnerabilities, actualValues.ServicesViolations = countJasResults(sarifutils.GetRunsByToolName(content, ServicesToolName)) + actualValues.Vulnerabilities += actualValues.ServicesVulnerabilities + actualValues.Violations += actualValues.ServicesViolations + // SAST actualValues.SastVulnerabilities, actualValues.SastViolations = countJasResults(sarifutils.GetRunsByToolName(content, SastToolName)) actualValues.Vulnerabilities += actualValues.SastVulnerabilities actualValues.Violations += actualValues.SastViolations // Violations run - actualValues.Violations, actualValues.ScaViolations, actualValues.SecurityViolations, actualValues.LicenseViolations, actualValues.ApplicableViolations, actualValues.UndeterminedViolations, actualValues.NotCoveredViolations, actualValues.NotApplicableViolations, actualValues.MissingContextViolations, actualValues.SastViolations, actualValues.IacViolations, actualValues.SecretsViolations, actualValues.InactiveSecretsViolations = countViolations(sarifutils.GetRunsByToolName(content, sarifparser.PolicyEnforcerToolName)) + actualValues.Violations, actualValues.ScaViolations, actualValues.SecurityViolations, actualValues.LicenseViolations, actualValues.ApplicableViolations, actualValues.UndeterminedViolations, actualValues.NotCoveredViolations, actualValues.NotApplicableViolations, actualValues.MissingContextViolations, actualValues.SastViolations, actualValues.IacViolations, actualValues.ServicesViolations, actualValues.SecretsViolations, actualValues.InactiveSecretsViolations = countViolations(sarifutils.GetRunsByToolName(content, sarifparser.PolicyEnforcerToolName)) return } @@ -215,7 +221,7 @@ func countJasResults(runs []*sarif.Run) (vulnerabilities, violations int) { return } -func countViolations(policyRuns []*sarif.Run) (total, sca, sec, lic, applic, undetermined, notCover, notApplic, missingCtx, sast, iac, secrets, inactive int) { +func countViolations(policyRuns []*sarif.Run) (total, sca, sec, lic, applic, undetermined, notCover, notApplic, missingCtx, sast, iac, services, secrets, inactive int) { for _, run := range policyRuns { for _, result := range run.Results { total++ @@ -233,6 +239,8 @@ func countViolations(policyRuns []*sarif.Run) (total, sca, sec, lic, applic, und sast++ case violationutils.IacViolationType: iac++ + case violationutils.ServicesViolationType: + services++ case violationutils.SecretsViolationType: secrets++ } diff --git a/tests/validations/test_validate_simple_json.go b/tests/validations/test_validate_simple_json.go index 6612c66fd..c572db978 100644 --- a/tests/validations/test_validate_simple_json.go +++ b/tests/validations/test_validate_simple_json.go @@ -48,17 +48,19 @@ func GetSimpleJsonActualValues(t *testing.T, content string) (actualValues Valid func toActualValuesSimpleJson(results formats.SimpleJsonResults) (actualValues ValidationCountActualValues) { actualValues = ValidationCountActualValues{ // Total - Vulnerabilities: len(results.Vulnerabilities) + len(results.SecretsVulnerabilities) + len(results.SastVulnerabilities) + len(results.IacsVulnerabilities), - Violations: len(results.SecurityViolations) + len(results.LicensesViolations) + len(results.OperationalRiskViolations) + len(results.SecretsViolations) + len(results.SastViolations) + len(results.IacsViolations), + Vulnerabilities: len(results.Vulnerabilities) + len(results.SecretsVulnerabilities) + len(results.SastVulnerabilities) + len(results.IacsVulnerabilities) + len(results.ServicesVulnerabilities), + Violations: len(results.SecurityViolations) + len(results.LicensesViolations) + len(results.OperationalRiskViolations) + len(results.SecretsViolations) + len(results.SastViolations) + len(results.IacsViolations) + len(results.ServicesViolations), Licenses: len(results.Licenses), // Jas vulnerabilities - SastVulnerabilities: len(results.SastVulnerabilities), - SecretsVulnerabilities: len(results.SecretsVulnerabilities), - IacVulnerabilities: len(results.IacsVulnerabilities), + SastVulnerabilities: len(results.SastVulnerabilities), + SecretsVulnerabilities: len(results.SecretsVulnerabilities), + IacVulnerabilities: len(results.IacsVulnerabilities), + ServicesVulnerabilities: len(results.ServicesVulnerabilities), // Jas violations - SastViolations: len(results.SastViolations), - SecretsViolations: len(results.SecretsViolations), - IacViolations: len(results.IacsViolations), + SastViolations: len(results.SastViolations), + SecretsViolations: len(results.SecretsViolations), + IacViolations: len(results.IacsViolations), + ServicesViolations: len(results.ServicesViolations), // Sca vulnerabilities ScaVulnerabilities: len(results.Vulnerabilities), // Sca violations diff --git a/tests/validations/test_validate_summary.go b/tests/validations/test_validate_summary.go index e29c43c21..b80cf03fe 100644 --- a/tests/validations/test_validate_summary.go +++ b/tests/validations/test_validate_summary.go @@ -32,13 +32,15 @@ func toActualValuesSummary(results formats.ResultsSummary) (actualValues Validat Vulnerabilities: results.GetTotalVulnerabilities(), Violations: results.GetTotalViolations(), // Jas vulnerabilities - SastVulnerabilities: results.GetTotalVulnerabilities(formats.SastResult), - SecretsVulnerabilities: results.GetTotalVulnerabilities(formats.SecretsResult), - IacVulnerabilities: results.GetTotalVulnerabilities(formats.IacResult), + SastVulnerabilities: results.GetTotalVulnerabilities(formats.SastResult), + SecretsVulnerabilities: results.GetTotalVulnerabilities(formats.SecretsResult), + IacVulnerabilities: results.GetTotalVulnerabilities(formats.IacResult), + ServicesVulnerabilities: results.GetTotalVulnerabilities(formats.ServicesResult), // Jas violations - SastViolations: results.GetTotalViolations(formats.SastResult), - SecretsViolations: results.GetTotalViolations(formats.SecretsResult), - IacViolations: results.GetTotalViolations(formats.IacResult), + SastViolations: results.GetTotalViolations(formats.SastResult), + SecretsViolations: results.GetTotalViolations(formats.SecretsResult), + IacViolations: results.GetTotalViolations(formats.IacResult), + ServicesViolations: results.GetTotalViolations(formats.ServicesResult), // Sca vulnerabilities ScaVulnerabilities: results.GetTotalVulnerabilities(formats.ScaSecurityResult), // Sca violations diff --git a/tests/validations/test_validation.go b/tests/validations/test_validation.go index 9c362a5c0..7ad798728 100644 --- a/tests/validations/test_validation.go +++ b/tests/validations/test_validation.go @@ -58,6 +58,8 @@ type ScanCount struct { Sast int // Expected number of Iac issues Iac int + // Expected number of Services issues + Services int // Expected number of Secrets issues Secrets int // Expected number of Malicious Code issues @@ -285,10 +287,10 @@ type ValidationCountActualValues struct { // Total counts Vulnerabilities, Violations, Licenses, SbomComponents int // Vulnerabilities counts - SastVulnerabilities, SecretsVulnerabilities, IacVulnerabilities, ScaVulnerabilities int + SastVulnerabilities, SecretsVulnerabilities, IacVulnerabilities, ServicesVulnerabilities, ScaVulnerabilities int ApplicableVulnerabilities, UndeterminedVulnerabilities, NotCoveredVulnerabilities, NotApplicableVulnerabilities, MissingContextVulnerabilities, InactiveSecretsVulnerabilities int // Violations counts - SastViolations, SecretsViolations, IacViolations, ScaViolations int + SastViolations, SecretsViolations, IacViolations, ServicesViolations, ScaViolations int SecurityViolations, LicenseViolations, OperationalViolations int ApplicableViolations, UndeterminedViolations, NotCoveredViolations, NotApplicableViolations, MissingContextViolations, InactiveSecretsViolations int // Sbom counts @@ -318,7 +320,7 @@ func ValidateVulnerabilitiesCount(t *testing.T, outputType string, exactMatch bo if params == nil { return } - ValidateScanTypeCount(t, outputType, false, exactMatch, params.ValidateScan, actual.ScaVulnerabilities, actual.SastVulnerabilities, actual.SecretsVulnerabilities, actual.IacVulnerabilities) + ValidateScanTypeCount(t, outputType, false, exactMatch, params.ValidateScan, actual.ScaVulnerabilities, actual.SastVulnerabilities, actual.SecretsVulnerabilities, actual.IacVulnerabilities, actual.ServicesVulnerabilities) ValidateApplicabilityStatusCount(t, outputType, false, exactMatch, params.ValidateApplicabilityStatus, actual.ApplicableVulnerabilities, actual.UndeterminedVulnerabilities, actual.NotCoveredVulnerabilities, actual.NotApplicableVulnerabilities, actual.MissingContextVulnerabilities, actual.InactiveSecretsVulnerabilities) } @@ -326,12 +328,12 @@ func ValidateViolationCount(t *testing.T, outputType string, exactMatch bool, pa if params == nil { return } - ValidateScanTypeCount(t, outputType, true, exactMatch, params.ValidateScan, actual.ScaViolations, actual.SastViolations, actual.SecretsViolations, actual.IacViolations) + ValidateScanTypeCount(t, outputType, true, exactMatch, params.ValidateScan, actual.ScaViolations, actual.SastViolations, actual.SecretsViolations, actual.IacViolations, actual.ServicesViolations) ValidateApplicabilityStatusCount(t, outputType, true, exactMatch, params.ValidateApplicabilityStatus, actual.ApplicableViolations, actual.UndeterminedViolations, actual.NotCoveredViolations, actual.NotApplicableViolations, actual.MissingContextViolations, actual.InactiveSecretsViolations) ValidateScaViolationCount(t, outputType, exactMatch, params.ValidateType, actual.SecurityViolations, actual.LicenseViolations, actual.OperationalViolations) } -func ValidateScanTypeCount(t *testing.T, outputType string, violation, exactMatch bool, params *ScanCount, scaViolations, sastViolations, secretsViolations, iacViolations int) { +func ValidateScanTypeCount(t *testing.T, outputType string, violation, exactMatch bool, params *ScanCount, scaViolations, sastViolations, secretsViolations, iacViolations, servicesViolations int) { if params == nil { return } @@ -343,6 +345,7 @@ func ValidateScanTypeCount(t *testing.T, outputType string, violation, exactMatc CountValidation[int]{Expected: params.Sast, Actual: sastViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("sast %s", suffix), outputType, exactMatch, params.Sast, sastViolations)}, CountValidation[int]{Expected: params.Secrets, Actual: secretsViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("secrets %s", suffix), outputType, exactMatch, params.Secrets, secretsViolations)}, CountValidation[int]{Expected: params.Iac, Actual: iacViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("IaC %s", suffix), outputType, exactMatch, params.Iac, iacViolations)}, + CountValidation[int]{Expected: params.Services, Actual: servicesViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("Services %s", suffix), outputType, exactMatch, params.Services, servicesViolations)}, CountValidation[int]{Expected: params.Sca, Actual: scaViolations, Msg: GetValidationCountErrMsg(fmt.Sprintf("Sca %s", suffix), outputType, exactMatch, params.Sca, scaViolations)}, ) } diff --git a/utils/formats/conversion.go b/utils/formats/conversion.go index fa180a6b1..1441e55d7 100644 --- a/utils/formats/conversion.go +++ b/utils/formats/conversion.go @@ -180,9 +180,9 @@ func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow return } -func ConvertToIacOrSastTableRow(rows []SourceCodeRow) (tableRows []iacOrSastTableRow) { +func ConvertToBasicJasTableRow(rows []SourceCodeRow) (tableRows []basicJasTableRow) { for i := range rows { - tableRows = append(tableRows, iacOrSastTableRow{ + tableRows = append(tableRows, basicJasTableRow{ severity: rows[i].Severity, file: rows[i].File, lineColumn: strconv.Itoa(rows[i].StartLine) + ":" + strconv.Itoa(rows[i].StartColumn), diff --git a/utils/formats/simplejsonapi.go b/utils/formats/simplejsonapi.go index 2e029276f..6645c7c50 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"` @@ -33,6 +35,7 @@ type ScanStatus struct { ScaStatusCode *int `json:"scaScanStatusCode,omitempty"` SastStatusCode *int `json:"sastScanStatusCode,omitempty"` IacStatusCode *int `json:"iacScanStatusCode,omitempty"` + ServicesStatusCode *int `json:"servicesScanStatusCode,omitempty"` SecretsStatusCode *int `json:"secretsScanStatusCode,omitempty"` ApplicabilityStatusCode *int `json:"ContextualAnalysisScanStatusCode,omitempty"` MaliciousStatusCode *int `json:"MaliciousStatusCode,omitempty"` diff --git a/utils/formats/summary.go b/utils/formats/summary.go index c8d80dfba..1f38e3d4f 100644 --- a/utils/formats/summary.go +++ b/utils/formats/summary.go @@ -7,6 +7,7 @@ import ( const ( IacResult SummaryResultType = "IAC" + ServicesResult SummaryResultType = "Services" SecretsResult SummaryResultType = "Secrets" SastResult SummaryResultType = "SAST" ScaResult SummaryResultType = "SCA" @@ -38,6 +39,7 @@ type ScanSummary struct { type ScanResultSummary struct { ScaResults *ScaScanResultSummary `json:"sca,omitempty"` IacResults *ResultSummary `json:"iac,omitempty"` + ServicesResults *ResultSummary `json:"services,omitempty"` SecretsResults *ResultSummary `json:"secrets,omitempty"` SastResults *ResultSummary `json:"sast,omitempty"` MaliciousResults *ResultSummary `json:"maliciousCode,omitempty"` @@ -184,6 +186,9 @@ func (srs *ScanResultSummary) GetTotal(filterTypes ...SummaryResultType) (total if srs.IacResults != nil && isFilterApply(IacResult, filterTypes) { total += srs.IacResults.GetTotal() } + if srs.ServicesResults != nil && isFilterApply(ServicesResult, filterTypes) { + total += srs.ServicesResults.GetTotal() + } if srs.SecretsResults != nil && isFilterApply(SecretsResult, filterTypes) { total += srs.SecretsResults.GetTotal() } @@ -229,6 +234,9 @@ func (ss *ScanResultSummary) GetSummaryBySeverity() (summary ResultSummary) { if ss.IacResults != nil { summary = MergeResultSummaries(summary, *ss.IacResults) } + if ss.ServicesResults != nil { + summary = MergeResultSummaries(summary, *ss.ServicesResults) + } if ss.SecretsResults != nil { summary = MergeResultSummaries(summary, *ss.SecretsResults) } @@ -306,6 +314,9 @@ func extractIssuesToSummary(issues *ScanResultSummary, destination *ScanResultSu if issues.IacResults != nil { destination.IacResults = mergeResultSummariesPointers(destination.IacResults, issues.IacResults) } + if issues.ServicesResults != nil { + destination.ServicesResults = mergeResultSummariesPointers(destination.ServicesResults, issues.ServicesResults) + } if issues.SecretsResults != nil { destination.SecretsResults = mergeResultSummariesPointers(destination.SecretsResults, issues.SecretsResults) } diff --git a/utils/formats/summary_test.go b/utils/formats/summary_test.go index df701bed2..10d9a1d5d 100644 --- a/utils/formats/summary_test.go +++ b/utils/formats/summary_test.go @@ -126,7 +126,7 @@ func TestScanResultSummary(t *testing.T) { { name: "Multiple filters", summary: testSummary, - resultTypeFilters: []SummaryResultType{ScaSecurityResult, ScaLicenseResult, IacResult, SecretsResult, SastResult}, + resultTypeFilters: []SummaryResultType{ScaSecurityResult, ScaLicenseResult, IacResult, ServicesResult, SecretsResult, SastResult}, expectedTotal: 5, expectedScanIds: ids, expectedMoreInfoUrls: urls, @@ -227,7 +227,7 @@ func TestResultsSummary(t *testing.T) { { name: "With filters", summary: ResultsSummary{Scans: testScans}, - filters: []SummaryResultType{ScaLicenseResult, IacResult, SecretsResult}, + filters: []SummaryResultType{ScaLicenseResult, IacResult, ServicesResult, SecretsResult}, expectedTotalVulnerabilities: 4, expectedTotalViolations: 2, }, diff --git a/utils/formats/table.go b/utils/formats/table.go index b3ec7e57a..f58529e00 100644 --- a/utils/formats/table.go +++ b/utils/formats/table.go @@ -15,11 +15,14 @@ type ResultsTables struct { LicenseViolationsTable []licenseViolationTableRow OperationalRiskViolationsTable []operationalRiskViolationTableRow // Iac tables - IacVulnerabilitiesTable []iacOrSastTableRow - IacViolationsTable []iacOrSastTableRow + IacVulnerabilitiesTable []basicJasTableRow + IacViolationsTable []basicJasTableRow // Sast tables - SastVulnerabilitiesTable []iacOrSastTableRow - SastViolationsTable []iacOrSastTableRow + SastVulnerabilitiesTable []basicJasTableRow + SastViolationsTable []basicJasTableRow + // Services + ServicesVulnerabilitiesTable []basicJasTableRow + ServicesViolationsTable []basicJasTableRow // Secrets SecretsVulnerabilitiesTable []secretsTableRow SecretsViolationsTable []secretsTableRow @@ -169,7 +172,7 @@ type secretsTableRow struct { watch string `col-name:"Watch Name" omitempty:"true"` } -type iacOrSastTableRow struct { +type basicJasTableRow struct { severity string `col-name:"Severity"` file string `col-name:"File"` lineColumn string `col-name:"Line:Column"` diff --git a/utils/formats/violationutils/violations.go b/utils/formats/violationutils/violations.go index 58fad0e1f..c77c5ab9c 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"` + Iac []JasViolation `json:"iac,omitempty"` + Services []JasViolation `json:"services,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.Iac) > 0 || len(vs.Sast) > 0 || len(vs.Services) > 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.Iac) + len(vs.Sast) + len(vs.Services) } func (vs *Violations) String() string { @@ -79,6 +81,9 @@ func (vs *Violations) String() string { if len(vs.Iac) > 0 { out = append(out, fmt.Sprintf("%d IaC", len(vs.Iac))) } + if len(vs.Services) > 0 { + out = append(out, fmt.Sprintf("%d Services", len(vs.Services))) + } if len(vs.Sast) > 0 { out = append(out, fmt.Sprintf("%d SAST", len(vs.Sast))) } @@ -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/formats/violationutils/violations_test.go b/utils/formats/violationutils/violations_test.go index ef3ef29ce..773026c45 100644 --- a/utils/formats/violationutils/violations_test.go +++ b/utils/formats/violationutils/violations_test.go @@ -64,3 +64,14 @@ func TestGetOperationalRiskReadableData(t *testing.T) { }) } } + +func TestViolations_Services(t *testing.T) { + violations := Violations{ + Services: []JasViolation{{ + Violation: Violation{ViolationType: ServicesViolationType}, + }}, + } + assert.True(t, violations.HasViolations()) + assert.Equal(t, 1, violations.Count()) + assert.Contains(t, violations.String(), "1 Services") +} diff --git a/utils/jasutils/jasutils.go b/utils/jasutils/jasutils.go index 621e9262a..5c02fdfd6 100644 --- a/utils/jasutils/jasutils.go +++ b/utils/jasutils/jasutils.go @@ -21,6 +21,7 @@ const ( Secrets JasScanType = "Secrets" IaC JasScanType = "IaC" Sast JasScanType = "Sast" + Services JasScanType = "Services" MaliciousCode JasScanType = "MaliciousCode" ) @@ -96,6 +97,8 @@ func SubScanTypeToJasScanType(subScanType utils.SubScanType) JasScanType { return IaC case utils.SecretsScan: return Secrets + case utils.ServicesScan: + return Services case utils.ContextualAnalysisScan: return Applicability case utils.MaliciousCodeScan: diff --git a/utils/jasutils/jasutils_test.go b/utils/jasutils/jasutils_test.go index 863291c27..0f606ed2b 100644 --- a/utils/jasutils/jasutils_test.go +++ b/utils/jasutils/jasutils_test.go @@ -1,8 +1,10 @@ package jasutils import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/stretchr/testify/assert" ) func TestCveToApplicabilityRuleId(t *testing.T) { @@ -28,3 +30,12 @@ func TestApplicabilityRuleIdToCve(t *testing.T) { assert.Equal(t, test.expectedOutput, ApplicabilityRuleIdToCve(test.ruleId)) } } + +func TestSubScanTypeToJasScanType(t *testing.T) { + assert.Equal(t, Secrets, SubScanTypeToJasScanType(utils.SecretsScan)) + assert.Equal(t, IaC, SubScanTypeToJasScanType(utils.IacScan)) + assert.Equal(t, Services, SubScanTypeToJasScanType(utils.ServicesScan)) + assert.Equal(t, Sast, SubScanTypeToJasScanType(utils.SastScan)) + assert.Equal(t, Applicability, SubScanTypeToJasScanType(utils.ContextualAnalysisScan)) + assert.Equal(t, MaliciousCode, SubScanTypeToJasScanType(utils.MaliciousCodeScan)) +} diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go index 24371de3b..a1ab10946 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -70,6 +70,7 @@ type ResultsStreamFormatParser[T interface{}] interface { ParseCVEs(enrichedSbom *cyclonedx.BOM, applicableScan ...[]*sarif.Run) error // Parse JAS content to the current scan target ParseSecrets(secrets ...[]*sarif.Run) error + ParseServices(services ...[]*sarif.Run) error ParseIacs(iacs ...[]*sarif.Run) error ParseSast(sast ...[]*sarif.Run) error ParseMalicious(malicious ...[]*sarif.Run) error @@ -217,6 +218,10 @@ func parseJasResults[T interface{}](params ResultConvertParams, parser ResultsSt if err = parser.ParseSecrets(targetResults.JasResults.JasVulnerabilities.SecretsScanResults); err != nil { return } + // Parsing JAS Services results + if err = parser.ParseServices(targetResults.JasResults.JasVulnerabilities.ServicesScanResults); err != nil { + return + } // Parsing JAS IAC results if err = parser.ParseIacs(targetResults.JasResults.JasVulnerabilities.IacScanResults); err != nil { return diff --git a/utils/results/conversion/convertor_test.go b/utils/results/conversion/convertor_test.go index f2859b34f..45551c178 100644 --- a/utils/results/conversion/convertor_test.go +++ b/utils/results/conversion/convertor_test.go @@ -146,16 +146,16 @@ func getAuditTestResults(unique bool) (*results.SecurityCommandResults, validati } if unique { // Only count CVE findings, not impacted components - expected.Total.Vulnerabilities = 7 + expected.Total.Vulnerabilities = 8 expected.Vulnerabilities = &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 4, Iac: 1, Secrets: 2}, + ValidateScan: &validations.ScanCount{Sca: 4, Iac: 1, Services: 1, Secrets: 2}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 1, NotApplicable: 2, NotCovered: 1}, } } else { // Count all findings (pair of issueId+impactedComponent) - expected.Total.Vulnerabilities = 8 + expected.Total.Vulnerabilities = 9 expected.Vulnerabilities = &validations.VulnerabilityCount{ - ValidateScan: &validations.ScanCount{Sca: 5, Iac: 1, Secrets: 2}, + ValidateScan: &validations.ScanCount{Sca: 5, Iac: 1, Services: 1, Secrets: 2}, ValidateApplicabilityStatus: &validations.ApplicabilityStatusCount{Applicable: 1, NotApplicable: 3, NotCovered: 1}, } } @@ -364,6 +364,18 @@ func getAuditTestResults(unique bool) (*results.SecurityCommandResults, validati // No Violations []*sarif.Run{}, 0, ) + // Services scan results + npmTargetResults.AddJasScanResults(jasutils.Services, + []*sarif.Run{{ + Tool: &sarif.Tool{Driver: sarifutils.CreateDummyDriver(validations.ServicesToolName, validations.CreateDummyJasRule("GITHUB-ACTIONS-permissions-write-all"))}, + Invocations: []*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(filepath.Join("Users", "user", "project-with-issues")))}, + Results: []*sarif.Result{ + validations.CreateDummyJasResult("GITHUB-ACTIONS-permissions-write-all", severityutils.LevelError, formats.Location{File: filepath.Join("Users", "user", "project-with-issues", ".github", "workflows", "services.yml"), StartLine: 10, StartColumn: 1, EndLine: 10, EndColumn: 20, Snippet: "permissions: write-all"}), + }, + }}, + // No Violations + []*sarif.Run{}, 0, + ) // Secrets scan results npmTargetResults.AddJasScanResults(jasutils.Secrets, []*sarif.Run{{ diff --git a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go index ad3823f16..51193dfb9 100644 --- a/utils/results/conversion/cyclonedxparser/cyclonedxparser.go +++ b/utils/results/conversion/cyclonedxparser/cyclonedxparser.go @@ -207,6 +207,28 @@ func (cdc *CmdResultsCycloneDxConverter) ParseSecrets(secrets ...[]*sarif.Run) ( }) } +func (cdc *CmdResultsCycloneDxConverter) ParseServices(services ...[]*sarif.Run) (err error) { + if cdc.bom == nil { + return results.ErrResetConvertor + } + source := cdc.addJasService(services) + return results.ForEachJasIssue(results.CollectRuns(services...), cdc.entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (e error) { + affectedComponent := cdc.getOrCreateFileComponent(getRelativePath(location, cdc.currentTarget)) + // Create a new JAS vulnerability, add it to the BOM and return it + ratings := []cyclonedx.VulnerabilityRating{severityutils.CreateSeverityRating(severity, jasutils.Applicable, source)} + jasIssue := cdc.getOrCreateJasIssue(sarifutils.GetResultRuleId(result), sarifutils.GetRuleScannerId(rule), sarifutils.GetResultMsgText(result), sarifutils.GetRuleShortDescriptionText(rule), source, sarifutils.GetRuleCWE(rule), ratings) + // Add the location to the vulnerability + results.AddFileIssueAffects(jasIssue, *affectedComponent, cyclonedx.Property{ + Name: fmt.Sprintf( + jasIssueLocationPropertyTemplate, "services", affectedComponent.BOMRef, + sarifutils.GetLocationStartLine(location), sarifutils.GetLocationStartColumn(location), sarifutils.GetLocationEndLine(location), sarifutils.GetLocationEndColumn(location), + ), + Value: sarifutils.GetLocationSnippetText(location), + }) + 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..bda3900ef 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -49,6 +49,7 @@ var ( const ( ScaRun RunInJfrogReport = "sca" SecretsRun RunInJfrogReport = "secrets" + ServicesRun RunInJfrogReport = "services" IacRun RunInJfrogReport = "iac" SastRun RunInJfrogReport = "sast" ViolationsRun RunInJfrogReport = "violations" @@ -77,6 +78,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 +158,10 @@ func (sc *CmdResultsSarifConverter) flush() { if sc.currentTargetConvertedRuns.secretsCurrentRun != nil { sc.current.Runs = append(sc.current.Runs, sc.currentTargetConvertedRuns.secretsCurrentRun) } + // Flush services if needed + 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) @@ -297,6 +303,11 @@ func (sc *CmdResultsSarifConverter) ParseViolations(violationsScanResults violat iacResult, iacRule := createJasViolation(iacViolation) sc.addResultsToCurrentRun(ViolationsRun, []*sarif.ReportingDescriptor{iacRule}, iacResult) } + // Services violations + for _, servicesViolation := range violationsScanResults.Services { + servicesResult, servicesRule := createJasViolation(servicesViolation) + sc.addResultsToCurrentRun(ViolationsRun, []*sarif.ReportingDescriptor{servicesRule}, servicesResult) + } // Sast violations for _, sastViolation := range violationsScanResults.Sast { sastResult, sastRule := createJasViolation(sastViolation) @@ -436,6 +447,14 @@ func (sc *CmdResultsSarifConverter) ParseSecrets(secrets ...[]*sarif.Run) (err e return } +func (sc *CmdResultsSarifConverter) ParseServices(services ...[]*sarif.Run) (err error) { + if err = sc.validateBeforeParse(); err != nil || !sc.entitledForJas { + return + } + sc.currentTargetConvertedRuns.servicesCurrentRun = combineJasRunsToCurrentRun(sc.currentTargetConvertedRuns.servicesCurrentRun, patchSarifRuns(sc.getVulnerabilitiesConvertParams(utils.ServicesScan), results.CollectRuns(services...)...)...) + return +} + func (sc *CmdResultsSarifConverter) ParseIacs(iacs ...[]*sarif.Run) (err error) { if err = sc.validateBeforeParse(); err != nil || !sc.entitledForJas { return @@ -467,6 +486,8 @@ func (sc *CmdResultsSarifConverter) addResultsToCurrentRun(runType RunInJfrogRep currentRun = sc.currentTargetConvertedRuns.scaCurrentRun case SecretsRun: currentRun = sc.currentTargetConvertedRuns.secretsCurrentRun + case ServicesRun: + currentRun = sc.currentTargetConvertedRuns.servicesCurrentRun case IacRun: currentRun = sc.currentTargetConvertedRuns.iacCurrentRun case SastRun: @@ -930,6 +951,8 @@ func getResultViolationType(violationType string) utils.SubScanType { return utils.SecretsScan case violationutils.IacViolationType: return utils.IacScan + case violationutils.ServicesViolationType: + return utils.ServicesScan case violationutils.SastViolationType: return utils.SastScan default: @@ -948,6 +971,10 @@ func getScanType(defaultType utils.SubScanType, scanType string) utils.SubScanTy if strings.HasPrefix(scanType, "EXP") || strings.Contains(scanType, "SECRET") { return utils.SecretsScan } + // TODO: Add more rules to identify Services and validate them + if strings.Contains(scanType, "GITHUB-ACTIONS") { + return utils.ServicesScan + } // TODO: Add more rules to identify IAC // Default to SAST return utils.SastScan diff --git a/utils/results/conversion/sarifparser/sarifparser_test.go b/utils/results/conversion/sarifparser/sarifparser_test.go index 26cf25a7d..48ba7cca8 100644 --- a/utils/results/conversion/sarifparser/sarifparser_test.go +++ b/utils/results/conversion/sarifparser/sarifparser_test.go @@ -669,3 +669,10 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { }) } } + +func TestGetScanType(t *testing.T) { + assert.Equal(t, utils.ServicesScan, getScanType("", "GITHUB-ACTIONS-permissions-write-all")) + assert.Equal(t, utils.SecretsScan, getScanType("", "EXP-SECRET-TOKEN")) + assert.Equal(t, utils.SastScan, getScanType("", "aws_cloudfront_tls_only")) + assert.Equal(t, utils.IacScan, getScanType(utils.IacScan, "GITHUB-ACTIONS-permissions-write-all")) +} diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go index cedfc397d..9ed68c9fb 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -221,6 +221,12 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseViolations(violationsScanResults violation.ViolationContext = convertToViolationContext(jasViolation.Violation) sjc.current.SecretsViolations = append(sjc.current.SecretsViolations, violation) } + // Services Violations + for _, jasViolation := range violationsScanResults.Services { + violation := createSourceCodeRow(jasViolation.Rule, jasViolation.Severity, jasViolation.Result, jasViolation.Location, []*sarif.Invocation{}, sjc.pretty) + violation.ViolationContext = convertToViolationContext(jasViolation.Violation) + sjc.current.ServicesViolations = append(sjc.current.ServicesViolations, violation) + } // IaC Violations for _, jasViolation := range violationsScanResults.Iac { violation := createSourceCodeRow(jasViolation.Rule, jasViolation.Severity, jasViolation.Result, jasViolation.Location, []*sarif.Invocation{}, sjc.pretty) @@ -370,6 +376,21 @@ func (sjc *CmdResultsSimpleJsonConverter) ParseSecrets(secrets ...[]*sarif.Run) return } +func (sjc *CmdResultsSimpleJsonConverter) ParseServices(services ...[]*sarif.Run) (err error) { + if !sjc.entitledForJas { + return + } + if sjc.current == nil { + return results.ErrResetConvertor + } + servicesSimpleJson, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, results.CollectRuns(services...)...) + if err != nil || len(servicesSimpleJson) == 0 { + return + } + sjc.current.ServicesVulnerabilities = append(sjc.current.ServicesVulnerabilities, servicesSimpleJson...) + return +} + func (sjc *CmdResultsSimpleJsonConverter) ParseIacs(iacs ...[]*sarif.Run) (err error) { if !sjc.entitledForJas { return @@ -695,6 +716,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 +729,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/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go index 9fd9ec4c0..5cbeeb241 100644 --- a/utils/results/conversion/summaryparser/summaryparser.go +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -224,6 +224,7 @@ func (sc *CmdResultsSummaryConverter) ParseViolations(violations violationutils. sc.parseScaViolations(violations), sc.parseSecretsViolations(violations.Secrets), sc.parseIacViolations(violations.Iac), + sc.parseServicesViolations(violations.Services), sc.parseSastViolations(violations.Sast), ) } @@ -243,6 +244,20 @@ func (sc *CmdResultsSummaryConverter) ParseSecrets(secrets ...[]*sarif.Run) (err return results.ForEachJasIssue(results.CollectRuns(secrets...), sc.entitledForJas, sc.getJasHandler(jasutils.Secrets)) } +func (sc *CmdResultsSummaryConverter) ParseServices(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 sc.currentScan.Vulnerabilities.ServicesResults == nil { + sc.currentScan.Vulnerabilities.ServicesResults = &formats.ResultSummary{} + } + return results.ForEachJasIssue(results.CollectRuns(services...), sc.entitledForJas, sc.getJasHandler(jasutils.Services)) +} + func (sc *CmdResultsSummaryConverter) ParseIacs(iacs ...[]*sarif.Run) (err error) { if !sc.entitledForJas || sc.currentScan.Vulnerabilities == nil { // JAS results are only supported as vulnerabilities for now @@ -297,6 +312,8 @@ func (sc *CmdResultsSummaryConverter) getJasHandler(scanType jasutils.JasScanTyp resultStatus = tokenStatus } count = sc.currentScan.Vulnerabilities.SecretsResults + case jasutils.Services: + count = sc.currentScan.Vulnerabilities.ServicesResults case jasutils.IaC: count = sc.currentScan.Vulnerabilities.IacResults case jasutils.Sast: @@ -353,6 +370,20 @@ func (sc *CmdResultsSummaryConverter) parseIacViolations(iacViolations []violati return } +func (sc *CmdResultsSummaryConverter) parseServicesViolations(servicesViolations []violationutils.JasViolation) (err error) { + if err = sc.validateBeforeParse(); err != nil || sc.currentScan.Violations == nil { + return + } + if sc.currentScan.Violations.ServicesResults == nil { + sc.currentScan.Violations.ServicesResults = &formats.ResultSummary{} + } + for _, servicesViolation := range servicesViolations { + sc.currentScan.Violations.Watches = utils.UniqueUnion(sc.currentScan.Violations.Watches, servicesViolation.Watch) + countJasIssues(sc.currentScan.Violations.ServicesResults, servicesViolation.Location, servicesViolation.Severity, formats.NoStatus) + } + return +} + func (sc *CmdResultsSummaryConverter) parseSastViolations(sastViolations []violationutils.JasViolation) (err error) { if err = sc.validateBeforeParse(); err != nil || sc.currentScan.Violations == nil { return diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go index 81bcfcccc..efba18eaf 100644 --- a/utils/results/conversion/tableparser/tableparser.go +++ b/utils/results/conversion/tableparser/tableparser.go @@ -45,10 +45,12 @@ func (tc *CmdResultsTableConverter) Get() (formats.ResultsTables, error) { OperationalRiskViolationsTable: formats.ConvertToOperationalRiskViolationTableRow(simpleJsonFormat.OperationalRiskViolations), SecretsVulnerabilitiesTable: formats.ConvertToSecretsTableRow(simpleJsonFormat.SecretsVulnerabilities), SecretsViolationsTable: formats.ConvertToSecretsTableRow(simpleJsonFormat.SecretsViolations), - IacVulnerabilitiesTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.IacsVulnerabilities), - IacViolationsTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.IacsViolations), - SastVulnerabilitiesTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.SastVulnerabilities), - SastViolationsTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.SastViolations), + IacVulnerabilitiesTable: formats.ConvertToBasicJasTableRow(simpleJsonFormat.IacsVulnerabilities), + IacViolationsTable: formats.ConvertToBasicJasTableRow(simpleJsonFormat.IacsViolations), + ServicesVulnerabilitiesTable: formats.ConvertToBasicJasTableRow(simpleJsonFormat.ServicesVulnerabilities), + ServicesViolationsTable: formats.ConvertToBasicJasTableRow(simpleJsonFormat.ServicesViolations), + SastVulnerabilitiesTable: formats.ConvertToBasicJasTableRow(simpleJsonFormat.SastVulnerabilities), + SastViolationsTable: formats.ConvertToBasicJasTableRow(simpleJsonFormat.SastViolations), MaliciousVulnerabilitiesTable: formats.ConvertToMaliciousTableRow(simpleJsonFormat.MaliciousVulnerabilities), }, nil } @@ -86,6 +88,10 @@ func (tc *CmdResultsTableConverter) ParseSecrets(secrets ...[]*sarif.Run) (err e return tc.simpleJsonConvertor.ParseSecrets(secrets...) } +func (tc *CmdResultsTableConverter) ParseServices(services ...[]*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseServices(services...) +} + func (tc *CmdResultsTableConverter) ParseIacs(iacs ...[]*sarif.Run) (err error) { return tc.simpleJsonConvertor.ParseIacs(iacs...) } diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go index 90c9eb0b3..967e83e55 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -262,6 +262,9 @@ func (rw *ResultsWriter) printTables() (err error) { if err = rw.printJasTablesIfNeeded(tableContent, utils.IacScan, jasutils.IaC); err != nil { return } + if err = rw.printJasTablesIfNeeded(tableContent, utils.ServicesScan, jasutils.Services); err != nil { + return + } if err = rw.printJasTablesIfNeeded(tableContent, utils.SastScan, jasutils.Sast); err != nil { return } @@ -410,6 +413,14 @@ func PrintJasTable(tables formats.ResultsTables, entitledForJas bool, scanType j return coreutils.PrintTable(tables.IacVulnerabilitiesTable, "Infrastructure as Code Vulnerabilities", "✨ No Infrastructure as Code vulnerabilities were found ✨", false) } + case jasutils.Services: + if violations { + return coreutils.PrintTable(tables.ServicesViolationsTable, "Services Violations", + "✨ No services violations were found ✨", false) + } else { + return coreutils.PrintTable(tables.ServicesVulnerabilitiesTable, "Services Detection", + "✨ No services were found ✨", false) + } case jasutils.Sast: if violations { return coreutils.PrintTable(tables.SastViolationsTable, "Static Application Security Testing (SAST) Violations", diff --git a/utils/results/output/securityJobSummary.go b/utils/results/output/securityJobSummary.go index 05f742a0c..c628c9db8 100644 --- a/utils/results/output/securityJobSummary.go +++ b/utils/results/output/securityJobSummary.go @@ -631,6 +631,11 @@ func getResultsTypesSummaryString(index commandsummary.Index, violations bool, s content += TabTag.Format(fmt.Sprintf("%d %s", count, formats.SastResult.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.IacResults != nil { if count := summary.GetTotal(formats.IacResult); count > 0 { content += TabTag.Format(fmt.Sprintf("%d %s", count, formats.IacResult.String())) diff --git a/utils/results/results.go b/utils/results/results.go index 25a97db1a..6230f79ae 100644 --- a/utils/results/results.go +++ b/utils/results/results.go @@ -27,6 +27,7 @@ const ( CmdStepSca = "SCA Scan" CmdStepContextualAnalysis = "Contextual Analysis Enrichment" CmdStepIaC = "IaC Scan" + CmdStepServices = "Services Scan" CmdStepSecrets = "Secret Detection Scan" CmdStepSast = "Static Application Security Testing (SAST)" CmdStepMaliciousCode = "Malicious Code" @@ -127,6 +128,7 @@ type ResultsStatus struct { ContextualAnalysisStatusCode *int `json:"contextual_analysis,omitempty"` SecretsScanStatusCode *int `json:"secrets,omitempty"` IacScanStatusCode *int `json:"iac,omitempty"` + ServicesScanStatusCode *int `json:"services,omitempty"` SastScanStatusCode *int `json:"sast,omitempty"` MaliciousScanStatusCode *int `json:"malicious_code,omitempty"` ViolationsStatusCode *int `json:"violations,omitempty"` @@ -144,6 +146,8 @@ func (status *ResultsStatus) IsScanFailed(step SecurityCommandStep) bool { return isScanFailed(status.SecretsScanStatusCode) case CmdStepIaC: return isScanFailed(status.IacScanStatusCode) + case CmdStepServices: + return isScanFailed(status.ServicesScanStatusCode) case CmdStepSast: return isScanFailed(status.SastScanStatusCode) case CmdStepMaliciousCode: @@ -176,6 +180,10 @@ func (status *ResultsStatus) UpdateStatus(step SecurityCommandStep, statusCode * if shouldUpdateStatus(status.SecretsScanStatusCode, statusCode) { status.SecretsScanStatusCode = statusCode } + case CmdStepServices: + if shouldUpdateStatus(status.ServicesScanStatusCode, statusCode) { + status.ServicesScanStatusCode = statusCode + } case CmdStepIaC: if shouldUpdateStatus(status.IacScanStatusCode, statusCode) { status.IacScanStatusCode = statusCode @@ -233,6 +241,7 @@ type JasScansResults struct { type JasScanResults struct { SecretsScanResults []*sarif.Run `json:"secrets,omitempty"` IacScanResults []*sarif.Run `json:"iac,omitempty"` + ServicesScanResults []*sarif.Run `json:"services,omitempty"` SastScanResults []*sarif.Run `json:"sast,omitempty"` MaliciousScanResults []*sarif.Run `json:"malicious_code,omitempty"` } @@ -329,6 +338,10 @@ func (st ScanTarget) IsScanRequestedByCentralConfig(scanType utils.SubScanType) if module.ScanConfig.IacScannerConfig.EnableIacScan { return utils.NewBoolPtr(true) } + case utils.ServicesScan: + if module.ScanConfig.ServicesScannerConfig.EnableServicesScan { + return utils.NewBoolPtr(true) + } case utils.SecretsScan: if module.ScanConfig.SecretsScannerConfig.EnableSecretsScan { return utils.NewBoolPtr(true) @@ -370,6 +383,8 @@ func (st ScanTarget) GetCentralConfigExclusions(scanType utils.SubScanType) []st exclusions.AddElements(module.ScanConfig.ContextualAnalysisScannerConfig.ExcludePatterns...) case utils.IacScan: exclusions.AddElements(module.ScanConfig.IacScannerConfig.ExcludePatterns...) + case utils.ServicesScan: + exclusions.AddElements(module.ScanConfig.ServicesScannerConfig.ExcludePatterns...) case utils.SecretsScan: exclusions.AddElements(module.ScanConfig.SecretsScannerConfig.ExcludePatterns...) case utils.SastScan: @@ -453,6 +468,7 @@ func (r *SecurityCommandResults) IsJASRequested(requestedScans ...utils.SubScanT return utils.IsScanRequested(r.CmdType, utils.ContextualAnalysisScan, r.IsScanRequestedByCentralConfig(utils.ContextualAnalysisScan), requestedScans...) || utils.IsScanRequested(r.CmdType, utils.SecretsScan, r.IsScanRequestedByCentralConfig(utils.SecretsScan), requestedScans...) || utils.IsScanRequested(r.CmdType, utils.IacScan, r.IsScanRequestedByCentralConfig(utils.IacScan), requestedScans...) || + utils.IsScanRequested(r.CmdType, utils.ServicesScan, r.IsScanRequestedByCentralConfig(utils.ServicesScan), requestedScans...) || utils.IsScanRequested(r.CmdType, utils.SastScan, r.IsScanRequestedByCentralConfig(utils.SastScan), requestedScans...) } @@ -670,6 +686,7 @@ func (r *SecurityCommandResults) GetStatusCodes() ResultsStatus { status.UpdateStatus(CmdStepSca, targetResults.ResultsStatus.ScaScanStatusCode) status.UpdateStatus(CmdStepContextualAnalysis, targetResults.ResultsStatus.ContextualAnalysisStatusCode) status.UpdateStatus(CmdStepSecrets, targetResults.ResultsStatus.SecretsScanStatusCode) + status.UpdateStatus(CmdStepServices, targetResults.ResultsStatus.ServicesScanStatusCode) status.UpdateStatus(CmdStepIaC, targetResults.ResultsStatus.IacScanStatusCode) status.UpdateStatus(CmdStepSast, targetResults.ResultsStatus.SastScanStatusCode) status.UpdateStatus(CmdStepMaliciousCode, targetResults.ResultsStatus.MaliciousScanStatusCode) @@ -838,6 +855,12 @@ func (sr *TargetResults) AddJasScanResults(scanType jasutils.JasScanType, vulner sr.JasResults.JasVulnerabilities.SecretsScanResults = append(sr.JasResults.JasVulnerabilities.SecretsScanResults, vulnerabilitiesRuns...) sr.JasResults.JasViolations.SecretsScanResults = append(sr.JasResults.JasViolations.SecretsScanResults, violationsRuns...) } + 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...) + } case jasutils.IaC: sr.ResultsStatus.UpdateStatus(CmdStepIaC, &exitCode) if sr.JasResults != nil { @@ -935,6 +958,8 @@ func (jsr *JasScansResults) GetVulnerabilitiesResults(scanType jasutils.JasScanT switch scanType { case jasutils.Secrets: return jsr.JasVulnerabilities.SecretsScanResults + case jasutils.Services: + return jsr.JasVulnerabilities.ServicesScanResults case jasutils.IaC: return jsr.JasVulnerabilities.IacScanResults case jasutils.Sast: @@ -949,6 +974,8 @@ func (jsr *JasScansResults) GetViolationsResults(scanType jasutils.JasScanType) switch scanType { case jasutils.Secrets: return jsr.JasViolations.SecretsScanResults + case jasutils.Services: + return jsr.JasViolations.ServicesScanResults case jasutils.IaC: return jsr.JasViolations.IacScanResults case jasutils.Sast: diff --git a/utils/results/results_test.go b/utils/results/results_test.go index 2bb5cec9b..1f52faaee 100644 --- a/utils/results/results_test.go +++ b/utils/results/results_test.go @@ -73,6 +73,7 @@ func TestScanTarget_IsScanRequestedByCentralConfig(t *testing.T) { ScaScannerConfig: xscServices.ScaScannerConfig{EnableScaScan: true}, ContextualAnalysisScannerConfig: xscServices.CaScannerConfig{EnableCaScan: true}, IacScannerConfig: xscServices.IacScannerConfig{EnableIacScan: true}, + ServicesScannerConfig: xscServices.ServicesScannerConfig{EnableServicesScan: true}, SecretsScannerConfig: xscServices.SecretsScannerConfig{EnableSecretsScan: true}, SastScannerConfig: xscServices.SastScannerConfig{EnableSastScan: true}, }, @@ -102,6 +103,12 @@ func TestScanTarget_IsScanRequestedByCentralConfig(t *testing.T) { scanType: utils.IacScan, expected: utils.NewBoolPtr(true), }, + { + name: "Services enabled", + target: ScanTarget{CentralConfigModules: []xscServices.Module{enabledModule}}, + scanType: utils.ServicesScan, + expected: utils.NewBoolPtr(true), + }, { name: "Secrets enabled", target: ScanTarget{CentralConfigModules: []xscServices.Module{enabledModule}}, @@ -287,6 +294,16 @@ func TestScanTarget_GetCentralConfigExclusions(t *testing.T) { scanType: utils.IacScan, expected: []string{"**/test-infra/**"}, }, + { + name: "Services exclusions", + target: ScanTarget{CentralConfigModules: []xscServices.Module{{ + ScanConfig: xscServices.ScanConfig{ + ServicesScannerConfig: xscServices.ServicesScannerConfig{ExcludePatterns: []string{"**/.github/workflows/**"}}, + }, + }}}, + scanType: utils.ServicesScan, + expected: []string{"**/.github/workflows/**"}, + }, { name: "SAST exclusions", target: ScanTarget{CentralConfigModules: []xscServices.Module{{ diff --git a/utils/utils.go b/utils/utils.go index d252156f7..6028cc030 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -60,9 +60,9 @@ const ( var ( // Exclude pattern for files. - DefaultJasExcludePatterns = []string{"**/.git/**", "**/*test*/**", "**/*venv*/**", NodeModulesPattern, "**/target/**", "**/dist/**"} + DefaultJasExcludePatterns = []string{"**/*.git/**", "**/*test*/**", "**/*venv*/**", NodeModulesPattern, "**/target/**", "**/dist/**"} // Exclude pattern for directories. - DefaultScaExcludePatterns = []string{"*.git*", "*node_modules*", "*target*", "*venv*", "*test*", "dist"} + DefaultScaExcludePatterns = []string{"*.git", "*node_modules*", "*target*", "*venv*", "*test*", "dist"} ) const ( @@ -71,6 +71,7 @@ const ( IacScan SubScanType = "iac" SastScan SubScanType = "sast" SecretsScan SubScanType = "secrets" + ServicesScan SubScanType = "services" SecretTokenValidationScan SubScanType = "secrets_token_validation" MaliciousCodeScan SubScanType = "malicious_code" ) @@ -81,6 +82,7 @@ var subScanTypeToText = map[SubScanType]string{ IacScan: "IaC", SastScan: "SAST", SecretsScan: "Secrets", + ServicesScan: "Services", MaliciousCodeScan: "Malicious Code", } @@ -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, ServicesScan, SecretTokenValidationScan, MaliciousCodeScan} } // IsScanRequested returns true if the scan is requested, otherwise false. If requestedScans is empty, all scans are considered requested. @@ -121,7 +123,7 @@ 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 { @@ -132,7 +134,7 @@ func IsScanRequested(cmdType CommandType, subScan SubScanType, centralConfigRequ } func getScanFindingName(scanType SubScanType) string { - if scanType == SecretsScan { + if scanType == SecretsScan || scanType == ServicesScan { return fmt.Sprintf("%s exposures", subScanTypeToText[scanType]) } return fmt.Sprintf("%s vulnerabilities", subScanTypeToText[scanType]) diff --git a/utils/xsc/analyticsmetrics_test.go b/utils/xsc/analyticsmetrics_test.go index 27c0c990f..28e25b3e0 100644 --- a/utils/xsc/analyticsmetrics_test.go +++ b/utils/xsc/analyticsmetrics_test.go @@ -169,7 +169,7 @@ func TestCreateFinalizedEvent(t *testing.T) { name: "Valid audit result", auditResults: getDummyContentForGeneralEvent(true, false, false), expected: xscservices.XscAnalyticsGeneralEventFinalize{ - XscAnalyticsBasicGeneralEvent: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 7, EventStatus: xscservices.Completed}, + XscAnalyticsBasicGeneralEvent: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 9, EventStatus: xscservices.Completed}, }, }, { @@ -231,6 +231,10 @@ func getDummyContentForGeneralEvent(withJas, withErr, withResultContext bool) *r sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 0, 0, 0, 0, ""))), sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 1, 1, 1, 1, ""))), } + scanResults.JasResults.JasVulnerabilities.ServicesScanResults = []*sarif.Run{ + sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 0, 0, 0, 0, ""))), + sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 1, 1, 1, 1, ""))), + } scanResults.JasResults.JasVulnerabilities.SastScanResults = []*sarif.Run{ sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 0, 0, 0, 0, ""))), sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 1, 1, 1, 1, ""))), diff --git a/utils/xsc/configprofile_test.go b/utils/xsc/configprofile_test.go index ed7e04253..89e5532e3 100644 --- a/utils/xsc/configprofile_test.go +++ b/utils/xsc/configprofile_test.go @@ -187,6 +187,10 @@ func getComparisonConfigProfile() *services.ConfigProfile { EnableIacScan: true, ExcludePatterns: []string{"*.tfstate"}, }, + ServicesScannerConfig: services.ServicesScannerConfig{ + EnableServicesScan: true, + ExcludePatterns: []string{"**/build/**"}, + }, }, }, },