diff --git a/audit_test.go b/audit_test.go index 86c2829c..76e01a53 100644 --- a/audit_test.go +++ b/audit_test.go @@ -14,7 +14,8 @@ import ( "github.com/jfrog/jfrog-cli-security/cli" "github.com/jfrog/jfrog-cli-security/cli/docs" - "github.com/jfrog/jfrog-cli-security/formats" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/validations" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" @@ -35,12 +36,19 @@ import ( func TestXrayAuditNpmJson(t *testing.T) { output := testAuditNpm(t, string(format.Json), false) - securityTestUtils.VerifyJsonScanResults(t, output, 1, 0, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + SecurityViolations: 1, + Licenses: 1, + }) } func TestXrayAuditNpmSimpleJson(t *testing.T) { output := testAuditNpm(t, string(format.SimpleJson), true) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 1, 0, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + SecurityViolations: 1, + Vulnerabilities: 1, + Licenses: 1, + }) } func testAuditNpm(t *testing.T, format string, withVuln bool) string { @@ -67,12 +75,18 @@ func testAuditNpm(t *testing.T, format string, withVuln bool) string { func TestXrayAuditConanJson(t *testing.T) { output := testAuditConan(t, string(format.Json), true) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 8, 2) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 8, + Licenses: 2, + }) } func TestXrayAuditConanSimpleJson(t *testing.T) { output := testAuditConan(t, string(format.SimpleJson), true) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 8, 2) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 8, + Licenses: 2, + }) } func testAuditConan(t *testing.T, format string, withVuln bool) string { @@ -97,12 +111,18 @@ func testAuditConan(t *testing.T, format string, withVuln bool) string { func TestXrayAuditPnpmJson(t *testing.T) { output := testXrayAuditPnpm(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func TestXrayAuditPnpmSimpleJson(t *testing.T) { output := testXrayAuditPnpm(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func testXrayAuditPnpm(t *testing.T, format string) string { @@ -124,21 +144,30 @@ func testXrayAuditPnpm(t *testing.T, format string) string { func TestXrayAuditYarnV2Json(t *testing.T) { testXrayAuditYarn(t, "yarn-v2", func() { output := runXrayAuditYarnWithOutput(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) }) } func TestXrayAuditYarnV2SimpleJson(t *testing.T) { testXrayAuditYarn(t, "yarn-v3", func() { output := runXrayAuditYarnWithOutput(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) }) } func TestXrayAuditYarnV1Json(t *testing.T) { testXrayAuditYarn(t, "yarn-v1", func() { output := runXrayAuditYarnWithOutput(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) }) } @@ -157,7 +186,10 @@ func TestXrayAuditYarnV1JsonWithoutDevDependencies(t *testing.T) { func TestXrayAuditYarnV1SimpleJson(t *testing.T) { testXrayAuditYarn(t, "yarn-v1", func() { output := runXrayAuditYarnWithOutput(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) }) } @@ -231,7 +263,10 @@ func TestXrayAuditNugetJson(t *testing.T) { t.Run(fmt.Sprintf("projectName:%s,runInstallCommand:%t", test.projectName, runInstallCommand), func(t *testing.T) { output := testXrayAuditNuget(t, test.projectName, test.format, test.restoreTech) - securityTestUtils.VerifyJsonScanResults(t, output, 0, test.minVulnerabilities, test.minLicences) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: test.minVulnerabilities, + Licenses: test.minLicences, + }) }) } } @@ -271,7 +306,10 @@ func TestXrayAuditNugetSimpleJson(t *testing.T) { t.Run(fmt.Sprintf("projectName:%s,runInstallCommand:%t", test.projectName, runInstallCommand), func(t *testing.T) { output := testXrayAuditNuget(t, test.projectName, test.format, test.restoreTech) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, test.minVulnerabilities, test.minLicences) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: test.minVulnerabilities, + Licenses: test.minLicences, + }) }) } } @@ -297,12 +335,18 @@ func testXrayAuditNuget(t *testing.T, projectName, format string, restoreTech st func TestXrayAuditGradleJson(t *testing.T) { output := testXrayAuditGradle(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 3, 3) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 3, + }) } func TestXrayAuditGradleSimpleJson(t *testing.T) { output := testXrayAuditGradle(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 3, 3) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 3, + }) } func testXrayAuditGradle(t *testing.T, format string) string { @@ -321,12 +365,18 @@ func testXrayAuditGradle(t *testing.T, format string) string { func TestXrayAuditMavenJson(t *testing.T) { output := testXscAuditMaven(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func TestXrayAuditMavenSimpleJson(t *testing.T) { output := testXscAuditMaven(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func testXscAuditMaven(t *testing.T, format string) string { @@ -370,28 +420,44 @@ func TestXrayAuditMultiProjects(t *testing.T) { securityTestUtils.CreateJfrogHomeConfig(t, true) defer securityTestUtils.CleanTestsHomeEnv() output := securityTests.PlatformCli.WithoutCredentials().RunCliCmdWithOutput(t, "audit", "--format="+string(format.SimpleJson), workingDirsFlag) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 35, 0) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 6, 3, 0, 23, 2, 1, 0) + + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Sast: 1, + Iac: 9, + Secrets: 6, + + Vulnerabilities: 35, + Applicable: 3, + Undetermined: 0, + NotCovered: 23, + NotApplicable: 2, + }) } func TestXrayAuditPipJson(t *testing.T) { output := testXrayAuditPip(t, string(format.Json), "") - securityTestUtils.VerifyJsonScanResults(t, output, 0, 3, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 1, + }) } func TestXrayAuditPipSimpleJson(t *testing.T) { output := testXrayAuditPip(t, string(format.SimpleJson), "") - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 3, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 1, + }) } func TestXrayAuditPipJsonWithRequirementsFile(t *testing.T) { output := testXrayAuditPip(t, string(format.Json), "requirements.txt") - securityTestUtils.VerifyJsonScanResults(t, output, 0, 2, 0) + validations.VerifyJsonResults(t, output, validations.ValidationParams{Vulnerabilities: 2}) } func TestXrayAuditPipSimpleJsonWithRequirementsFile(t *testing.T) { output := testXrayAuditPip(t, string(format.SimpleJson), "requirements.txt") - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 2, 0) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{Vulnerabilities: 2}) } func testXrayAuditPip(t *testing.T, format, requirementsFile string) string { @@ -415,12 +481,18 @@ func testXrayAuditPip(t *testing.T, format, requirementsFile string) string { func TestXrayAuditPipenvJson(t *testing.T) { output := testXrayAuditPipenv(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 3, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 1, + }) } func TestXrayAuditPipenvSimpleJson(t *testing.T) { output := testXrayAuditPipenv(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 3, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 1, + }) } func testXrayAuditPipenv(t *testing.T, format string) string { @@ -439,12 +511,18 @@ func testXrayAuditPipenv(t *testing.T, format string) string { func TestXrayAuditPoetryJson(t *testing.T) { output := testXrayAuditPoetry(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 3, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 1, + }) } func TestXrayAuditPoetrySimpleJson(t *testing.T) { output := testXrayAuditPoetry(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 3, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 3, + Licenses: 1, + }) } func testXrayAuditPoetry(t *testing.T, format string) string { @@ -474,62 +552,24 @@ func addDummyPackageDescriptor(t *testing.T, hasPackageJson bool) { // JAS func TestXrayAuditSastCppFlagSimpleJson(t *testing.T) { - output := testAuditC(t, string(format.SimpleJson), true) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 0, 0, 0, 0, 0, 0, 0, 0) - + output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("package-managers", "c"), "3", false, true) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Sast: 1, + }) } func TestXrayAuditWithoutSastCppFlagSimpleJson(t *testing.T) { - output := testAuditC(t, string(format.SimpleJson), false) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, 0) -} - -// Helper for both C & Cpp Sast scans tests -func testAuditC(t *testing.T, format string, enableCppFlag bool) string { - cliToRun, cleanUp := securityTestUtils.InitTestWithMockCommandOrParams(t, getJasAuditMockCommand) - defer cleanUp() - securityTestUtils.InitSecurityTest(t, scangraph.GraphScanMinXrayVersion) - tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) - defer createTempDirCallback() - cProjectPath := filepath.Join(filepath.FromSlash(securityTestUtils.GetTestResourcesPath()), "projects", "package-managers", "c") - // Copy the c project from the testdata to a temp dir - assert.NoError(t, biutils.CopyDir(cProjectPath, tempDirPath, true, nil)) - prevWd := securityTestUtils.ChangeWD(t, tempDirPath) - defer clientTests.ChangeDirAndAssert(t, prevWd) - watchName, deleteWatch := securityTestUtils.CreateTestWatch(t, "audit-policy", "audit-watch", xrayUtils.High) - defer deleteWatch() - if enableCppFlag { - unsetEnv := clientTests.SetEnvWithCallbackAndAssert(t, "JFROG_SAST_ENABLE_CPP", "1") - defer unsetEnv() - } - args := []string{"audit", "--licenses", "--vuln", "--format=" + format, "--watches=" + watchName, "--fail=false"} - return cliToRun.WithoutCredentials().RunCliCmdWithOutput(t, args...) + output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("package-managers", "c"), "3", false, false) + // verify no results for Sast + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{}) } func TestXrayAuditNotEntitledForJas(t *testing.T) { cliToRun, cleanUp := securityTestUtils.InitTestWithMockCommandOrParams(t, getNoJasAuditMockCommand) defer cleanUp() - output := testXrayAuditJas(t, cliToRun, filepath.Join("jas", "jas"), "3", false) - // Verify that scan results are printed - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 8, 0) - // Verify that JAS results are not printed - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, 0) -} - -func getJasAuditMockCommand() components.Command { - return components.Command{ - Name: docs.Audit, - Flags: docs.GetCommandFlags(docs.Audit), - Action: func(c *components.Context) error { - auditCmd, err := cli.CreateAuditCmd(c) - if err != nil { - return err - } - // Disable Jas for this test - auditCmd.SetUseJas(true) - return progressbar.ExecWithProgress(auditCmd) - }, - } + output := testXrayAuditJas(t, cliToRun, filepath.Join("jas", "jas"), "3", false, false) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{Vulnerabilities: 8}) } func getNoJasAuditMockCommand() components.Command { @@ -549,35 +589,60 @@ func getNoJasAuditMockCommand() components.Command { } func TestXrayAuditJasSimpleJson(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "3", false) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 8, 0) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 6, 3, 1, 1, 2, 0, 0) + output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "3", false, false) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Sast: 1, + Iac: 9, + Secrets: 6, + + Vulnerabilities: 8, + Applicable: 3, + Undetermined: 1, + NotCovered: 1, + NotApplicable: 2, + }) } func TestXrayAuditJasSimpleJsonWithTokenValidation(t *testing.T) { securityTestUtils.InitSecurityTest(t, jasutils.DynamicTokenValidationMinXrayVersion) - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "3", true) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, 5) + output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "3", true, false) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{Inactive: 5}) } func TestXrayAuditJasSimpleJsonWithOneThread(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "1", false) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 8, 0) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 6, 3, 1, 1, 2, 0, 0) + output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "1", false, false) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Sast: 1, + Iac: 9, + Secrets: 6, + + Vulnerabilities: 8, + Applicable: 3, + Undetermined: 1, + NotCovered: 1, + NotApplicable: 2, + }) } func TestXrayAuditJasSimpleJsonWithConfig(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas-config"), "3", false) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 1, 3, 1, 1, 2, 0, 0) + output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas-config"), "3", false, false) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Secrets: 1, + + Vulnerabilities: 8, + Applicable: 3, + Undetermined: 1, + NotCovered: 1, + NotApplicable: 2, + }) } func TestXrayAuditJasNoViolationsSimpleJson(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("package-managers", "npm", "npm"), "3", false) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 1, 0) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 1, 0, 0) + output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("package-managers", "npm", "npm"), "3", false, false) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{Vulnerabilities: 1, NotApplicable: 1}) } -func testXrayAuditJas(t *testing.T, testCli *coreTests.JfrogCli, project string, threads string, validateSecrets bool) string { +func testXrayAuditJas(t *testing.T, testCli *coreTests.JfrogCli, project string, threads string, validateSecrets, validateSastCpp bool) string { securityTestUtils.InitSecurityTest(t, scangraph.GraphScanMinXrayVersion) tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) defer createTempDirCallback() @@ -595,6 +660,10 @@ func testXrayAuditJas(t *testing.T, testCli *coreTests.JfrogCli, project string, if validateSecrets { args = append(args, "--secrets", "--validate-secrets") } + if validateSastCpp { + unsetEnv := clientTests.SetEnvWithCallbackAndAssert(t, "JFROG_SAST_ENABLE_CPP", "1") + defer unsetEnv() + } return testCli.WithoutCredentials().RunCliCmdWithOutput(t, args...) } @@ -643,7 +712,7 @@ func TestXrayRecursiveScan(t *testing.T) { output := securityTests.PlatformCli.RunCliCmdWithOutput(t, "audit", "--format=json") // We anticipate the identification of five vulnerabilities: four originating from the .NET project and one from the NPM project. - securityTestUtils.VerifyJsonScanResults(t, output, 0, 4, 0) + validations.VerifyJsonResults(t, output, validations.ValidationParams{Vulnerabilities: 4}) var results []services.ScanResponse err = json.Unmarshal([]byte(output), &results) @@ -667,5 +736,6 @@ func TestAuditOnEmptyProject(t *testing.T) { chdirCallback := clientTests.ChangeDirWithCallback(t, baseWd, tempDirPath) defer chdirCallback() output := securityTests.PlatformCli.WithoutCredentials().RunCliCmdWithOutput(t, "audit", "--format="+string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, 0) + // No issues should be found in an empty project + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{}) } diff --git a/cli/docs/flags.go b/cli/docs/flags.go index 55d639b2..00258de7 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -121,6 +121,7 @@ const ( WorkingDirs = "working-dirs" OutputDir = "output-dir" SkipAutoInstall = "skip-auto-install" + AllowPartialResults = "allow-partial-results" // Unique curation flags CurationOutput = "curation-format" @@ -157,7 +158,7 @@ var commandFlags = map[string][]string{ url, user, password, accessToken, ServerId, InsecureTls, Project, Watches, RepoPath, Licenses, OutputFormat, ExcludeTestDeps, useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm, Pnpm, Yarn, Go, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, Threads, - Sca, Iac, Sast, Secrets, WithoutCA, ScanVuln, SecretValidation, OutputDir, SkipAutoInstall, + Sca, Iac, Sast, Secrets, WithoutCA, ScanVuln, SecretValidation, OutputDir, SkipAutoInstall, AllowPartialResults, }, CurationAudit: { CurationOutput, WorkingDirs, Threads, RequirementsFile, @@ -232,9 +233,10 @@ var flagsMap = map[string]components.Flag{ "Set to false if you wish to not use the gradle or maven wrapper.", components.WithBoolDefaultValue(true), ), - WorkingDirs: components.NewStringFlag(WorkingDirs, "A comma-separated list of relative working directories, to determine audit targets locations."), - OutputDir: components.NewStringFlag(OutputDir, "Target directory to save partial results to.", components.SetHiddenStrFlag()), - SkipAutoInstall: components.NewBoolFlag(SkipAutoInstall, "Set to true to skip auto-install of dependencies in un-built modules. Currently supported for Yarn and NPM only.", components.SetHiddenBoolFlag()), + WorkingDirs: components.NewStringFlag(WorkingDirs, "A comma-separated list of relative working directories, to determine audit targets locations."), + OutputDir: components.NewStringFlag(OutputDir, "Target directory to save partial results to.", components.SetHiddenStrFlag()), + SkipAutoInstall: components.NewBoolFlag(SkipAutoInstall, "Set to true to skip auto-install of dependencies in un-built modules. Currently supported for Yarn and NPM only.", components.SetHiddenBoolFlag()), + AllowPartialResults: components.NewBoolFlag(AllowPartialResults, "Set to true to allow partial results and continuance of the scan in case of certain errors.", components.SetHiddenBoolFlag()), ExclusionsAudit: components.NewStringFlag( Exclusions, "List of exclusions separated by semicolons, utilized to skip sub-projects from undergoing an audit. These exclusions may incorporate the * and ? wildcards.", diff --git a/cli/scancommands.go b/cli/scancommands.go index 0099a4d5..93dd483b 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -478,7 +478,8 @@ func CreateAuditCmd(c *components.Context) (*audit.AuditCommand, error) { SetFixableOnly(c.GetBoolFlagValue(flags.FixableOnly)). SetThirdPartyApplicabilityScan(c.GetBoolFlagValue(flags.ThirdPartyContextualAnalysis)). SetScansResultsOutputDir(scansOutputDir). - SetSkipAutoInstall(c.GetBoolFlagValue(flags.SkipAutoInstall)) + SetSkipAutoInstall(c.GetBoolFlagValue(flags.SkipAutoInstall)). + SetAllowPartialResults(c.GetBoolFlagValue(flags.AllowPartialResults)) if c.GetStringFlagValue(flags.Watches) != "" { auditCmd.SetWatches(splitByCommaAndTrim(c.GetStringFlagValue(flags.Watches))) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 85ec44f2..74fe8846 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -4,20 +4,26 @@ import ( "errors" "fmt" - "github.com/jfrog/gofrog/log" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-security/commands/audit/sca" "github.com/jfrog/jfrog-cli-security/jas" "github.com/jfrog/jfrog-cli-security/jas/applicability" "github.com/jfrog/jfrog-cli-security/jas/runner" "github.com/jfrog/jfrog-cli-security/jas/secrets" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/output" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" "github.com/jfrog/jfrog-cli-security/utils/xsc" "golang.org/x/exp/slices" xrayutils "github.com/jfrog/jfrog-cli-security/utils/xray" clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray" "github.com/jfrog/jfrog-client-go/xray/services" xscservices "github.com/jfrog/jfrog-client-go/xsc/services" @@ -130,11 +136,10 @@ func (auditCmd *AuditCommand) Run() (err error) { } } var messages []string - if !auditResults.ExtendedScanResults.EntitledForJas { - messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'IaC Scan' and ‘SAST’.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink("https://jfrog.com/xray/")} + if !auditResults.EntitledForJas { + messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'IaC Scan' and ‘SAST’.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink(utils.JasInfoURL)} } - if err = utils.NewResultsWriter(auditResults). - SetIsMultipleRootProject(auditResults.IsMultipleProject()). + if err = output.NewResultsWriter(auditResults). SetHasViolationContext(auditCmd.HasViolationContext()). SetIncludeVulnerabilities(auditCmd.IncludeVulnerabilities). SetIncludeLicenses(auditCmd.IncludeLicenses). @@ -146,13 +151,13 @@ func (auditCmd *AuditCommand) Run() (err error) { return } - if auditResults.ScansErr != nil { - return auditResults.ScansErr + if err = auditResults.GetErrors(); err != nil { + return } // Only in case Xray's context was given (!auditCmd.IncludeVulnerabilities), and the user asked to fail the build accordingly, do so. - if auditCmd.Fail && !auditCmd.IncludeVulnerabilities && utils.CheckIfFailBuild(auditResults.GetScaScansXrayResults()) { - err = utils.NewFailBuildError() + if auditCmd.Fail && !auditCmd.IncludeVulnerabilities && results.CheckIfFailBuild(auditResults.GetScaScansXrayResults()) { + err = results.NewFailBuildError() } return } @@ -168,9 +173,8 @@ func (auditCmd *AuditCommand) HasViolationContext() bool { // Runs an audit scan based on the provided auditParams. // Returns an audit Results object containing all the scan results. // If the current server is entitled for JAS, the advanced security results will be included in the scan results. -func RunAudit(auditParams *AuditParams) (results *utils.Results, err error) { - // Initialize Results struct - results = utils.NewAuditResults(utils.SourceCode) +func RunAudit(auditParams *AuditParams) (cmdResults *results.SecurityCommandResults, err error) { + // Prepare serverDetails, err := auditParams.ServerDetails() if err != nil { return @@ -182,27 +186,36 @@ func RunAudit(auditParams *AuditParams) (results *utils.Results, err error) { if err = clientutils.ValidateMinimumVersion(clientutils.Xray, auditParams.xrayVersion, scangraph.GraphScanMinXrayVersion); err != nil { return } - results.XrayVersion = auditParams.xrayVersion - results.ExtendedScanResults.EntitledForJas, err = isEntitledForJas(xrayManager, auditParams) + entitledForJas, err := isEntitledForJas(xrayManager, auditParams) if err != nil { return } - results.ExtendedScanResults.SecretValidation = jas.CheckForSecretValidation(xrayManager, auditParams.xrayVersion, slices.Contains(auditParams.AuditBasicParams.ScansToPerform(), utils.SecretTokenValidationScan)) - results.MultiScanId = auditParams.commonGraphScanParams.MultiScanId - auditParallelRunner := utils.CreateSecurityParallelRunner(auditParams.threads) - auditParallelRunner.ErrWg.Add(1) - jfrogAppsConfig, err := jas.CreateJFrogAppsConfig(auditParams.workingDirs) + // Initialize Results struct + cmdResults = initCmdResults( + entitledForJas, + jas.CheckForSecretValidation(xrayManager, auditParams.xrayVersion, slices.Contains(auditParams.AuditBasicParams.ScansToPerform(), utils.SecretTokenValidationScan)), + auditParams, + ) + jfrogAppsConfig, err := jas.CreateJFrogAppsConfig(cmdResults.GetTargetsPaths()) if err != nil { - return results, fmt.Errorf("failed to create JFrogAppsConfig: %s", err.Error()) + return cmdResults, fmt.Errorf("failed to create JFrogAppsConfig: %s", err.Error()) } + // Initialize the parallel runner + auditParallelRunner := utils.CreateSecurityParallelRunner(auditParams.threads) + auditParallelRunner.ErrWg.Add(1) + // Add the JAS scans to the parallel runner var jasScanner *jas.JasScanner var jasScanErr error - if jasScanner, jasScanErr = RunJasScans(auditParallelRunner, auditParams, results, jfrogAppsConfig); jasScanErr != nil { + if jasScanner, jasScanErr = RunJasScans(auditParallelRunner, auditParams, cmdResults, jfrogAppsConfig); jasScanErr != nil { auditParallelRunner.AddErrorToChan(jasScanErr) } + if auditParams.Progress() != nil { + auditParams.Progress().SetHeadlineMsg("Scanning for issues") + } // The sca scan doesn't require the analyzer manager, so it can run separately from the analyzer manager download routine. - if scaScanErr := buildDepTreeAndRunScaScan(auditParallelRunner, auditParams, results); scaScanErr != nil { - auditParallelRunner.AddErrorToChan(scaScanErr) + if scaScanErr := buildDepTreeAndRunScaScan(auditParallelRunner, auditParams, cmdResults); scaScanErr != nil { + // If error to be caught, we add it to the auditParallelRunner error queue and continue. The error need not be returned + _ = createErrorIfPartialResultsDisabled(auditParams, auditParallelRunner, fmt.Sprintf("An error has occurred during SCA scan process. SCA scan is skipped for the following directories: %s.", auditParams.workingDirs), scaScanErr) } go func() { auditParallelRunner.ScaScansWg.Wait() @@ -219,12 +232,9 @@ func RunAudit(auditParams *AuditParams) (results *utils.Results, err error) { go func() { defer auditParallelRunner.ErrWg.Done() for e := range auditParallelRunner.ErrorsQueue { - results.ScansErr = errors.Join(results.ScansErr, e) + cmdResults.Error = errors.Join(cmdResults.Error, e) } }() - if auditParams.Progress() != nil { - auditParams.Progress().SetHeadlineMsg("Scanning for issues") - } auditParallelRunner.Runner.Run() auditParallelRunner.ErrWg.Wait() return @@ -238,8 +248,8 @@ func isEntitledForJas(xrayManager *xray.XrayServicesManager, auditParams *AuditP return jas.IsEntitledForJas(xrayManager, auditParams.xrayVersion) } -func RunJasScans(auditParallelRunner *utils.SecurityParallelRunner, auditParams *AuditParams, results *utils.Results, jfrogAppsConfig *jfrogappsconfig.JFrogAppsConfig) (jasScanner *jas.JasScanner, err error) { - if !results.ExtendedScanResults.EntitledForJas { +func RunJasScans(auditParallelRunner *utils.SecurityParallelRunner, auditParams *AuditParams, scanResults *results.SecurityCommandResults, jfrogAppsConfig *jfrogappsconfig.JFrogAppsConfig) (jasScanner *jas.JasScanner, err error) { + if !scanResults.EntitledForJas { log.Info("Not entitled for JAS, skipping advance security scans...") return } @@ -248,7 +258,9 @@ func RunJasScans(auditParallelRunner *utils.SecurityParallelRunner, auditParams err = fmt.Errorf("failed to get server details: %s", err.Error()) return } - jasScanner, err = jas.CreateJasScanner(jfrogAppsConfig, serverDetails, auditParams.minSeverityFilter, jas.GetAnalyzerManagerXscEnvVars(auditParams.commonGraphScanParams.MultiScanId, results.ExtendedScanResults.SecretValidation, results.GetScaScannedTechnologies()...), auditParams.Exclusions()...) + auditParallelRunner.ResultsMu.Lock() + jasScanner, err = jas.CreateJasScanner(serverDetails, scanResults.SecretValidation, auditParams.minSeverityFilter, jas.GetAnalyzerManagerXscEnvVars(auditParams.commonGraphScanParams.MultiScanId, scanResults.GetTechnologies()...), auditParams.Exclusions()...) + auditParallelRunner.ResultsMu.Unlock() if err != nil { err = fmt.Errorf("failed to create jas scanner: %s", err.Error()) return @@ -258,22 +270,115 @@ func RunJasScans(auditParallelRunner *utils.SecurityParallelRunner, auditParams } auditParallelRunner.JasWg.Add(1) if _, jasErr := auditParallelRunner.Runner.AddTaskWithError(func(threadId int) error { - return downloadAnalyzerManagerAndRunScanners(auditParallelRunner, jasScanner, results, auditParams, threadId) + return downloadAnalyzerManagerAndRunScanners(auditParallelRunner, scanResults, serverDetails, auditParams, jasScanner, jfrogAppsConfig, threadId) }, auditParallelRunner.AddErrorToChan); jasErr != nil { auditParallelRunner.AddErrorToChan(fmt.Errorf("failed to create AM downloading task, skipping JAS scans...: %s", jasErr.Error())) } return } -func downloadAnalyzerManagerAndRunScanners(auditParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, scanResults *utils.Results, auditParams *AuditParams, threadId int) (err error) { +func downloadAnalyzerManagerAndRunScanners(auditParallelRunner *utils.SecurityParallelRunner, scanResults *results.SecurityCommandResults, + serverDetails *config.ServerDetails, auditParams *AuditParams, scanner *jas.JasScanner, jfrogAppsConfig *jfrogappsconfig.JFrogAppsConfig, threadId int) (err error) { defer func() { auditParallelRunner.JasWg.Done() }() if err = jas.DownloadAnalyzerManagerIfNeeded(threadId); err != nil { return fmt.Errorf("%s failed to download analyzer manager: %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) } - if err = runner.AddJasScannersTasks(auditParallelRunner, scanResults, auditParams.DirectDependencies(), auditParams.thirdPartyApplicabilityScan, scanner, applicability.ApplicabilityScannerType, secrets.SecretsScannerType, auditParallelRunner.AddErrorToChan, auditParams.ScansToPerform(), auditParams.configProfile, auditParams.scanResultsOutputDir); err != nil { - return fmt.Errorf("%s failed to run JAS scanners: %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) + // Run JAS scanners for each scan target + for _, scan := range scanResults.Targets { + module := jas.GetModule(scan.Target, jfrogAppsConfig) + if module == nil { + scan.AddError(fmt.Errorf("can't find module for path %s", scan.Target)) + continue + } + params := runner.JasRunnerParams{ + Runner: auditParallelRunner, + ServerDetails: serverDetails, + Scanner: scanner, + Module: *module, + ConfigProfile: auditParams.configProfile, + ScansToPreform: auditParams.ScansToPerform(), + SecretsScanType: secrets.SecretsScannerType, + DirectDependencies: auditParams.DirectDependencies(), + ThirdPartyApplicabilityScan: auditParams.thirdPartyApplicabilityScan, + ApplicableScanType: applicability.ApplicabilityScannerType, + ScanResults: scan, + TargetOutputDir: auditParams.scanResultsOutputDir, + } + if err = runner.AddJasScannersTasks(params); err != nil { + return fmt.Errorf("%s failed to run JAS scanners: %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) + } } return } + +func initCmdResults(entitledForJas, secretValidation bool, params *AuditParams) (cmdResults *results.SecurityCommandResults) { + cmdResults = results.NewCommandResults(utils.SourceCode, params.xrayVersion, entitledForJas, secretValidation).SetMultiScanId(params.commonGraphScanParams.MultiScanId) + detectScanTargets(cmdResults, params) + if params.IsRecursiveScan() && len(params.workingDirs) == 1 && len(cmdResults.Targets) == 0 { + // No SCA targets were detected, add the root directory as a target for JAS scans. + cmdResults.NewScanResults(results.ScanTarget{Target: params.workingDirs[0]}) + } + scanInfo, err := coreutils.GetJsonIndent(cmdResults) + if err != nil { + return + } + log.Info(fmt.Sprintf("Preforming scans on %d targets:\n%s", len(cmdResults.Targets), scanInfo)) + return +} + +func detectScanTargets(cmdResults *results.SecurityCommandResults, params *AuditParams) { + for _, requestedDirectory := range params.workingDirs { + if !fileutils.IsPathExists(requestedDirectory, false) { + log.Warn("The working directory", requestedDirectory, "doesn't exist. Skipping SCA scan...") + continue + } + // Detect descriptors and technologies in the requested directory. + techToWorkingDirs, err := techutils.DetectTechnologiesDescriptors(requestedDirectory, params.IsRecursiveScan(), params.Technologies(), getRequestedDescriptors(params), sca.GetExcludePattern(params.AuditBasicParams)) + if err != nil { + log.Warn("Couldn't detect technologies in", requestedDirectory, "directory.", err.Error()) + continue + } + // Create scans to preform + for tech, workingDirs := range techToWorkingDirs { + if tech == techutils.Dotnet { + // We detect Dotnet and Nuget the same way, if one detected so does the other. + // We don't need to scan for both and get duplicate results. + continue + } + if len(workingDirs) == 0 { + // Requested technology (from params) descriptors/indicators were not found, scan only requested directory for this technology. + cmdResults.NewScanResults(results.ScanTarget{Target: requestedDirectory, Technology: tech}) + } + for workingDir, descriptors := range workingDirs { + // Add scan for each detected working directory. + cmdResults.NewScanResults(results.ScanTarget{Target: workingDir, Technology: tech}).SetDescriptors(descriptors...) + } + } + } +} + +// This function checks if partial results are allowed. If so we log the error and continue. +// If partial results are not allowed and a SecurityParallelRunner is provided we add the error to its error queue and return without an error, since the errors will be later collected from the queue. +// If partial results are not allowed and a SecurityParallelRunner is not provided we return the error. +func createErrorIfPartialResultsDisabled(auditParams *AuditParams, auditParallelRunner *utils.SecurityParallelRunner, extraMassageForLog string, err error) error { + if err == nil { + return nil + } + + if auditParams.AllowPartialResults() { + if extraMassageForLog == "" { + extraMassageForLog = "An error has occurred during the audit scans" + } + log.Warn(fmt.Sprintf("%s\nSince partial results are allowed, the error is skipped: %s", extraMassageForLog, err.Error())) + return nil + } + + // When SecurityParallelRunner is provided we add the error to the queue, otherwise we return the error + if auditParallelRunner != nil { + auditParallelRunner.AddErrorToChan(err) + return nil + } + return err +} diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 2f86776c..54960817 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -1,21 +1,196 @@ package audit import ( + "errors" + "fmt" + "net/http" "path/filepath" + "sort" "strings" "testing" + "github.com/stretchr/testify/assert" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion" + "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-cli-security/utils/validations" + "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/format" coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - "github.com/jfrog/jfrog-cli-security/utils" - "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" scanservices "github.com/jfrog/jfrog-client-go/xray/services" "github.com/jfrog/jfrog-client-go/xsc/services" - "github.com/stretchr/testify/assert" ) +func TestDetectScansToPreform(t *testing.T) { + + dir, cleanUp := createTestDir(t) + + tests := []struct { + name string + wd string + params func() *AuditParams + expected []*results.TargetResults + }{ + { + name: "Test specific technologies", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{dir}) + param.SetTechnologies([]string{"maven", "npm", "go"}).SetIsRecursiveScan(true) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Maven, + Target: filepath.Join(dir, "dir", "maven"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{ + filepath.Join(dir, "dir", "maven", "pom.xml"), + filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), + filepath.Join(dir, "dir", "maven", "maven-sub2", "pom.xml"), + }, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Npm, + Target: filepath.Join(dir, "dir", "npm"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Go, + Target: filepath.Join(dir, "dir", "go"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, + }, + }, + }, + }, + { + name: "Test all", + wd: dir, + params: func() *AuditParams { + param := NewAuditParams().SetWorkingDirs([]string{dir}) + param.SetIsRecursiveScan(true) + return param + }, + expected: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Maven, + Target: filepath.Join(dir, "dir", "maven"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{ + filepath.Join(dir, "dir", "maven", "pom.xml"), + filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), + filepath.Join(dir, "dir", "maven", "maven-sub2", "pom.xml"), + }, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Npm, + Target: filepath.Join(dir, "dir", "npm"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Go, + Target: filepath.Join(dir, "dir", "go"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Yarn, + Target: filepath.Join(dir, "yarn"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "yarn", "package.json")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Pip, + Target: filepath.Join(dir, "yarn", "Pip"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "yarn", "Pip", "requirements.txt")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Pipenv, + Target: filepath.Join(dir, "yarn", "Pipenv"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "yarn", "Pipenv", "Pipfile")}, + }, + }, + { + ScanTarget: results.ScanTarget{ + Technology: techutils.Nuget, + Target: filepath.Join(dir, "Nuget"), + }, + JasResults: &results.JasScansResults{}, + ScaResults: &results.ScaScanResults{ + Descriptors: []string{filepath.Join(dir, "Nuget", "project.sln"), filepath.Join(dir, "Nuget", "Nuget-sub", "project.csproj")}, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + results := results.NewCommandResults(utils.SourceCode, "", true, true) + detectScanTargets(results, test.params()) + if assert.Len(t, results.Targets, len(test.expected)) { + for i := range results.Targets { + if results.Targets[i].ScaResults != nil { + sort.Strings(results.Targets[i].ScaResults.Descriptors) + } + if test.expected[i].ScaResults != nil { + sort.Strings(test.expected[i].ScaResults.Descriptors) + } + } + } + assert.ElementsMatch(t, test.expected, results.Targets) + }) + } + + cleanUp() +} + // Note: Currently, if a config profile is provided, the scan will use the profile's settings, IGNORING jfrog-apps-config if exists. func TestAuditWithConfigProfile(t *testing.T) { testcases := []struct { @@ -94,7 +269,7 @@ func TestAuditWithConfigProfile(t *testing.T) { for _, testcase := range testcases { t.Run(testcase.name, func(t *testing.T) { - mockServer, serverDetails := utils.XrayServer(t, utils.EntitlementsMinVersion) + mockServer, serverDetails := validations.XrayServer(t, utils.EntitlementsMinVersion) defer mockServer.Close() tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) @@ -119,36 +294,23 @@ func TestAuditWithConfigProfile(t *testing.T) { XscVersion: services.ConfigProfileMinXscVersion, MultiScanId: "random-msi", }) - auditParams.SetIsRecursiveScan(true) + auditParams.SetWorkingDirs([]string{tempDirPath}).SetIsRecursiveScan(true) auditResults, err := RunAudit(auditParams) assert.NoError(t, err) // Currently, the only supported scanners are Secrets and Sast, therefore if a config profile is utilized - all other scanners are disabled. - if testcase.expectedSastIssues > 0 { - assert.NotNil(t, auditResults.ExtendedScanResults.SastScanResults) - assert.Equal(t, testcase.expectedSastIssues, len(auditResults.ExtendedScanResults.SastScanResults[0].Results)) - } else { - assert.Nil(t, auditResults.ExtendedScanResults.SastScanResults) - } - - if testcase.expectedSecretsIssues > 0 { - assert.NotNil(t, auditResults.ExtendedScanResults.SecretsScanResults) - assert.Equal(t, testcase.expectedSecretsIssues, len(auditResults.ExtendedScanResults.SecretsScanResults[0].Results)) - } else { - assert.Nil(t, auditResults.ExtendedScanResults.SecretsScanResults) - } - - assert.Nil(t, auditResults.ScaResults) - assert.Nil(t, auditResults.ExtendedScanResults.ApplicabilityScanResults) - assert.Nil(t, auditResults.ExtendedScanResults.IacScanResults) + summary, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: true}).ConvertToSummary(auditResults) + assert.NoError(t, err) + // Validate Sast and Secrets have the expected number of issues and that Iac and Sca did not run + validations.ValidateCommandSummaryOutput(t, validations.ValidationParams{Actual: summary, ExactResultsMatch: true, Sast: testcase.expectedSastIssues, Secrets: testcase.expectedSecretsIssues, Vulnerabilities: testcase.expectedSastIssues + testcase.expectedSecretsIssues}) }) } } // This test tests audit flow when providing --output-dir flag func TestAuditWithScansOutputDir(t *testing.T) { - mockServer, serverDetails := utils.XrayServer(t, utils.EntitlementsMinVersion) + mockServer, serverDetails := validations.XrayServer(t, utils.EntitlementsMinVersion) defer mockServer.Close() outputDirPath, removeOutputDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) @@ -170,7 +332,7 @@ func TestAuditWithScansOutputDir(t *testing.T) { SetCommonGraphScanParams(&scangraph.CommonGraphScanParams{ ScanType: scanservices.Dependency, IncludeVulnerabilities: true, - MultiScanId: utils.TestScaScanId, + MultiScanId: validations.TestScaScanId, }). SetScansResultsOutputDir(outputDirPath) auditParams.SetIsRecursiveScan(true) @@ -197,3 +359,144 @@ func searchForStrWithSubString(t *testing.T, filesList []string, subString strin } assert.Fail(t, "File %s not found in the list", subString) } + +func TestAuditWithPartialResults(t *testing.T) { + testcases := []struct { + name string + allowPartialResults bool + useJas bool + testDirPath string + }{ + { + name: "Failure in SCA during dependency tree construction", + allowPartialResults: false, + useJas: false, + testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-un-installable"), + }, + { + name: "Failure in SCA during scan itself", + allowPartialResults: false, + useJas: false, + testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-project"), + }, + { + name: "Skip failure in SCA during dependency tree construction", + allowPartialResults: true, + useJas: false, + testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-un-installable"), + }, + { + name: "Skip failure in SCA during scan itself", + allowPartialResults: true, + useJas: false, + testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-project"), + }, + // TODO when applying allow-partial-results to JAS make sure to add a test case that checks failures in JAS scans + add some JAS api call to the mock server + } + + serverMock, serverDetails := validations.CreateXrayRestsMockServer(func(w http.ResponseWriter, r *http.Request) { + if r.RequestURI == "/xray/api/v1/system/version" { + _, err := w.Write([]byte(fmt.Sprintf(`{"xray_version": "%s", "xray_revision": "xxx"}`, scangraph.GraphScanMinXrayVersion))) + if !assert.NoError(t, err) { + return + } + } + if strings.HasPrefix(r.RequestURI, "/xray/api/v1/scan/graph") && r.Method == http.MethodPost { + // We set SCA scan graph API to fail + w.WriteHeader(http.StatusBadRequest) + } + }) + defer serverMock.Close() + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + + assert.NoError(t, biutils.CopyDir(testcase.testDirPath, tempDirPath, false, nil)) + + auditBasicParams := (&utils.AuditBasicParams{}). + SetServerDetails(serverDetails). + SetOutputFormat(format.Table). + SetUseJas(testcase.useJas). + SetAllowPartialResults(testcase.allowPartialResults) + + auditParams := NewAuditParams(). + SetWorkingDirs([]string{tempDirPath}). + SetGraphBasicParams(auditBasicParams). + SetCommonGraphScanParams(&scangraph.CommonGraphScanParams{ + ScanType: scanservices.Dependency, + IncludeVulnerabilities: true, + MultiScanId: validations.TestScaScanId, + }) + auditParams.SetIsRecursiveScan(true) + + scanResults, err := RunAudit(auditParams) + if testcase.allowPartialResults { + assert.NoError(t, scanResults.GetErrors()) + assert.NoError(t, err) + } else { + assert.Error(t, scanResults.GetErrors()) + assert.NoError(t, err) + } + }) + } +} + +func TestCreateErrorIfPartialResultsDisabled(t *testing.T) { + testcases := []struct { + name string + allowPartialResults bool + auditParallelRunner bool + }{ + { + name: "Allow partial results - no error expected", + allowPartialResults: true, + auditParallelRunner: true, + }, + { + name: "Partial results disabled with SecurityParallelRunner", + allowPartialResults: false, + auditParallelRunner: true, + }, + { + name: "Partial results disabled without SecurityParallelRunner", + allowPartialResults: false, + auditParallelRunner: false, + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + auditBasicParams := (&utils.AuditBasicParams{}).SetAllowPartialResults(testcase.allowPartialResults) + auditParams := NewAuditParams().SetGraphBasicParams(auditBasicParams) + + var auditParallelRunner *utils.SecurityParallelRunner + if testcase.auditParallelRunner { + auditParallelRunner = utils.CreateSecurityParallelRunner(1) + } + + err := createErrorIfPartialResultsDisabled(auditParams, auditParallelRunner, "", errors.New("error")) + if testcase.allowPartialResults { + assert.NoError(t, err) + } else { + if testcase.auditParallelRunner { + assert.False(t, isErrorsQueueEmpty(auditParallelRunner)) + } else { + assert.Error(t, err) + } + } + }) + } +} + +func isErrorsQueueEmpty(spr *utils.SecurityParallelRunner) bool { + select { + case <-spr.ErrorsQueue: + // Channel is not empty + return false + default: + // Channel is empty + return true + } +} diff --git a/commands/audit/sca/npm/npm.go b/commands/audit/sca/npm/npm.go index 6cb84df3..54d0ad9b 100644 --- a/commands/audit/sca/npm/npm.go +++ b/commands/audit/sca/npm/npm.go @@ -8,6 +8,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/npm" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray" "github.com/jfrog/jfrog-client-go/utils/log" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" @@ -107,9 +108,9 @@ func addIgnoreScriptsFlag(npmArgs []string) []string { func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo *biutils.PackageInfo) (*xrayUtils.GraphNode, []string) { treeMap := make(map[string]xray.DepTreeNode) for _, dependency := range dependencies { - dependencyId := utils.NpmPackageTypeIdentifier + dependency.Id + dependencyId := techutils.Npm.GetPackageTypeId() + dependency.Id for _, requestedByNode := range dependency.RequestedBy { - parent := utils.NpmPackageTypeIdentifier + requestedByNode[0] + parent := techutils.Npm.GetPackageTypeId() + requestedByNode[0] depTreeNode, ok := treeMap[parent] if ok { depTreeNode.Children = appendUniqueChild(depTreeNode.Children, dependencyId) @@ -119,7 +120,7 @@ func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo * treeMap[parent] = depTreeNode } } - graph, nodeMapTypes := xray.BuildXrayDependencyTree(treeMap, utils.NpmPackageTypeIdentifier+packageInfo.BuildInfoModuleId()) + graph, nodeMapTypes := xray.BuildXrayDependencyTree(treeMap, techutils.Npm.GetPackageTypeId()+packageInfo.BuildInfoModuleId()) return graph, maps.Keys(nodeMapTypes) } diff --git a/commands/audit/sca/npm/npm_test.go b/commands/audit/sca/npm/npm_test.go index 93bc9649..e3647f76 100644 --- a/commands/audit/sca/npm/npm_test.go +++ b/commands/audit/sca/npm/npm_test.go @@ -8,6 +8,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-cli-security/commands/audit/sca" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" @@ -106,7 +107,7 @@ func TestParseNpmDependenciesList(t *testing.T) { } expectedUniqueDeps := []string{xrayDependenciesTree.Id} for _, dep := range dependencies { - expectedUniqueDeps = append(expectedUniqueDeps, utils.NpmPackageTypeIdentifier+dep.Id) + expectedUniqueDeps = append(expectedUniqueDeps, techutils.Npm.GetPackageTypeId()+dep.Id) } assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") diff --git a/commands/audit/sca/pnpm/pnpm.go b/commands/audit/sca/pnpm/pnpm.go index 5886f195..6be885ec 100644 --- a/commands/audit/sca/pnpm/pnpm.go +++ b/commands/audit/sca/pnpm/pnpm.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/jfrog-cli-security/commands/audit/sca/npm" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" @@ -178,7 +179,7 @@ func createProjectDependenciesTree(project pnpmLsProject) map[string]xray.DepTre // Return npm://: of a dependency func getDependencyId(depName, version string) string { - return utils.NpmPackageTypeIdentifier + depName + ":" + version + return techutils.Npm.GetPackageTypeId() + depName + ":" + version } func appendTransitiveDependencies(parent string, dependencies map[string]pnpmLsDependency, result map[string]xray.DepTreeNode) { diff --git a/commands/audit/sca/yarn/yarn.go b/commands/audit/sca/yarn/yarn.go index 94df520e..a8cc9196 100644 --- a/commands/audit/sca/yarn/yarn.go +++ b/commands/audit/sca/yarn/yarn.go @@ -16,6 +16,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" @@ -222,5 +223,5 @@ func parseYarnDependenciesMap(dependencies map[string]*bibuildutils.YarnDependen } func getXrayDependencyId(yarnDependency *bibuildutils.YarnDependency) string { - return utils.NpmPackageTypeIdentifier + yarnDependency.Name() + ":" + yarnDependency.Details.Version + return techutils.Npm.GetPackageTypeId() + yarnDependency.Name() + ":" + yarnDependency.Details.Version } diff --git a/commands/audit/sca/yarn/yarn_test.go b/commands/audit/sca/yarn/yarn_test.go index 023e93c8..46ae80c6 100644 --- a/commands/audit/sca/yarn/yarn_test.go +++ b/commands/audit/sca/yarn/yarn_test.go @@ -1,23 +1,27 @@ package yarn import ( + "os" + "path/filepath" + "strings" + "testing" + "errors" + "github.com/jfrog/build-info-go/build" bibuildutils "github.com/jfrog/build-info-go/build/utils" biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-cli-security/commands/audit/sca" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "strings" - "testing" ) func TestParseYarnDependenciesList(t *testing.T) { + npmId := techutils.Npm.GetPackageTypeId() yarnDependencies := map[string]*bibuildutils.YarnDependency{ "pack1@npm:1.0.0": {Value: "pack1@npm:1.0.0", Details: bibuildutils.YarnDepDetails{Version: "1.0.0", Dependencies: []bibuildutils.YarnDependencyPointer{{Locator: "pack4@npm:4.0.0"}}}}, "pack2@npm:2.0.0": {Value: "pack2@npm:2.0.0", Details: bibuildutils.YarnDepDetails{Version: "2.0.0", Dependencies: []bibuildutils.YarnDependencyPointer{{Locator: "pack4@npm:4.0.0"}, {Locator: "pack5@npm:5.0.0"}}}}, @@ -26,31 +30,25 @@ func TestParseYarnDependenciesList(t *testing.T) { "pack5@npm:5.0.0": {Value: "pack5@npm:5.0.0", Details: bibuildutils.YarnDepDetails{Version: "5.0.0", Dependencies: []bibuildutils.YarnDependencyPointer{{Locator: "pack2@npm:2.0.0"}}}}, } - rootXrayId := utils.NpmPackageTypeIdentifier + "@jfrog/pack3:3.0.0" + rootXrayId := npmId + "@jfrog/pack3:3.0.0" expectedTree := &xrayUtils.GraphNode{ Id: rootXrayId, Nodes: []*xrayUtils.GraphNode{ - {Id: utils.NpmPackageTypeIdentifier + "pack1:1.0.0", + {Id: npmId + "pack1:1.0.0", Nodes: []*xrayUtils.GraphNode{ - {Id: utils.NpmPackageTypeIdentifier + "pack4:4.0.0", + {Id: npmId + "pack4:4.0.0", Nodes: []*xrayUtils.GraphNode{}}, }}, - {Id: utils.NpmPackageTypeIdentifier + "pack2:2.0.0", + {Id: npmId + "pack2:2.0.0", Nodes: []*xrayUtils.GraphNode{ - {Id: utils.NpmPackageTypeIdentifier + "pack4:4.0.0", + {Id: npmId + "pack4:4.0.0", Nodes: []*xrayUtils.GraphNode{}}, - {Id: utils.NpmPackageTypeIdentifier + "pack5:5.0.0", + {Id: npmId + "pack5:5.0.0", Nodes: []*xrayUtils.GraphNode{}}, }}, }, } - expectedUniqueDeps := []string{ - utils.NpmPackageTypeIdentifier + "pack1:1.0.0", - utils.NpmPackageTypeIdentifier + "pack2:2.0.0", - utils.NpmPackageTypeIdentifier + "pack4:4.0.0", - utils.NpmPackageTypeIdentifier + "pack5:5.0.0", - utils.NpmPackageTypeIdentifier + "@jfrog/pack3:3.0.0", - } + expectedUniqueDeps := []string{npmId + "pack1:1.0.0", npmId + "pack2:2.0.0", npmId + "pack4:4.0.0", npmId + "pack5:5.0.0", npmId + "@jfrog/pack3:3.0.0"} xrayDependenciesTree, uniqueDeps := parseYarnDependenciesMap(yarnDependencies, rootXrayId) assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") diff --git a/commands/audit/scarunner.go b/commands/audit/scarunner.go index 847b7732..5794e890 100644 --- a/commands/audit/scarunner.go +++ b/commands/audit/scarunner.go @@ -16,7 +16,6 @@ import ( "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/gofrog/parallel" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/commands/audit/sca" _go "github.com/jfrog/jfrog-cli-security/commands/audit/sca/go" "github.com/jfrog/jfrog-cli-security/commands/audit/sca/java" @@ -28,6 +27,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils" xrayutils "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/artifactory" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray" "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" @@ -38,7 +38,20 @@ import ( xrayCmdUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" ) -func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner, auditParams *AuditParams, results *xrayutils.Results) (err error) { +// We can only preform SCA scan if we identified at least one technology for a target. +func hasAtLeastOneTech(cmdResults *results.SecurityCommandResults) bool { + if len(cmdResults.Targets) == 0 { + return false + } + for _, scan := range cmdResults.Targets { + if scan.Technology != "" { + return true + } + } + return false +} + +func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner, auditParams *AuditParams, cmdResults *results.SecurityCommandResults) (err error) { if len(auditParams.ScansToPerform()) > 0 && !slices.Contains(auditParams.ScansToPerform(), xrayutils.ScaScan) { log.Debug("Skipping SCA scan as requested by input...") return @@ -57,23 +70,20 @@ func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner if err != nil { return } - - scans := getScaScansToPreform(auditParams) - if len(scans) == 0 { + if !hasAtLeastOneTech(cmdResults) { log.Info("Couldn't determine a package manager or build tool used by this project. Skipping the SCA scan...") return } - scanInfo, err := coreutils.GetJsonIndent(scans) - if err != nil { - return - } - log.Info(fmt.Sprintf("Preforming %d SCA scans:\n%s", len(scans), scanInfo)) - defer func() { // Make sure to return to the original working directory, buildDependencyTree may change it err = errors.Join(err, os.Chdir(currentWorkingDir)) }() - for _, scan := range scans { + // Preform SCA scans + for _, scan := range cmdResults.Targets { + if scan.Technology == "" { + log.Warn(fmt.Sprintf("Couldn't determine a package manager or build tool used by this project. Skipping the SCA scan in '%s'...", scan.Target)) + continue + } // Get the dependency tree for the technology in the working directory. treeResult, bdtErr := buildDependencyTree(scan, auditParams) if bdtErr != nil { @@ -82,54 +92,18 @@ func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner log.Warn(bdtErr.Error()) continue } - err = errors.Join(err, fmt.Errorf("audit command in '%s' failed:\n%s", scan.Target, bdtErr.Error())) + err = errors.Join(err, createErrorIfPartialResultsDisabled(auditParams, nil, fmt.Sprintf("Dependencies tree construction ha failed for the following target: %s", scan.Target), fmt.Errorf("failed to get dependencies trees in '%s':\n%s", scan.Target, bdtErr.Error()))) continue } // Create sca scan task auditParallelRunner.ScaScansWg.Add(1) _, taskErr := auditParallelRunner.Runner.AddTaskWithError(executeScaScanTask(auditParallelRunner, serverDetails, auditParams, scan, treeResult), func(err error) { - auditParallelRunner.AddErrorToChan(fmt.Errorf("audit command in '%s' failed:\n%s", scan.Target, err.Error())) + _ = createErrorIfPartialResultsDisabled(auditParams, auditParallelRunner, fmt.Sprintf("Failed to execute SCA scan for the following target: %s", scan.Target), fmt.Errorf("SCA scan failed in '%s':\n%s", scan.Target, err.Error())) + auditParallelRunner.ScaScansWg.Done() }) if taskErr != nil { return fmt.Errorf("failed to create sca scan task for '%s': %s", scan.Target, taskErr.Error()) } - // Add the scan to the results - auditParallelRunner.ResultsMu.Lock() - results.ScaResults = append(results.ScaResults, scan) - auditParallelRunner.ResultsMu.Unlock() - } - return -} - -// Calculate the scans to preform -func getScaScansToPreform(params *AuditParams) (scansToPreform []*xrayutils.ScaScanResult) { - for _, requestedDirectory := range params.workingDirs { - if !fileutils.IsPathExists(requestedDirectory, false) { - log.Warn("The working directory", requestedDirectory, "doesn't exist. Skipping SCA scan...") - continue - } - // Detect descriptors and technologies in the requested directory. - techToWorkingDirs, err := techutils.DetectTechnologiesDescriptors(requestedDirectory, params.IsRecursiveScan(), params.Technologies(), getRequestedDescriptors(params), sca.GetExcludePattern(params.AuditBasicParams)) - if err != nil { - log.Warn("Couldn't detect technologies in", requestedDirectory, "directory.", err.Error()) - continue - } - // Create scans to preform - for tech, workingDirs := range techToWorkingDirs { - if tech == techutils.Dotnet { - // We detect Dotnet and Nuget the same way, if one detected so does the other. - // We don't need to scan for both and get duplicate results. - continue - } - if len(workingDirs) == 0 { - // Requested technology (from params) descriptors/indicators was not found, scan only requested directory for this technology. - scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{Target: requestedDirectory, Technology: tech}) - } - for workingDir, descriptors := range workingDirs { - // Add scan for each detected working directory. - scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{Target: workingDir, Technology: tech, Descriptors: descriptors}) - } - } } return } @@ -144,21 +118,24 @@ func getRequestedDescriptors(params *AuditParams) map[techutils.Technology][]str // Preform the SCA scan for the given scan information. func executeScaScanTask(auditParallelRunner *utils.SecurityParallelRunner, serverDetails *config.ServerDetails, auditParams *AuditParams, - scan *xrayutils.ScaScanResult, treeResult *DependencyTreeResult) parallel.TaskFunc { + scan *results.TargetResults, treeResult *DependencyTreeResult) parallel.TaskFunc { return func(threadId int) (err error) { log.Info(clientutils.GetLogMsgPrefix(threadId, false)+"Running SCA scan for", scan.Target, "vulnerable dependencies in", scan.Target, "directory...") + var xrayErr error defer func() { - auditParallelRunner.ScaScansWg.Done() + if xrayErr == nil { + // We Sca waitGroup as done only when we have no errors. If we have errors we mark it done in the error's handler function + auditParallelRunner.ScaScansWg.Done() + } }() // Scan the dependency tree. scanResults, xrayErr := runScaWithTech(scan.Technology, auditParams, serverDetails, *treeResult.FlatTree, treeResult.FullDepTrees) if xrayErr != nil { return fmt.Errorf("%s Xray dependency tree scan request on '%s' failed:\n%s", clientutils.GetLogMsgPrefix(threadId, false), scan.Technology, xrayErr.Error()) } - scan.IsMultipleRootProject = clientutils.Pointer(len(treeResult.FullDepTrees) > 1) auditParallelRunner.ResultsMu.Lock() + scan.NewScaScanResults(scanResults...).IsMultipleRootProject = clientutils.Pointer(len(treeResult.FullDepTrees) > 1) addThirdPartyDependenciesToParams(auditParams, scan.Technology, treeResult.FlatTree, treeResult.FullDepTrees) - scan.XrayResults = append(scan.XrayResults, scanResults...) err = dumpScanResponseToFileIfNeeded(scanResults, auditParams.scanResultsOutputDir, utils.ScaScan) auditParallelRunner.ResultsMu.Unlock() return @@ -376,7 +353,7 @@ func logDeps(uniqueDeps any) (err error) { } // This method will change the working directory to the scan's working directory. -func buildDependencyTree(scan *utils.ScaScanResult, params *AuditParams) (*DependencyTreeResult, error) { +func buildDependencyTree(scan *results.TargetResults, params *AuditParams) (*DependencyTreeResult, error) { if err := os.Chdir(scan.Target); err != nil { return nil, errorutils.CheckError(err) } diff --git a/commands/audit/scarunner_test.go b/commands/audit/scarunner_test.go index 1a265381..7db9d5f8 100644 --- a/commands/audit/scarunner_test.go +++ b/commands/audit/scarunner_test.go @@ -3,18 +3,15 @@ package audit import ( "os" "path/filepath" - "sort" "testing" - xrayutils "github.com/jfrog/jfrog-cli-security/utils" - "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" ) -func TestGetDirectDependenciesList(t *testing.T) { +func TestGetDirectDependenciesFromTree(t *testing.T) { tests := []struct { dependenciesTrees []*xrayUtils.GraphNode expectedResult []string @@ -116,109 +113,3 @@ func createEmptyFile(t *testing.T, path string) { assert.NoError(t, err) assert.NoError(t, file.Close()) } - -func TestGetScaScansToPreform(t *testing.T) { - - dir, cleanUp := createTestDir(t) - - tests := []struct { - name string - wd string - params func() *AuditParams - expected []*xrayutils.ScaScanResult - }{ - { - name: "Test specific technologies", - wd: dir, - params: func() *AuditParams { - param := NewAuditParams().SetWorkingDirs([]string{dir}) - param.SetTechnologies([]string{"maven", "npm", "go"}).SetIsRecursiveScan(true) - return param - }, - expected: []*xrayutils.ScaScanResult{ - { - Technology: techutils.Maven, - Target: filepath.Join(dir, "dir", "maven"), - Descriptors: []string{ - filepath.Join(dir, "dir", "maven", "pom.xml"), - filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), - filepath.Join(dir, "dir", "maven", "maven-sub2", "pom.xml"), - }, - }, - { - Technology: techutils.Npm, - Target: filepath.Join(dir, "dir", "npm"), - Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, - }, - { - Technology: techutils.Go, - Target: filepath.Join(dir, "dir", "go"), - Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, - }, - }, - }, - { - name: "Test all", - wd: dir, - params: func() *AuditParams { - param := NewAuditParams().SetWorkingDirs([]string{dir}) - param.SetIsRecursiveScan(true) - return param - }, - expected: []*xrayutils.ScaScanResult{ - { - Technology: techutils.Maven, - Target: filepath.Join(dir, "dir", "maven"), - Descriptors: []string{ - filepath.Join(dir, "dir", "maven", "pom.xml"), - filepath.Join(dir, "dir", "maven", "maven-sub", "pom.xml"), - filepath.Join(dir, "dir", "maven", "maven-sub2", "pom.xml"), - }, - }, - { - Technology: techutils.Npm, - Target: filepath.Join(dir, "dir", "npm"), - Descriptors: []string{filepath.Join(dir, "dir", "npm", "package.json")}, - }, - { - Technology: techutils.Go, - Target: filepath.Join(dir, "dir", "go"), - Descriptors: []string{filepath.Join(dir, "dir", "go", "go.mod")}, - }, - { - Technology: techutils.Yarn, - Target: filepath.Join(dir, "yarn"), - Descriptors: []string{filepath.Join(dir, "yarn", "package.json")}, - }, - { - Technology: techutils.Pip, - Target: filepath.Join(dir, "yarn", "Pip"), - Descriptors: []string{filepath.Join(dir, "yarn", "Pip", "requirements.txt")}, - }, - { - Technology: techutils.Pipenv, - Target: filepath.Join(dir, "yarn", "Pipenv"), - Descriptors: []string{filepath.Join(dir, "yarn", "Pipenv", "Pipfile")}, - }, - { - Technology: techutils.Nuget, - Target: filepath.Join(dir, "Nuget"), - Descriptors: []string{filepath.Join(dir, "Nuget", "project.sln"), filepath.Join(dir, "Nuget", "Nuget-sub", "project.csproj")}, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - result := getScaScansToPreform(test.params()) - for i := range result { - sort.Strings(result[i].Descriptors) - sort.Strings(test.expected[i].Descriptors) - } - assert.ElementsMatch(t, test.expected, result) - }) - } - - cleanUp() -} diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index 780a317f..319d32f2 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jfrog/jfrog-cli-security/utils/formats" "net/http" "os" "path/filepath" @@ -16,18 +17,6 @@ import ( "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/gofrog/parallel" - - "github.com/jfrog/jfrog-client-go/artifactory" - "github.com/jfrog/jfrog-client-go/auth" - clientutils "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/io/httputils" - "github.com/jfrog/jfrog-client-go/utils/log" - xrayClient "github.com/jfrog/jfrog-client-go/xray" - xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" - - "github.com/jfrog/build-info-go/build/utils/dotnet/dependencies" - rtUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/common/cliutils" outFormat "github.com/jfrog/jfrog-cli-core/v2/common/format" @@ -38,10 +27,20 @@ import ( "github.com/jfrog/jfrog-cli-security/commands/audit" "github.com/jfrog/jfrog-cli-security/commands/audit/sca/python" - "github.com/jfrog/jfrog-cli-security/formats" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results/output" "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/auth" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/httputils" + "github.com/jfrog/jfrog-client-go/utils/log" + xrayClient "github.com/jfrog/jfrog-client-go/xray" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" + + "github.com/jfrog/build-info-go/build/utils/dotnet/dependencies" ) const ( @@ -253,7 +252,7 @@ func (ca *CurationAuditCommand) Run() (err error) { for projectPath, packagesStatus := range results { err = errors.Join(err, printResult(ca.OutputFormat(), projectPath, packagesStatus.packagesStatus)) } - err = errors.Join(err, utils.RecordSecurityCommandSummary(utils.NewCurationSummary(convertResultsToSummary(results)))) + err = errors.Join(err, output.RecordSecurityCommandSummary(output.NewCurationSummary(convertResultsToSummary(results)))) return } @@ -454,7 +453,7 @@ func printResult(format outFormat.OutputFormat, projectPath string, packagesStat switch format { case outFormat.Json: if len(packagesStatus) > 0 { - err := utils.PrintJson(packagesStatus) + err := output.PrintJson(packagesStatus) if err != nil { return err } @@ -769,7 +768,7 @@ func toNugetDownloadUrl(artifactoryUrl, repo, compName, compVersion string) stri // input - repo: libs-release // output - downloadUrl: /libs-release/org/apache/tomcat/embed/tomcat-embed-jasper/8.0.33/tomcat-embed-jasper-8.0.33.jar func getNugetNameScopeAndVersion(id, artiUrl, repo string) (downloadUrls []string, name, version string) { - name, version, _ = utils.SplitComponentId(id) + name, version, _ = techutils.SplitComponentId(id) downloadUrls = append(downloadUrls, toNugetDownloadUrl(artiUrl, repo, name, version)) for _, versionVariant := range dependencies.CreateAlternativeVersionForms(version) { diff --git a/commands/curation/curationaudit_test.go b/commands/curation/curationaudit_test.go index 95118871..4941ea7c 100644 --- a/commands/curation/curationaudit_test.go +++ b/commands/curation/curationaudit_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/jfrog/jfrog-cli-security/utils/formats" "net/http" "net/http/httptest" "os" @@ -17,8 +18,6 @@ import ( "sync" "testing" - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" clienttestutils "github.com/jfrog/jfrog-client-go/utils/tests" diff --git a/commands/enrich/enrich.go b/commands/enrich/enrich.go index badbe2df..ec3c748e 100644 --- a/commands/enrich/enrich.go +++ b/commands/enrich/enrich.go @@ -5,15 +5,20 @@ import ( "encoding/xml" "errors" "fmt" + "os" + "os/exec" + "path/filepath" + "github.com/beevik/etree" "github.com/jfrog/gofrog/parallel" "github.com/jfrog/jfrog-cli-core/v2/common/spec" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/commands/enrich/enrichgraph" - "github.com/jfrog/jfrog-cli-security/formats" "github.com/jfrog/jfrog-cli-security/utils" - xrutils "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/output" + "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray" "github.com/jfrog/jfrog-client-go/artifactory/services/fspatterns" clientutils "github.com/jfrog/jfrog-client-go/utils" @@ -22,18 +27,11 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray/services" - "os" - "os/exec" ) type FileContext func(string) parallel.TaskFunc type indexFileHandlerFunc func(file string) -type ScanInfo struct { - Target string - Result *services.ScanResponse -} - type EnrichCommand struct { serverDetails *config.ServerDetails spec *spec.SpecFiles @@ -64,8 +62,15 @@ func (enrichCmd *EnrichCommand) ServerDetails() (*config.ServerDetails, error) { return enrichCmd.serverDetails, nil } -func AppendVulnsToJson(results *utils.Results) error { - fileName := utils.GetScaScanFileName(results) +func getScaScanFileName(cmdResults *results.SecurityCommandResults) string { + if len(cmdResults.Targets) > 0 { + return cmdResults.Targets[0].Target + } + return "" +} + +func AppendVulnsToJson(cmdResults *results.SecurityCommandResults) error { + fileName := getScaScanFileName(cmdResults) fileContent, err := os.ReadFile(fileName) if err != nil { fmt.Println("Error reading file:", err) @@ -78,7 +83,7 @@ func AppendVulnsToJson(results *utils.Results) error { return err } var vulnerabilities []map[string]string - xrayResults := results.GetScaScansXrayResults()[0] + xrayResults := cmdResults.GetScaScansXrayResults()[0] for _, vuln := range xrayResults.Vulnerabilities { for component := range vuln.Components { vulnerability := map[string]string{"bom-ref": component, "id": vuln.Cves[0].Id} @@ -86,18 +91,18 @@ func AppendVulnsToJson(results *utils.Results) error { } } data["vulnerabilities"] = vulnerabilities - return utils.PrintJson(data) + return output.PrintJson(data) } -func AppendVulnsToXML(results *utils.Results) error { - fileName := utils.GetScaScanFileName(results) +func AppendVulnsToXML(cmdResults *results.SecurityCommandResults) error { + fileName := getScaScanFileName(cmdResults) result := etree.NewDocument() err := result.ReadFromFile(fileName) if err != nil { return err } destination := result.FindElements("//bom")[0] - xrayResults := results.GetScaScansXrayResults()[0] + xrayResults := cmdResults.GetScaScansXrayResults()[0] vulns := destination.CreateElement("vulnerabilities") for _, vuln := range xrayResults.Vulnerabilities { for component := range vuln.Components { @@ -114,7 +119,7 @@ func AppendVulnsToXML(results *utils.Results) error { return nil } -func isXML(scaResults []*utils.ScaScanResult) (bool, error) { +func isXML(scaResults []*results.TargetResults) (bool, error) { if len(scaResults) == 0 { return false, errors.New("unable to retrieve results") } @@ -151,30 +156,16 @@ func (enrichCmd *EnrichCommand) Run() (err error) { log.Info("JFrog Xray version is:", xrayVersion) - threads := 1 - if enrichCmd.threads > 1 { - threads = enrichCmd.threads - } + scanResults := results.NewCommandResults(utils.SBOM, xrayVersion, false, false) - // resultsArr is a two-dimensional array. Each array in it contains a list of ScanResponses that were requested and collected by a specific thread. - resultsArr := make([][]*ScanInfo, threads) fileProducerConsumer := parallel.NewRunner(enrichCmd.threads, 20000, false) - fileProducerErrors := make([][]formats.SimpleJsonError, threads) indexedFileProducerConsumer := parallel.NewRunner(enrichCmd.threads, 20000, false) - indexedFileProducerErrors := make([][]formats.SimpleJsonError, threads) fileCollectingErrorsQueue := clientutils.NewErrorsQueue(1) // Start walking on the filesystem to "produce" files that match the given pattern // while the consumer uses the indexer to index those files. - enrichCmd.prepareScanTasks(fileProducerConsumer, indexedFileProducerConsumer, resultsArr, indexedFileProducerErrors, fileCollectingErrorsQueue, xrayVersion) + enrichCmd.prepareScanTasks(fileProducerConsumer, indexedFileProducerConsumer, scanResults, fileCollectingErrorsQueue, xrayVersion) enrichCmd.performScanTasks(fileProducerConsumer, indexedFileProducerConsumer) - // Handle results - var flatResults []*xrutils.ScaScanResult - for _, arr := range resultsArr { - for _, res := range arr { - flatResults = append(flatResults, &xrutils.ScaScanResult{Target: res.Target, XrayResults: []services.ScanResponse{*res.Result}}) - } - } if enrichCmd.progress != nil { if err = enrichCmd.progress.Quit(); err != nil { return err @@ -183,22 +174,15 @@ func (enrichCmd *EnrichCommand) Run() (err error) { } fileCollectingErr := fileCollectingErrorsQueue.GetError() - var scanErrors []formats.SimpleJsonError if fileCollectingErr != nil { - scanErrors = append(scanErrors, formats.SimpleJsonError{ErrorMessage: fileCollectingErr.Error()}) + scanResults.Error = errors.Join(scanResults.Error, fileCollectingErr) } - scanErrors = appendErrorSlice(scanErrors, fileProducerErrors) - scanErrors = appendErrorSlice(scanErrors, indexedFileProducerErrors) - - scanResults := xrutils.NewAuditResults(utils.SBOM) - scanResults.XrayVersion = xrayVersion - scanResults.ScaResults = flatResults - isxml, err := isXML(scanResults.ScaResults) + isXml, err := isXML(scanResults.Targets) if err != nil { return } - if isxml { + if isXml { if err = AppendVulnsToXML(scanResults); err != nil { return } @@ -211,9 +195,8 @@ func (enrichCmd *EnrichCommand) Run() (err error) { if err != nil { return err } - - if len(scanErrors) > 0 { - return errorutils.CheckError(errors.New(scanErrors[0].ErrorMessage)) + if scanResults.GetErrors() != nil { + return errorutils.CheckError(scanResults.GetErrors()) } log.Info("Enrich process completed successfully.") return nil @@ -227,13 +210,13 @@ func (enrichCmd *EnrichCommand) CommandName() string { return "xr_enrich" } -func (enrichCmd *EnrichCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, resultsArr [][]*ScanInfo, indexedFileErrors [][]formats.SimpleJsonError, fileCollectingErrorsQueue *clientutils.ErrorsQueue, xrayVersion string) { +func (enrichCmd *EnrichCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, cmdResults *results.SecurityCommandResults, fileCollectingErrorsQueue *clientutils.ErrorsQueue, xrayVersion string) { go func() { defer fileProducer.Done() // Iterate over file-spec groups and produce indexing tasks. // When encountering an error, log and move to next group. specFiles := enrichCmd.spec.Files - artifactHandlerFunc := enrichCmd.createIndexerHandlerFunc(indexedFileProducer, resultsArr, indexedFileErrors, xrayVersion) + artifactHandlerFunc := enrichCmd.createIndexerHandlerFunc(indexedFileProducer, cmdResults, xrayVersion) taskHandler := getAddTaskToProducerFunc(fileProducer, artifactHandlerFunc) err := FileForEnriching(specFiles[0], taskHandler) @@ -244,14 +227,18 @@ func (enrichCmd *EnrichCommand) prepareScanTasks(fileProducer, indexedFileProduc }() } -func (enrichCmd *EnrichCommand) createIndexerHandlerFunc(indexedFileProducer parallel.Runner, resultsArr [][]*ScanInfo, indexedFileErrors [][]formats.SimpleJsonError, xrayVersion string) FileContext { +func (enrichCmd *EnrichCommand) createIndexerHandlerFunc(indexedFileProducer parallel.Runner, cmdResults *results.SecurityCommandResults, xrayVersion string) FileContext { return func(filePath string) parallel.TaskFunc { return func(threadId int) (err error) { // Add a new task to the second producer/consumer // which will send the indexed binary to Xray and then will store the received result. taskFunc := func(threadId int) (err error) { - fileContent, err := os.ReadFile(filePath) + // Create a scan target for the file. + targetResults := cmdResults.NewScanResults(results.ScanTarget{Target: filePath, Name: filepath.Base(filePath)}) + log.Debug(clientutils.GetLogMsgPrefix(threadId, false)+"enrich file:", targetResults.Target) + fileContent, err := os.ReadFile(targetResults.Target) if err != nil { + targetResults.AddError(err) return err } params := &services.XrayGraphImportParams{ @@ -264,14 +251,16 @@ func (enrichCmd *EnrichCommand) createIndexerHandlerFunc(indexedFileProducer par SetXrayVersion(xrayVersion) xrayManager, err := xray.CreateXrayServiceManager(importGraphParams.ServerDetails()) if err != nil { + targetResults.AddError(err) return err } scanResults, err := enrichgraph.RunImportGraphAndGetResults(importGraphParams, xrayManager) if err != nil { - indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) + targetResults.AddError(err) return } - resultsArr[threadId] = append(resultsArr[threadId], &ScanInfo{Target: filePath, Result: scanResults}) + targetResults.NewScaScanResults(*scanResults) + targetResults.Technology = techutils.Technology(scanResults.ScannedPackageType) return } @@ -319,10 +308,3 @@ func FileForEnriching(fileData spec.File, dataHandlerFunc indexFileHandlerFunc) } return errors.New("directory instead of a single file") } - -func appendErrorSlice(scanErrors []formats.SimpleJsonError, errorsToAdd [][]formats.SimpleJsonError) []formats.SimpleJsonError { - for _, errorSlice := range errorsToAdd { - scanErrors = append(scanErrors, errorSlice...) - } - return scanErrors -} diff --git a/commands/git/countcontributors.go b/commands/git/countcontributors.go index bc3e6aa0..c135b3e9 100644 --- a/commands/git/countcontributors.go +++ b/commands/git/countcontributors.go @@ -4,19 +4,20 @@ import ( "context" "errors" "fmt" + "sort" + "strings" + "time" + "github.com/google/go-github/v56/github" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/froggit-go/vcsutils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results/output" ioUtils "github.com/jfrog/jfrog-client-go/utils/io" "github.com/jfrog/jfrog-client-go/utils/log" "golang.org/x/exp/maps" "gopkg.in/yaml.v3" "os" - "sort" - "strings" - "time" ) type scmTypeName string @@ -192,7 +193,7 @@ func (cc *CountContributorsCommand) Run() error { report.ScannedRepos = totalScannedRepos report.SkippedRepos = totalSkippedRepos - return utils.PrintJson(report) + return output.PrintJson(report) } func (cc *CountContributorsCommand) getVcsCountContributors() ([]VcsCountContributors, error) { diff --git a/commands/scan/buildscan.go b/commands/scan/buildscan.go index 30c4f390..30f52998 100644 --- a/commands/scan/buildscan.go +++ b/commands/scan/buildscan.go @@ -10,6 +10,8 @@ import ( outputFormat "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/output" xrayutils "github.com/jfrog/jfrog-cli-security/utils/xray" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -112,7 +114,7 @@ func (bsc *BuildScanCommand) Run() (err error) { } // If failBuild flag is true and also got fail build response from Xray if bsc.failBuild && isFailBuildResponse { - return utils.NewFailBuildError() + return results.NewFailBuildError() } return } @@ -143,24 +145,21 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS log.Info("The scan data is available at: " + buildScanResults.MoreDetailsUrl) isFailBuildResponse = buildScanResults.FailBuild - scanResponse := []services.ScanResponse{{ + cmdResults := results.NewCommandResults(utils.Build, xrayVersion, false, false) + scanResults := cmdResults.NewScanResults(results.ScanTarget{Name: fmt.Sprintf("%s (%s)", params.BuildName, params.BuildNumber)}) + scanResults.NewScaScanResults(services.ScanResponse{ Violations: buildScanResults.Violations, Vulnerabilities: buildScanResults.Vulnerabilities, XrayDataUrl: buildScanResults.MoreDetailsUrl, - }} + }) - scanResults := utils.NewAuditResults(utils.Build) - scanResults.XrayVersion = xrayVersion - scanResults.ScaResults = []*utils.ScaScanResult{{Target: fmt.Sprintf("%s (%s)", params.BuildName, params.BuildNumber), XrayResults: scanResponse}} - - resultsPrinter := utils.NewResultsWriter(scanResults). + resultsPrinter := output.NewResultsWriter(cmdResults). SetOutputFormat(bsc.outputFormat). SetHasViolationContext(true). SetIncludeVulnerabilities(bsc.includeVulnerabilities). SetIncludeLicenses(false). SetIsMultipleRootProject(true). - SetPrintExtendedTable(bsc.printExtendedTable). - SetExtraMessages(nil) + SetPrintExtendedTable(bsc.printExtendedTable) if bsc.outputFormat != outputFormat.Table { // Print the violations and/or vulnerabilities as part of one JSON. @@ -177,13 +176,21 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS } } } - err = utils.RecordSecurityCommandSummary(utils.NewBuildScanSummary( - scanResults, + err = bsc.recordResults(cmdResults, params) + return +} + +func (bsc *BuildScanCommand) recordResults(cmdResults *results.SecurityCommandResults, params services.XrayBuildParams) (err error) { + var summary output.ScanCommandResultSummary + if summary, err = output.NewBuildScanSummary( + cmdResults, bsc.serverDetails, bsc.includeVulnerabilities, params.BuildName, params.BuildNumber, - )) - return + ); err != nil { + return + } + return output.RecordSecurityCommandSummary(summary) } func (bsc *BuildScanCommand) CommandName() string { diff --git a/commands/scan/dockerscan.go b/commands/scan/dockerscan.go index 88728882..a2f23c42 100644 --- a/commands/scan/dockerscan.go +++ b/commands/scan/dockerscan.go @@ -11,6 +11,8 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/common/spec" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/output" "github.com/jfrog/jfrog-cli-security/utils/xray" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" @@ -85,7 +87,7 @@ func (dsc *DockerScanCommand) Run() (err error) { Pattern(imageTarPath). Target(dsc.targetRepoPath). BuildSpec()).SetThreads(1) - dsc.ScanCommand.SetRunJasScans(true) + dsc.ScanCommand.SetTargetNameOverride(dsc.imageTag).SetRunJasScans(true) err = dsc.setCredentialEnvsForIndexerApp() if err != nil { return errorutils.CheckError(err) @@ -96,29 +98,27 @@ func (dsc *DockerScanCommand) Run() (err error) { err = errorutils.CheckError(e) } }() - return dsc.ScanCommand.RunAndRecordResults(utils.DockerImage, func(scanResults *utils.Results) (err error) { + return dsc.ScanCommand.RunAndRecordResults(utils.DockerImage, func(scanResults *results.SecurityCommandResults) (err error) { if scanResults == nil { return } - if scanResults.ScaResults != nil { - for _, result := range scanResults.ScaResults { - result.Name = dsc.imageTag - } - } dsc.analyticsMetricsService.UpdateGeneralEvent(dsc.analyticsMetricsService.CreateXscAnalyticsGeneralEventFinalizeFromAuditResults(scanResults)) - if err = utils.RecordSarifOutput(scanResults, utils.GetAllSupportedScans()); err != nil { - return - } - return utils.RecordSecurityCommandSummary(utils.NewDockerScanSummary( - scanResults, - dsc.ScanCommand.serverDetails, - dsc.ScanCommand.includeVulnerabilities, - dsc.ScanCommand.hasViolationContext(), - dsc.imageTag, - )) + return dsc.recordResults(scanResults) }) } +func (dsc *DockerScanCommand) recordResults(scanResults *results.SecurityCommandResults) (err error) { + hasViolationContext := dsc.ScanCommand.hasViolationContext() + if err = output.RecordSarifOutput(scanResults, dsc.ScanCommand.includeVulnerabilities, hasViolationContext); err != nil { + return + } + var summary output.ScanCommandResultSummary + if summary, err = output.NewDockerScanSummary(scanResults, dsc.ScanCommand.serverDetails, dsc.ScanCommand.includeVulnerabilities, hasViolationContext, dsc.imageTag); err != nil { + return + } + return output.RecordSecurityCommandSummary(summary) +} + // When indexing RPM files inside the docker container, the indexer-app needs to connect to the Xray Server. // This is because RPM indexing is performed on the server side. This method therefore sets the Xray credentials as env vars to be read and used by the indexer-app. func (dsc *DockerScanCommand) setCredentialEnvsForIndexerApp() error { diff --git a/commands/scan/scan.go b/commands/scan/scan.go index d6c93667..056464e1 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -14,10 +14,13 @@ import ( "golang.org/x/exp/maps" "golang.org/x/exp/slices" + jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/jfrog/jfrog-cli-security/jas" "github.com/jfrog/jfrog-cli-security/jas/applicability" "github.com/jfrog/jfrog-cli-security/jas/runner" "github.com/jfrog/jfrog-cli-security/jas/secrets" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/output" "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-cli-security/utils/xray" @@ -30,7 +33,6 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/common/spec" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/formats" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-client-go/artifactory/services/fspatterns" clientutils "github.com/jfrog/jfrog-client-go/utils" @@ -44,12 +46,6 @@ import ( type FileContext func(string) parallel.TaskFunc type indexFileHandlerFunc func(file string) -type ScanInfo struct { - Target string - Result *services.ScanResponse - ExtendedScanResults *utils.ExtendedScanResults -} - const ( BypassArchiveLimitsMinXrayVersion = "3.59.0" indexingCommand = "graph" @@ -61,21 +57,23 @@ type ScanCommand struct { spec *spec.SpecFiles threads int // The location of the downloaded Xray indexer binary on the local file system. - indexerPath string - indexerTempDir string - outputFormat format.OutputFormat - projectKey string - minSeverityFilter severityutils.Severity - watches []string - includeVulnerabilities bool - includeLicenses bool - fail bool - printExtendedTable bool - validateSecrets bool - bypassArchiveLimits bool - fixableOnly bool - progress ioUtils.ProgressMgr + indexerPath string + indexerTempDir string + outputFormat format.OutputFormat + projectKey string + minSeverityFilter severityutils.Severity + watches []string + includeVulnerabilities bool + includeLicenses bool + fail bool + printExtendedTable bool + validateSecrets bool + bypassArchiveLimits bool + fixableOnly bool + progress ioUtils.ProgressMgr + // JAS is only supported for Docker images. commandSupportsJAS bool + targetNameOverride string analyticsMetricsService *xsc.AnalyticsMetricsService } @@ -99,6 +97,11 @@ func (scanCmd *ScanCommand) SetRunJasScans(run bool) *ScanCommand { return scanCmd } +func (scanCmd *ScanCommand) SetTargetNameOverride(targetName string) *ScanCommand { + scanCmd.targetNameOverride = targetName + return scanCmd +} + func (scanCmd *ScanCommand) SetProgress(progress ioUtils.ProgressMgr) { scanCmd.progress = progress } @@ -200,20 +203,22 @@ func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.BinaryGraphNo } func (scanCmd *ScanCommand) Run() (err error) { - return scanCmd.RunAndRecordResults(utils.Binary, func(scanResults *utils.Results) (err error) { - if err = utils.RecordSarifOutput(scanResults, utils.GetAllSupportedScans()); err != nil { - return - } - return utils.RecordSecurityCommandSummary(utils.NewBinaryScanSummary( - scanResults, - scanCmd.serverDetails, - scanCmd.includeVulnerabilities, - scanCmd.hasViolationContext(), - )) - }) + return scanCmd.RunAndRecordResults(utils.Binary, scanCmd.recordResults) +} + +func (scanCmd *ScanCommand) recordResults(scanResults *results.SecurityCommandResults) (err error) { + hasViolationContext := scanCmd.hasViolationContext() + if err = output.RecordSarifOutput(scanResults, scanCmd.includeVulnerabilities, hasViolationContext); err != nil { + return + } + var summary output.ScanCommandResultSummary + if summary, err = output.NewBinaryScanSummary(scanResults, scanCmd.serverDetails, scanCmd.includeVulnerabilities, hasViolationContext); err != nil { + return + } + return output.RecordSecurityCommandSummary(summary) } -func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recordResFunc func(scanResults *utils.Results) error) (err error) { +func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recordResFunc func(scanResults *results.SecurityCommandResults) error) (err error) { defer func() { if err != nil { var e *exec.ExitError @@ -228,17 +233,22 @@ func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recor if err != nil { return err } + entitledForJas, err := jas.IsEntitledForJas(xrayManager, xrayVersion) + if err != nil { + return err + } - scanResults := utils.NewAuditResults(cmdType) - scanResults.XrayVersion = xrayVersion + cmdResults := results.NewCommandResults( + cmdType, + xrayVersion, + entitledForJas && scanCmd.commandSupportsJAS, + jas.CheckForSecretValidation(xrayManager, xrayVersion, scanCmd.validateSecrets), + ) if scanCmd.analyticsMetricsService != nil { - scanResults.MultiScanId = scanCmd.analyticsMetricsService.GetMsi() + cmdResults.SetMultiScanId(scanCmd.analyticsMetricsService.GetMsi()) } - - scanResults.ExtendedScanResults.EntitledForJas, err = jas.IsEntitledForJas(xrayManager, xrayVersion) - scanResults.ExtendedScanResults.SecretValidation = jas.CheckForSecretValidation(xrayManager, xrayVersion, scanCmd.validateSecrets) errGroup := new(errgroup.Group) - if scanResults.ExtendedScanResults.EntitledForJas { + if cmdResults.EntitledForJas { // Download (if needed) the analyzer manager in a background routine. errGroup.Go(func() error { return jas.DownloadAnalyzerManagerIfNeeded(0) @@ -283,73 +293,58 @@ func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recor if err = errGroup.Wait(); err != nil { err = errors.New("failed while trying to get Analyzer Manager: " + err.Error()) } - // resultsArr is a two-dimensional array. Each array in it contains a list of ScanResponses that were requested and collected by a specific thread. - resultsArr := make([][]*ScanInfo, threads) - fileProducerConsumer := parallel.NewRunner(scanCmd.threads, 20000, false) - fileProducerErrors := make([][]formats.SimpleJsonError, threads) - indexedFileProducerConsumer := parallel.NewRunner(scanCmd.threads, 20000, false) - indexedFileProducerErrors := make([][]formats.SimpleJsonError, threads) + fileProducerConsumer := parallel.NewRunner(threads, 20000, false) + indexedFileProducerConsumer := parallel.NewRunner(threads, 20000, false) fileCollectingErrorsQueue := clientutils.NewErrorsQueue(1) // Parallel security runner for JAS scans - JasScanProducerConsumer := utils.NewSecurityParallelRunner(scanCmd.threads) - jasScanProducerErrors := make([][]formats.SimpleJsonError, threads) + JasScanProducerConsumer := utils.NewSecurityParallelRunner(threads) + // Start walking on the filesystem to "produce" files that match the given pattern // while the consumer uses the indexer to index those files. - scanCmd.prepareScanTasks(fileProducerConsumer, indexedFileProducerConsumer, &JasScanProducerConsumer, scanResults.ExtendedScanResults.EntitledForJas, scanResults.ExtendedScanResults.SecretValidation, resultsArr, fileProducerErrors, indexedFileProducerErrors, jasScanProducerErrors, fileCollectingErrorsQueue, xrayVersion) + scanCmd.prepareScanTasks(fileProducerConsumer, indexedFileProducerConsumer, &JasScanProducerConsumer, cmdResults, fileCollectingErrorsQueue) scanCmd.performScanTasks(fileProducerConsumer, indexedFileProducerConsumer, &JasScanProducerConsumer) // Handle results - flatResults := []*utils.ScaScanResult{} - - for _, arr := range resultsArr { - for _, res := range arr { - flatResults = append(flatResults, &utils.ScaScanResult{Target: res.Target, XrayResults: []services.ScanResponse{*res.Result}}) - scanResults.ExtendedScanResults.ApplicabilityScanResults = append(scanResults.ExtendedScanResults.ApplicabilityScanResults, res.ExtendedScanResults.ApplicabilityScanResults...) - scanResults.ExtendedScanResults.SecretsScanResults = append(scanResults.ExtendedScanResults.SecretsScanResults, res.ExtendedScanResults.SecretsScanResults...) - } - } if scanCmd.progress != nil { if err = scanCmd.progress.Quit(); err != nil { return err } - } fileCollectingErr := fileCollectingErrorsQueue.GetError() - var scanErrors []formats.SimpleJsonError if fileCollectingErr != nil { - scanErrors = append(scanErrors, formats.SimpleJsonError{ErrorMessage: fileCollectingErr.Error()}) + cmdResults.Error = errors.Join(cmdResults.Error, fileCollectingErr) } - scanErrors = appendErrorSlice(scanErrors, fileProducerErrors) - scanErrors = appendErrorSlice(scanErrors, indexedFileProducerErrors) - scanErrors = appendErrorSlice(scanErrors, jasScanProducerErrors) - scanResults.ScaResults = flatResults + // Wait for the Download of the AnalyzerManager to complete. + if err = errGroup.Wait(); err != nil { + err = errors.New("failed while trying to get Analyzer Manager: " + err.Error()) + } - if err = utils.NewResultsWriter(scanResults). + if err = output.NewResultsWriter(cmdResults). SetOutputFormat(scanCmd.outputFormat). SetHasViolationContext(scanCmd.hasViolationContext()). SetIncludeVulnerabilities(scanCmd.includeVulnerabilities). SetIncludeLicenses(scanCmd.includeLicenses). SetPrintExtendedTable(scanCmd.printExtendedTable). - SetIsMultipleRootProject(scanResults.IsMultipleProject()). + SetIsMultipleRootProject(cmdResults.HasMultipleTargets()). PrintScanResults(); err != nil { return } - if err = recordResFunc(scanResults); err != nil { + if err = recordResFunc(cmdResults); err != nil { return err } // If includeVulnerabilities is false it means that context was provided, so we need to check for build violations. // If user provided --fail=false, don't fail the build. if scanCmd.fail && !scanCmd.includeVulnerabilities { - if utils.CheckIfFailBuild(scanResults.GetScaScansXrayResults()) { - return utils.NewFailBuildError() + if results.CheckIfFailBuild(cmdResults.GetScaScansXrayResults()) { + return results.NewFailBuildError() } } - if len(scanErrors) > 0 { - return errorutils.CheckError(errors.New(scanErrors[0].ErrorMessage)) + if cmdResults.GetErrors() != nil { + return errorutils.CheckError(cmdResults.GetErrors()) } log.Info("Scan completed successfully.") return nil @@ -363,14 +358,14 @@ func (scanCmd *ScanCommand) CommandName() string { return "xr_scan" } -func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, jasFileProducerConsumer *utils.SecurityParallelRunner, entitledForJas bool, validateSecrets bool, resultsArr [][]*ScanInfo, fileErrors, indexedFileErrors, jasErrors [][]formats.SimpleJsonError, fileCollectingErrorsQueue *clientutils.ErrorsQueue, xrayVersion string) { +func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, jasFileProducerConsumer *utils.SecurityParallelRunner, cmdResults *results.SecurityCommandResults, fileCollectingErrorsQueue *clientutils.ErrorsQueue) { go func() { defer fileProducer.Done() // Iterate over file-spec groups and produce indexing tasks. // When encountering an error, log and move to next group. specFiles := scanCmd.spec.Files for i := range specFiles { - artifactHandlerFunc := scanCmd.createIndexerHandlerFunc(&specFiles[i], entitledForJas, validateSecrets, indexedFileProducer, jasFileProducerConsumer, resultsArr, fileErrors, indexedFileErrors, jasErrors, xrayVersion) + artifactHandlerFunc := scanCmd.createIndexerHandlerFunc(&specFiles[i], cmdResults, indexedFileProducer, jasFileProducerConsumer) taskHandler := getAddTaskToProducerFunc(fileProducer, artifactHandlerFunc) err := collectFilesForIndexing(specFiles[i], taskHandler) @@ -382,17 +377,27 @@ func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer p }() } -func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, entitledForJas bool, validateSecrets bool, indexedFileProducer parallel.Runner, jasFileProducerConsumer *utils.SecurityParallelRunner, resultsArr [][]*ScanInfo, fileErrors, indexedFileErrors, jasErrors [][]formats.SimpleJsonError, xrayVersion string) FileContext { +func (scanCmd *ScanCommand) getBinaryTargetName(binaryPath string) string { + if scanCmd.targetNameOverride != "" { + return scanCmd.targetNameOverride + } + return filepath.Base(binaryPath) +} + +func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, cmdResults *results.SecurityCommandResults, indexedFileProducer parallel.Runner, jasFileProducerConsumer *utils.SecurityParallelRunner) FileContext { return func(filePath string) parallel.TaskFunc { return func(threadId int) (err error) { logMsgPrefix := clientutils.GetLogMsgPrefix(threadId, false) - log.Info(logMsgPrefix+"Indexing file:", filePath) + // Create a scan target for the file. + targetResults := cmdResults.NewScanResults(results.ScanTarget{Target: filePath, Name: scanCmd.getBinaryTargetName(filePath)}) + log.Info(logMsgPrefix+"Indexing file:", targetResults.Target) if scanCmd.progress != nil { - scanCmd.progress.SetHeadlineMsg("Indexing file: " + filepath.Base(filePath) + " 🗄") + scanCmd.progress.SetHeadlineMsg("Indexing file: " + targetResults.Name + " 🗄") } - graph, err := scanCmd.indexFile(filePath) + // Index the file and get the dependencies graph. + graph, err := scanCmd.indexFile(targetResults.Target) if err != nil { - fileErrors[threadId] = append(fileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) + targetResults.AddError(err) return err } // In case of empty graph returned by the indexer, @@ -403,7 +408,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, entitledFo } // Add a new task to the second producer/consumer // which will send the indexed binary to Xray and then will store the received result. - taskFunc := func(threadId int) (err error) { + taskFunc := func(scanThreadId int) (err error) { params := &services.XrayGraphScanParams{ BinaryGraph: graph, RepoPath: getXrayRepoPathFromTarget(file.Target), @@ -420,7 +425,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, entitledFo scanGraphParams := scangraph.NewScanGraphParams(). SetServerDetails(scanCmd.serverDetails). SetXrayGraphScanParams(params). - SetXrayVersion(xrayVersion). + SetXrayVersion(cmdResults.XrayVersion). SetFixableOnly(scanCmd.fixableOnly). SetSeverityLevel(scanCmd.minSeverityFilter.String()) xrayManager, err := xray.CreateXrayServiceManager(scanGraphParams.ServerDetails()) @@ -429,52 +434,72 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, entitledFo } graphScanResults, err := scangraph.RunScanGraphAndGetResults(scanGraphParams, xrayManager) if err != nil { - log.Error(fmt.Sprintf("scanning '%s' failed with error: %s", graph.Id, err.Error())) - indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) + log.Error(fmt.Sprintf("%s sca scanning '%s' failed with error: %s", clientutils.GetLogMsgPrefix(scanThreadId, false), graph.Id, err.Error())) + targetResults.AddError(err) return + } else { + targetResults.NewScaScanResults(*graphScanResults) + targetResults.Technology = techutils.Technology(graphScanResults.ScannedPackageType) } - - scanResults := utils.Results{ - ScaResults: []*utils.ScaScanResult{{XrayResults: []services.ScanResponse{*graphScanResults}}}, - ExtendedScanResults: &utils.ExtendedScanResults{}, - MultiScanId: scanGraphParams.XrayGraphScanParams().MultiScanId, + if !cmdResults.EntitledForJas { + return } - if entitledForJas && scanCmd.commandSupportsJAS { - // Run Jas scans - jasErrHandlerFunc := func(err error) { - jasErrors[threadId] = append(jasErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) - } - workingDirs := []string{filePath} - depsList := depsListFromVulnerabilities(*graphScanResults) - jfrogAppsConfig, err := jas.CreateJFrogAppsConfig(workingDirs) - if err != nil { - log.Error(fmt.Sprintf("failed to create JFrogAppsConfig: %s", err.Error())) - indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) - } - scanner, err := jas.CreateJasScanner(jfrogAppsConfig, scanCmd.serverDetails, scanCmd.minSeverityFilter, jas.GetAnalyzerManagerXscEnvVars(scanResults.MultiScanId, validateSecrets, techutils.Technology(graphScanResults.ScannedPackageType))) - if err != nil { - log.Error(fmt.Sprintf("failed to create jas scanner: %s", err.Error())) - indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) - } else if scanner == nil { - log.Debug(fmt.Sprintf("Jas scanner was not created for %s, skipping Jas scans", filePath)) - return nil - } - err = runner.AddJasScannersTasks(jasFileProducerConsumer, &scanResults, &depsList, false, scanner, applicability.ApplicabilityDockerScanScanType, secrets.SecretsScannerDockerScanType, jasErrHandlerFunc, utils.GetAllSupportedScans(), nil, "") - if err != nil { - log.Error(fmt.Sprintf("scanning '%s' failed with error: %s", graph.Id, err.Error())) - indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) - } + // Run Jas scans + scanner, err := getJasScanner(cmdResults.MultiScanId, scanCmd.serverDetails, targetResults, cmdResults.SecretValidation, scanCmd.minSeverityFilter) + if err != nil { + return err + } + module, err := getJasModule(targetResults) + if err != nil { + return err + } + jasParams := runner.JasRunnerParams{ + Runner: jasFileProducerConsumer, + ServerDetails: scanCmd.serverDetails, + Scanner: scanner, + Module: module, + ScansToPreform: utils.GetAllSupportedScans(), + SecretsScanType: secrets.SecretsScannerDockerScanType, + DirectDependencies: directDepsListFromVulnerabilities(*graphScanResults), + ApplicableScanType: applicability.ApplicabilityDockerScanScanType, + ScanResults: targetResults, + } + err = runner.AddJasScannersTasks(jasParams) + if err != nil { + log.Error(fmt.Sprintf("%s jas scanning failed with error: %s", clientutils.GetLogMsgPrefix(scanThreadId, false), err.Error())) + targetResults.AddError(err) + } else if scanner == nil { + log.Debug(fmt.Sprintf("Jas scanner was not created for %s, skipping Jas scans", filePath)) + return nil } - resultsArr[threadId] = append(resultsArr[threadId], &ScanInfo{Target: filePath, Result: graphScanResults, ExtendedScanResults: scanResults.ExtendedScanResults}) return } - _, _ = indexedFileProducer.AddTask(taskFunc) return } } } +func getJasScanner(multiScanId string, serverDetails *config.ServerDetails, targetResults *results.TargetResults, secretValidation bool, minSeverity severityutils.Severity) (*jas.JasScanner, error) { + scanner, err := jas.CreateJasScanner(serverDetails, secretValidation, minSeverity, jas.GetAnalyzerManagerXscEnvVars(multiScanId, targetResults.GetTechnologies()...)) + if err != nil { + log.Error(fmt.Sprintf("failed to create jas scanner: %s", err.Error())) + targetResults.AddError(err) + return nil, err + } + return scanner, nil +} + +func getJasModule(targetResults *results.TargetResults) (jfrogappsconfig.Module, error) { + jfrogAppsConfig, err := jas.CreateJFrogAppsConfig([]string{targetResults.Target}) + if err != nil { + log.Error(fmt.Sprintf("failed to create JFrogAppsConfig module: %s", err.Error())) + targetResults.AddError(err) + return jfrogappsconfig.Module{}, err + } + return jfrogAppsConfig.Modules[0], nil +} + func getAddTaskToProducerFunc(producer parallel.Runner, fileHandlerFunc FileContext) indexFileHandlerFunc { return func(filePath string) { taskFunc := fileHandlerFunc(filePath) @@ -567,14 +592,8 @@ func getXrayRepoPathFromTarget(target string) (repoPath string) { return target[:strings.LastIndex(target, "/")+1] } -func appendErrorSlice(scanErrors []formats.SimpleJsonError, errorsToAdd [][]formats.SimpleJsonError) []formats.SimpleJsonError { - for _, errorSlice := range errorsToAdd { - scanErrors = append(scanErrors, errorSlice...) - } - return scanErrors -} - -func depsListFromVulnerabilities(scanResult ...services.ScanResponse) (depsList []string) { +func directDepsListFromVulnerabilities(scanResult ...services.ScanResponse) *[]string { + depsList := []string{} for _, result := range scanResult { for _, vulnerability := range result.Vulnerabilities { dependencies := maps.Keys(vulnerability.Components) @@ -585,7 +604,7 @@ func depsListFromVulnerabilities(scanResult ...services.ScanResponse) (depsList } } } - return + return &depsList } func ConditionalUploadDefaultScanFunc(serverDetails *config.ServerDetails, fileSpec *spec.SpecFiles, threads int, scanOutputFormat format.OutputFormat) error { diff --git a/go.mod b/go.mod index 1992b153..3eb77e51 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/google/go-github/v56 v56.0.0 github.com/gookit/color v1.5.4 github.com/jfrog/build-info-go v1.10.3 - github.com/jfrog/froggit-go v1.16.1 + github.com/jfrog/froggit-go v1.16.2 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-apps-config v1.0.1 github.com/jfrog/jfrog-cli-core/v2 v2.56.3 @@ -16,9 +16,9 @@ require ( github.com/owenrumney/go-sarif/v2 v2.3.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli v1.22.15 - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/sync v0.8.0 - golang.org/x/text v0.18.0 + golang.org/x/text v0.19.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -34,7 +34,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/c-bata/go-prompt v0.2.5 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.4.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -49,13 +49,12 @@ require ( github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grokify/mogo v0.62.6 // indirect + github.com/grokify/mogo v0.64.12 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jedib0t/go-pretty/v6 v6.5.9 // indirect @@ -64,7 +63,7 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/ktrysmt/go-bitbucket v0.9.73 // indirect + github.com/ktrysmt/go-bitbucket v0.9.80 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -94,22 +93,20 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/vbauerster/mpb/v8 v8.8.3 // indirect - github.com/xanzy/go-gitlab v0.95.2 // indirect + github.com/xanzy/go-gitlab v0.110.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.25.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/tools v0.26.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index a233827c..9365e582 100644 --- a/go.sum +++ b/go.sum @@ -1,642 +1,30 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= -cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= -cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= -cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= -cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= -cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= -cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= -cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= -cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= -cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= -cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= -cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= -cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= -cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= -cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= -cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= -cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= -cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= -cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= -cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= -cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= -cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= -cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= -cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= -cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= -cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= -cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= -cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= -cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= -cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= -cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= -cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= -cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= -cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= -cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= -cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= -cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= -cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= -cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= -cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= -cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= -cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= -cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= -cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= -cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= -cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= -cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= -cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= -cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= -cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= -cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= -cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= -cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= -cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= -cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= -cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= -cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= -cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= -cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= -cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= -cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= -cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= -cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= -cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= -cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= -cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CycloneDX/cyclonedx-go v0.9.0 h1:inaif7qD8bivyxp7XLgxUYtOXWtDez7+j72qKTMQTb8= github.com/CycloneDX/cyclonedx-go v0.9.0/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beevik/etree v1.4.0 h1:oz1UedHRepuY3p4N5OjE0nK1WLCqtzHf25bxplKOHLs= github.com/beevik/etree v1.4.0/go.mod h1:cyWiXwGoasx60gHvtnEh5x8+uIjUVnjWqBvEnhnqKDA= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -644,12 +32,6 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/c-bata/go-prompt v0.2.5 h1:3zg6PecEywxNn0xiqcXHD96fkbxghD+gdB2tbsYfl+Y= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -659,23 +41,9 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= +github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -685,34 +53,15 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/forPelevin/gomoji v1.2.0 h1:9k4WVSSkE1ARO/BWywxgEUBvR/jMnao6EZzrql5nxJ8= github.com/forPelevin/gomoji v1.2.0/go.mod h1:8+Z3KNGkdslmeGZBC3tCrwMrcPy5GRzAD+gL9NAwMXg= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -721,14 +70,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gfleury/go-bitbucket-v1 v0.0.0-20230825095122-9bc1711434ab h1:+7KwW/yy/ThnRXW9khailFFncxJiiFpxyk5BI9GK9pI= github.com/gfleury/go-bitbucket-v1 v0.0.0-20230825095122-9bc1711434ab/go.mod h1:IqOZzks2wlWCIai0esXnZPdPwxF2yOz0HcCYw5I4pCg= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= @@ -737,75 +80,20 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -813,77 +101,21 @@ github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBW github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= -github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/grokify/mogo v0.62.6 h1:90QtIE5vvQ30QbrUcC1trvKHbBM/CqpMpD2y+EiVuFs= -github.com/grokify/mogo v0.62.6/go.mod h1:gK6Qf761S7iOxI3LrILjoTOJWdQPgs8LxSPdDmuX8Xo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grokify/mogo v0.64.12 h1:BNrZ1qBFuX4qu5722CW6qtqu/mrrsZ3bhKu/w1KowKg= +github.com/grokify/mogo v0.64.12/go.mod h1:lDhfYIiOhJo7C2U3aL00PlUU9gLvmTONi4MdIWoGmGM= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= @@ -892,8 +124,8 @@ github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5 github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= github.com/jfrog/build-info-go v1.10.3 h1:9nqBdZD6xkuxiOvxg+idZ79QLFWQNuucvKkl8Xb42kw= github.com/jfrog/build-info-go v1.10.3/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= -github.com/jfrog/froggit-go v1.16.1 h1:FBIM1qevX/ag9unfmpGzfmZ36D8ulOJ+DPTSFUk3l5U= -github.com/jfrog/froggit-go v1.16.1/go.mod h1:TEJSzgiV+3D/GVGE8Y6j46ut1jrBLD1FL6WdMdKwwCE= +github.com/jfrog/froggit-go v1.16.2 h1:F//S83iXH14qsCwYzv0zB2JtjS2pJVEsUoEmYA+37dQ= +github.com/jfrog/froggit-go v1.16.2/go.mod h1:5VpdQfAcbuyFl9x/x8HGm7kVk719kEtW/8YJFvKcHPA= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= @@ -902,28 +134,18 @@ github.com/jfrog/jfrog-cli-core/v2 v2.56.3 h1:9ZZ7TGpobk4XShPzrHkRGfpYzs1w0rg7Hq github.com/jfrog/jfrog-cli-core/v2 v2.56.3/go.mod h1:xL9b2DrH5FemiTuk2bfUBfbQYC/RvpBkPxxV6XxssXs= github.com/jfrog/jfrog-client-go v1.47.2 h1:Lu+2n4EU+MzNfotV1VOvF/ZQIWsQJg11Z4YSVhumFy0= github.com/jfrog/jfrog-client-go v1.47.2/go.mod h1:fx2fq5XwZ7e2pzpBB9pXsP8+ZdKLB8g+A6fjGU6F2XI= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -933,11 +155,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ktrysmt/go-bitbucket v0.9.73 h1:4SvrTHvqW3vqJUjg+62/QGFNz0TrvwWs7+YsUE/gsbQ= -github.com/ktrysmt/go-bitbucket v0.9.73/go.mod h1:8kiUmY5juS79IQnMd+SUYhSNlqIft31b1/bHx4clC7w= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/ktrysmt/go-bitbucket v0.9.80 h1:S+vZTXKx/VG5yCaX4I3Bmwo8lxWr4ifvuHdTboHTMMc= +github.com/ktrysmt/go-bitbucket v0.9.80/go.mod h1:b8ogWEGxQMWoeFnT1ZE4aHIPGindI+9z/zAW/OVFjk0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= @@ -957,13 +176,10 @@ github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63QvybxhW1iBmQWEaCKdc4SKgALfTNZ+OphU= github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -978,10 +194,6 @@ github.com/owenrumney/go-sarif/v2 v2.3.0 h1:wP5yEpI53zr0v5cBmagXzLbHZp9Oylyo3AJD github.com/owenrumney/go-sarif/v2 v2.3.0/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -989,33 +201,22 @@ github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFz github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -1027,10 +228,6 @@ github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= @@ -1046,12 +243,9 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -1068,8 +262,8 @@ github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnN github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/xanzy/go-gitlab v0.95.2 h1:4p0IirHqEp5f0baK/aQqr4TR57IsD+8e4fuyAA1yi88= -github.com/xanzy/go-gitlab v0.95.2/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= +github.com/xanzy/go-gitlab v0.110.0 h1:hsFIFp01v/0D0sdUXoZfRk6CROzZbHQplk6NzKSFKhc= +github.com/xanzy/go-gitlab v0.110.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= @@ -1082,713 +276,119 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E= -google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= -google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1800,54 +400,7 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= -modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= -modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/jas/analyzermanager.go b/jas/analyzermanager.go index 1e2a4b5a..788d622a 100644 --- a/jas/analyzermanager.go +++ b/jas/analyzermanager.go @@ -80,7 +80,7 @@ func (am *AnalyzerManager) ExecWithOutputFile(configFile, scanCommand, workingDi cmd.Env = utils.ToCommandEnvVars(envVars) cmd.Dir = workingDir output, err := cmd.CombinedOutput() - if isCI() || err != nil { + if utils.IsCI() || err != nil { if len(output) > 0 { log.Debug(fmt.Sprintf("%s %q output: %s", workingDir, strings.Join(cmd.Args, " "), string(output))) } @@ -123,8 +123,7 @@ func GetAnalyzerManagerExecutable() (analyzerManagerPath string, err error) { return } if !exists { - log.Debug(fmt.Sprintf("The analyzer manager executable was not found at %s", analyzerManagerPath)) - err = errors.New("unable to locate the analyzer manager package. Advanced security scans cannot be performed without this package") + err = fmt.Errorf("unable to locate the analyzer manager package at %s. Advanced security scans cannot be performed without this package", analyzerManagerPath) } return analyzerManagerPath, err } @@ -137,10 +136,6 @@ func GetAnalyzerManagerExecutableName() string { return analyzerManager } -func isCI() bool { - return strings.ToLower(os.Getenv(coreutils.CI)) == "true" -} - func GetAnalyzerManagerEnvVariables(serverDetails *config.ServerDetails) (envVars map[string]string, err error) { envVars = map[string]string{ jfUserEnvVariable: serverDetails.User, @@ -148,7 +143,7 @@ func GetAnalyzerManagerEnvVariables(serverDetails *config.ServerDetails) (envVar jfPlatformUrlEnvVariable: serverDetails.Url, jfTokenEnvVariable: serverDetails.AccessToken, } - if !isCI() { + if !utils.IsCI() { analyzerManagerLogFolder, err := coreutils.CreateDirInJfrogHome(filepath.Join(coreutils.JfrogLogsDirName, analyzerManagerLogDirName)) if err != nil { return nil, err diff --git a/jas/applicability/applicabilitymanager.go b/jas/applicability/applicabilitymanager.go index 45b26ece..5e346bea 100644 --- a/jas/applicability/applicabilitymanager.go +++ b/jas/applicability/applicabilitymanager.go @@ -5,9 +5,9 @@ import ( "github.com/jfrog/gofrog/datastructures" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "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" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" diff --git a/jas/applicability/applicabilitymanager_test.go b/jas/applicability/applicabilitymanager_test.go index 55da459e..a26fb0c7 100644 --- a/jas/applicability/applicabilitymanager_test.go +++ b/jas/applicability/applicabilitymanager_test.go @@ -37,12 +37,11 @@ func TestNewApplicabilityScanManager_DependencyTreeDoesntExist(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() // Act - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, nil, scanner, false, ApplicabilityScannerType, "temoDirPath") + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, nil, scanner, false, ApplicabilityScannerType, "tempDirPath") // Assert if assert.NotNil(t, applicabilityManager) { assert.NotNil(t, applicabilityManager.scanner.ScannerDirCleanupFunc) - assert.Len(t, applicabilityManager.scanner.JFrogAppsConfig.Modules, 1) assert.NotEmpty(t, applicabilityManager.configFileName) assert.NotEmpty(t, applicabilityManager.resultsFileName) assert.Empty(t, applicabilityManager.directDependenciesCves) @@ -300,6 +299,8 @@ func TestParseResults_NewApplicabilityStatuses(t *testing.T) { // Arrange scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, string(jasutils.Applicability)) require.NoError(t, err) @@ -310,7 +311,7 @@ func TestParseResults_NewApplicabilityStatuses(t *testing.T) { t.Run(tc.name, func(t *testing.T) { applicabilityManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", tc.fileName) var err error - applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.resultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix, scanner.MinSeverity) + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, applicabilityDocsUrlSuffix, scanner.MinSeverity) if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { assert.Len(t, applicabilityManager.applicabilityScanResults, 1) assert.Len(t, applicabilityManager.applicabilityScanResults[0].Results, tc.expectedResults) diff --git a/jas/common.go b/jas/common.go index dc65cacf..a6499900 100644 --- a/jas/common.go +++ b/jas/common.go @@ -14,8 +14,8 @@ import ( jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "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/severityutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" @@ -32,7 +32,7 @@ import ( ) const ( - NoServerUrlError = "To incorporate the ‘Advanced Security’ scans into the audit output make sure platform url is provided and valid (run 'jf c add' prior to 'jf audit' via CLI, or provide JF_URL via Frogbot)" + NoServerUrlWarn = "To incorporate the ‘Advanced Security’ scans into the audit output make sure platform url is provided and valid (run 'jf c add' prior to 'jf audit' via CLI, or provide JF_URL via Frogbot)" NoServerDetailsError = "jfrog Server details are missing" ) @@ -40,14 +40,13 @@ type JasScanner struct { TempDir string AnalyzerManager AnalyzerManager ServerDetails *config.ServerDetails - JFrogAppsConfig *jfrogappsconfig.JFrogAppsConfig ScannerDirCleanupFunc func() error EnvVars map[string]string Exclusions []string MinSeverity severityutils.Severity } -func CreateJasScanner(jfrogAppsConfig *jfrogappsconfig.JFrogAppsConfig, serverDetails *config.ServerDetails, minSeverity severityutils.Severity, envVars map[string]string, exclusions ...string) (scanner *JasScanner, err error) { +func CreateJasScanner(serverDetails *config.ServerDetails, validateSecrets bool, minSeverity severityutils.Severity, envVars map[string]string, exclusions ...string) (scanner *JasScanner, err error) { if serverDetails == nil { err = errors.New(NoServerDetailsError) return @@ -59,12 +58,12 @@ func CreateJasScanner(jfrogAppsConfig *jfrogappsconfig.JFrogAppsConfig, serverDe if len(serverDetails.ArtifactoryUrl) != 0 { log.Debug("Artifactory URL provided without platform URL") } - log.Warn(NoServerUrlError) + log.Warn(NoServerUrlWarn) return } scanner = &JasScanner{} - if scanner.EnvVars, err = getJasEnvVars(serverDetails, envVars); err != nil { - return + if scanner.EnvVars, err = getJasEnvVars(serverDetails, validateSecrets, envVars); err != nil { + return scanner, err } var tempDir string if tempDir, err = fileutils.CreateTempDir(); err != nil { @@ -75,17 +74,17 @@ func CreateJasScanner(jfrogAppsConfig *jfrogappsconfig.JFrogAppsConfig, serverDe return fileutils.RemoveTempDir(tempDir) } scanner.ServerDetails = serverDetails - scanner.JFrogAppsConfig = jfrogAppsConfig scanner.Exclusions = exclusions scanner.MinSeverity = minSeverity return } -func getJasEnvVars(serverDetails *config.ServerDetails, vars map[string]string) (map[string]string, error) { +func getJasEnvVars(serverDetails *config.ServerDetails, validateSecrets bool, vars map[string]string) (map[string]string, error) { amBasicVars, err := GetAnalyzerManagerEnvVariables(serverDetails) if err != nil { return nil, err } + amBasicVars[JfSecretValidationEnvVariable] = strconv.FormatBool(validateSecrets) return utils.MergeMaps(utils.ToEnvVarsMap(os.Environ()), amBasicVars, vars), nil } @@ -94,9 +93,9 @@ func CreateJFrogAppsConfig(workingDirs []string) (*jfrogappsconfig.JFrogAppsConf return nil, errorutils.CheckError(err) } else if jfrogAppsConfig != nil { // jfrog-apps-config.yml exist in the workspace - for _, module := range jfrogAppsConfig.Modules { + for i := range jfrogAppsConfig.Modules { // converting to absolute path before starting the scan flow - module.SourceRoot, err = filepath.Abs(module.SourceRoot) + jfrogAppsConfig.Modules[i].SourceRoot, err = filepath.Abs(jfrogAppsConfig.Modules[i].SourceRoot) if err != nil { return nil, errorutils.CheckError(err) } @@ -202,7 +201,7 @@ func excludeMinSeverityResults(sarifResults []*sarif.Result, minSeverity severit func addScoreToRunRules(sarifRun *sarif.Run) { for _, sarifResult := range sarifRun.Results { - if rule, err := sarifRun.GetRuleById(*sarifResult.RuleID); err == nil { + if rule, err := sarifRun.GetRuleById(sarifutils.GetResultRuleId(sarifResult)); err == nil { // Add to the rule security-severity score based on results severity severity, err := severityutils.ParseSeverity(sarifutils.GetResultLevel(sarifResult), true) if err != nil { @@ -250,11 +249,9 @@ var FakeBasicXrayResults = []services.ScanResponse{ }, } -func InitJasTest(t *testing.T, workingDirs ...string) (*JasScanner, func()) { +func InitJasTest(t *testing.T) (*JasScanner, func()) { assert.NoError(t, DownloadAnalyzerManagerIfNeeded(0)) - jfrogAppsConfigForTest, err := CreateJFrogAppsConfig(workingDirs) - assert.NoError(t, err) - scanner, err := CreateJasScanner(jfrogAppsConfigForTest, &FakeServerDetails, "", GetAnalyzerManagerXscEnvVars("", false)) + scanner, err := CreateJasScanner(&FakeServerDetails, false, "", GetAnalyzerManagerXscEnvVars("")) assert.NoError(t, err) return scanner, func() { assert.NoError(t, scanner.ScannerDirCleanupFunc()) @@ -265,6 +262,15 @@ func GetTestDataPath() string { return filepath.Join("..", "..", "tests", "testdata", "other") } +func GetModule(root string, appConfig *jfrogappsconfig.JFrogAppsConfig) *jfrogappsconfig.Module { + for _, module := range appConfig.Modules { + if module.SourceRoot == root { + return &module + } + } + return nil +} + func ShouldSkipScanner(module jfrogappsconfig.Module, scanType jasutils.JasScanType) bool { lowerScanType := strings.ToLower(string(scanType)) if slices.Contains(module.ExcludeScanners, lowerScanType) { @@ -329,9 +335,8 @@ func CheckForSecretValidation(xrayManager *xray.XrayServicesManager, xrayVersion return err == nil && isEnabled } -func GetAnalyzerManagerXscEnvVars(msi string, validateSecrets bool, technologies ...techutils.Technology) map[string]string { +func GetAnalyzerManagerXscEnvVars(msi string, technologies ...techutils.Technology) map[string]string { envVars := map[string]string{utils.JfMsiEnvVariable: msi} - envVars[JfSecretValidationEnvVariable] = strconv.FormatBool(validateSecrets) if len(technologies) != 1 { return envVars } diff --git a/jas/common_test.go b/jas/common_test.go index 15d7cef2..f499db8e 100644 --- a/jas/common_test.go +++ b/jas/common_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "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/techutils" "github.com/owenrumney/go-sarif/v2/sarif" @@ -114,11 +114,13 @@ func TestConvertToFilesExcludePatterns(t *testing.T) { } } -func TestGetAnalyzerManagerEnvVariables(t *testing.T) { +func TestGetJasEnvVars(t *testing.T) { tests := []struct { - name string - serverDetails *config.ServerDetails - expectedOutput map[string]string + name string + serverDetails *config.ServerDetails + validateSecrets bool + extraEnvVars map[string]string + expectedOutput map[string]string }{ { name: "Valid server details", @@ -129,16 +131,36 @@ func TestGetAnalyzerManagerEnvVariables(t *testing.T) { AccessToken: "token", }, expectedOutput: map[string]string{ - jfPlatformUrlEnvVariable: "url", - jfUserEnvVariable: "user", - jfPasswordEnvVariable: "password", - jfTokenEnvVariable: "token", + jfPlatformUrlEnvVariable: "url", + jfUserEnvVariable: "user", + jfPasswordEnvVariable: "password", + jfTokenEnvVariable: "token", + JfSecretValidationEnvVariable: "false", + }, + }, + { + name: "With validate secrets", + serverDetails: &config.ServerDetails{ + Url: "url", + User: "user", + Password: "password", + AccessToken: "token", + }, + extraEnvVars: map[string]string{"test": "testValue"}, + validateSecrets: true, + expectedOutput: map[string]string{ + jfPlatformUrlEnvVariable: "url", + jfUserEnvVariable: "user", + jfPasswordEnvVariable: "password", + jfTokenEnvVariable: "token", + JfSecretValidationEnvVariable: "true", + "test": "testValue", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - envVars, err := GetAnalyzerManagerEnvVariables(test.serverDetails) + envVars, err := getJasEnvVars(test.serverDetails, test.validateSecrets, test.extraEnvVars) assert.NoError(t, err) for expectedKey, expectedValue := range test.expectedOutput { assert.Equal(t, expectedValue, envVars[expectedKey]) @@ -149,55 +171,37 @@ func TestGetAnalyzerManagerEnvVariables(t *testing.T) { func TestGetAnalyzerManagerXscEnvVars(t *testing.T) { tests := []struct { - name string - msi string - validateSecrets bool - technologies []techutils.Technology - expectedOutput map[string]string + name string + msi string + technologies []techutils.Technology + expectedOutput map[string]string }{ { name: "One valid technology", msi: "msi", technologies: []techutils.Technology{techutils.Maven}, expectedOutput: map[string]string{ - JfPackageManagerEnvVariable: string(techutils.Maven), - JfLanguageEnvVariable: string(techutils.Java), - JfSecretValidationEnvVariable: "false", - utils.JfMsiEnvVariable: "msi", + JfPackageManagerEnvVariable: string(techutils.Maven), + JfLanguageEnvVariable: string(techutils.Java), + utils.JfMsiEnvVariable: "msi", }, }, { - name: "Multiple technologies", - msi: "msi", - technologies: []techutils.Technology{techutils.Maven, techutils.Npm}, - expectedOutput: map[string]string{ - JfSecretValidationEnvVariable: "false", - utils.JfMsiEnvVariable: "msi", - }, + name: "Multiple technologies", + msi: "msi", + technologies: []techutils.Technology{techutils.Maven, techutils.Npm}, + expectedOutput: map[string]string{utils.JfMsiEnvVariable: "msi"}, }, { - name: "Zero technologies", - msi: "msi", - technologies: []techutils.Technology{}, - expectedOutput: map[string]string{ - utils.JfMsiEnvVariable: "msi", - JfSecretValidationEnvVariable: "false", - }, - }, - { - name: "with validate secrets", - msi: "msi", - validateSecrets: true, - technologies: []techutils.Technology{}, - expectedOutput: map[string]string{ - utils.JfMsiEnvVariable: "msi", - JfSecretValidationEnvVariable: "true", - }, + name: "Zero technologies", + msi: "msi", + technologies: []techutils.Technology{}, + expectedOutput: map[string]string{utils.JfMsiEnvVariable: "msi"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expectedOutput, GetAnalyzerManagerXscEnvVars(test.msi, test.validateSecrets, test.technologies...)) + assert.Equal(t, test.expectedOutput, GetAnalyzerManagerXscEnvVars(test.msi, test.technologies...)) }) } } diff --git a/jas/iac/iacscanner.go b/jas/iac/iacscanner.go index 67d0e98f..5a139d37 100644 --- a/jas/iac/iacscanner.go +++ b/jas/iac/iacscanner.go @@ -4,8 +4,8 @@ import ( "path/filepath" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/jas" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" clientutils "github.com/jfrog/jfrog-client-go/utils" diff --git a/jas/iac/iacscanner_test.go b/jas/iac/iacscanner_test.go index e1acc362..dd7b4348 100644 --- a/jas/iac/iacscanner_test.go +++ b/jas/iac/iacscanner_test.go @@ -16,8 +16,10 @@ import ( ) func TestNewIacScanManager(t *testing.T) { - scanner, cleanUp := jas.InitJasTest(t, "currentDir") + scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{"currentDir"}) + assert.NoError(t, err) // Act iacScanManager := newIacScanManager(scanner, "temoDirPath") @@ -26,13 +28,13 @@ func TestNewIacScanManager(t *testing.T) { if assert.NotNil(t, iacScanManager) { assert.NotEmpty(t, iacScanManager.configFileName) assert.NotEmpty(t, iacScanManager.resultsFileName) - assert.NotEmpty(t, iacScanManager.scanner.JFrogAppsConfig.Modules[0].SourceRoot) + assert.NotEmpty(t, jfrogAppsConfigForTest.Modules[0].SourceRoot) assert.Equal(t, &jas.FakeServerDetails, iacScanManager.scanner.ServerDetails) } } func TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { - scanner, cleanUp := jas.InitJasTest(t, "currentDir") + scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() scannerTempDir, err := jas.CreateScannerTempDirectory(scanner, jasutils.IaC.String()) @@ -58,14 +60,15 @@ func TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { func TestIacParseResults_EmptyResults(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) // Arrange iacScanManager := newIacScanManager(scanner, "temoDirPath") iacScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "iac-scan", "no-violations.sarif") // Act - var err error - iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, iacDocsUrlSuffix, scanner.MinSeverity) + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, iacDocsUrlSuffix, scanner.MinSeverity) if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { assert.Len(t, iacScanManager.iacScannerResults, 1) assert.Empty(t, iacScanManager.iacScannerResults[0].Results) @@ -75,13 +78,14 @@ func TestIacParseResults_EmptyResults(t *testing.T) { func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) // Arrange iacScanManager := newIacScanManager(scanner, "temoDirPath") iacScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "iac-scan", "contains-iac-violations.sarif") // Act - var err error - iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, iacDocsUrlSuffix, scanner.MinSeverity) + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, iacDocsUrlSuffix, scanner.MinSeverity) if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { assert.Len(t, iacScanManager.iacScannerResults, 1) assert.Len(t, iacScanManager.iacScannerResults[0].Results, 4) diff --git a/jas/runner/jasrunner.go b/jas/runner/jasrunner.go index d113b878..0c091c07 100644 --- a/jas/runner/jasrunner.go +++ b/jas/runner/jasrunner.go @@ -3,8 +3,10 @@ package runner import ( "encoding/json" "fmt" + "github.com/jfrog/gofrog/parallel" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-security/jas" "github.com/jfrog/jfrog-cli-security/jas/applicability" "github.com/jfrog/jfrog-cli-security/jas/iac" @@ -12,6 +14,7 @@ import ( "github.com/jfrog/jfrog-cli-security/jas/secrets" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xsc/services" @@ -19,88 +22,105 @@ import ( "golang.org/x/exp/slices" ) -func AddJasScannersTasks(securityParallelRunner *utils.SecurityParallelRunner, scanResults *utils.Results, directDependencies *[]string, thirdPartyApplicabilityScan bool, scanner *jas.JasScanner, scanType applicability.ApplicabilityScanType, - secretsScanType secrets.SecretsScanType, errHandlerFunc func(error), scansToPreform []utils.SubScanType, configProfile *services.ConfigProfile, scansOutputDir string) (err error) { +type JasRunnerParams struct { + Runner *utils.SecurityParallelRunner + ServerDetails *config.ServerDetails + Scanner *jas.JasScanner + + Module jfrogappsconfig.Module + ConfigProfile *services.ConfigProfile + + ScansToPreform []utils.SubScanType + + SecretsScanType secrets.SecretsScanType + + DirectDependencies *[]string + ThirdPartyApplicabilityScan bool + ApplicableScanType applicability.ApplicabilityScanType + + ScanResults *results.TargetResults + TargetOutputDir string +} + +func AddJasScannersTasks(params JasRunnerParams) (err error) { // Set the analyzer manager executable path. - if scanner.AnalyzerManager.AnalyzerManagerFullPath, err = jas.GetAnalyzerManagerExecutable(); err != nil { + if params.Scanner.AnalyzerManager.AnalyzerManagerFullPath, err = jas.GetAnalyzerManagerExecutable(); err != nil { return } // For docker scan we support only secrets and contextual scans. runAllScanners := false - if scanType == applicability.ApplicabilityScannerType || secretsScanType == secrets.SecretsScannerType { + if params.ApplicableScanType == applicability.ApplicabilityScannerType || params.SecretsScanType == secrets.SecretsScannerType { runAllScanners = true } - // Set environments variables for analytics in analyzers manager. - // Don't execute other scanners when scanning third party dependencies. - // Currently, if config profile exists, the only possible scanners to run are: Secrets, Sast - if !thirdPartyApplicabilityScan { - for _, module := range scanner.JFrogAppsConfig.Modules { - if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.SecretsScan) { - log.Debug("Skipping secrets scan as requested by input...") - } else if configProfile != nil { - // This code section is related to CentralizedConfig integration in CI Next. - log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run secrets scan...", configProfile.ProfileName)) - if configProfile.Modules[0].ScanConfig.SecretsScannerConfig.EnableSecretsScan { - err = addModuleJasScanTask(jfrogappsconfig.Module{}, jasutils.Secrets, securityParallelRunner, runSecretsScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module, secretsScanType, scansOutputDir), errHandlerFunc) - } else { - log.Debug(fmt.Sprintf("Skipping secrets scan as requested by '%s' config profile...", configProfile.ProfileName)) - } - } else if err = addModuleJasScanTask(module, jasutils.Secrets, securityParallelRunner, runSecretsScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module, secretsScanType, scansOutputDir), errHandlerFunc); err != nil { - return - } - if runAllScanners { - if configProfile == nil { - if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.IacScan) { - log.Debug("Skipping Iac scan as requested by input...") - } else if err = addModuleJasScanTask(module, jasutils.IaC, securityParallelRunner, runIacScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module, scansOutputDir), errHandlerFunc); err != nil { - return - } - } - if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.SastScan) { - log.Debug("Skipping Sast scan as requested by input...") - } else if configProfile != nil { - log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run Sast scan...", configProfile.ProfileName)) - if configProfile.Modules[0].ScanConfig.SastScannerConfig.EnableSastScan { - err = addModuleJasScanTask(jfrogappsconfig.Module{}, jasutils.Sast, securityParallelRunner, runSastScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module, scansOutputDir), errHandlerFunc) - } else { - log.Debug(fmt.Sprintf("Skipping Sast scan as requested by '%s' config profile...", configProfile.ProfileName)) - } - } else if err = addModuleJasScanTask(module, jasutils.Sast, securityParallelRunner, runSastScan(securityParallelRunner, scanner, scanResults.ExtendedScanResults, module, scansOutputDir), errHandlerFunc); err != nil { - return - } - } - } + if err = addJasScanTaskForModuleIfNeeded(params, utils.ContextualAnalysisScan, runContextualScan(params.Runner, params.Scanner, params.ScanResults, params.Module, params.DirectDependencies, params.ThirdPartyApplicabilityScan, params.ApplicableScanType, params.TargetOutputDir)); err != nil { + return } - - if configProfile != nil { - log.Debug("Config profile is in use. Skipping Contextual Analysis scan as it is not currently supported with a config profile...") + if params.ThirdPartyApplicabilityScan { + // Don't execute other scanners when scanning third party dependencies. + return + } + if err = addJasScanTaskForModuleIfNeeded(params, utils.SecretsScan, runSecretsScan(params.Runner, params.Scanner, params.ScanResults.JasResults, params.Module, params.SecretsScanType, params.TargetOutputDir)); err != nil { return } + if !runAllScanners { + return + } + if err = addJasScanTaskForModuleIfNeeded(params, utils.IacScan, runIacScan(params.Runner, params.Scanner, params.ScanResults.JasResults, params.Module, params.TargetOutputDir)); err != nil { + return + } + return addJasScanTaskForModuleIfNeeded(params, utils.SastScan, runSastScan(params.Runner, params.Scanner, params.ScanResults.JasResults, params.Module, params.TargetOutputDir)) +} - if len(scansToPreform) > 0 && !slices.Contains(scansToPreform, utils.ContextualAnalysisScan) { - log.Debug("Skipping contextual analysis scan as requested by input...") - return err +func addJasScanTaskForModuleIfNeeded(params JasRunnerParams, subScan utils.SubScanType, task parallel.TaskFunc) (err error) { + jasType := jasutils.SubScanTypeToJasScanType(subScan) + if jasType == "" { + return fmt.Errorf("failed to determine Jas scan type for %s", subScan) + } + if len(params.ScansToPreform) > 0 && !slices.Contains(params.ScansToPreform, subScan) { + log.Debug(fmt.Sprintf("Skipping %s scan as requested by input...", subScan)) } - for _, module := range scanner.JFrogAppsConfig.Modules { - if err = addModuleJasScanTask(module, jasutils.Applicability, securityParallelRunner, runContextualScan(securityParallelRunner, scanner, scanResults, module, directDependencies, thirdPartyApplicabilityScan, scanType, scansOutputDir), errHandlerFunc); err != nil { + if params.ConfigProfile != nil { + // This code section is related to CentralizedConfig integration in CI Next. + log.Debug(fmt.Sprintf("Using config profile '%s' to determine whether to run %s scan...", params.ConfigProfile.ProfileName, jasType)) + // Currently, if config profile exists, the only possible scanners to run are: Secrets, Sast + enabled := false + switch jasType { + case jasutils.Secrets: + enabled = params.ConfigProfile.Modules[0].ScanConfig.SecretsScannerConfig.EnableSecretsScan + case jasutils.Sast: + enabled = params.ConfigProfile.Modules[0].ScanConfig.SastScannerConfig.EnableSastScan + case jasutils.IaC: + log.Debug("Skipping Iac scan as it is not currently supported with a config profile...") + return + case jasutils.Applicability: + log.Debug("Skipping Contextual Analysis scan as it is not currently supported with a config profile...") return } + if enabled { + err = addModuleJasScanTask(jasType, params.Runner, task, params.ScanResults) + } else { + log.Debug(fmt.Sprintf("Skipping %s scan as requested by '%s' config profile...", jasType, params.ConfigProfile.ProfileName)) + } + return } - return err -} - -func addModuleJasScanTask(module jfrogappsconfig.Module, scanType jasutils.JasScanType, securityParallelRunner *utils.SecurityParallelRunner, task parallel.TaskFunc, errHandlerFunc func(error)) (err error) { - if jas.ShouldSkipScanner(module, scanType) { + if jas.ShouldSkipScanner(params.Module, jasType) { + log.Debug(fmt.Sprintf("Skipping %s scan as requested by local module config...", subScan)) return } + return addModuleJasScanTask(jasType, params.Runner, task, params.ScanResults) +} + +func addModuleJasScanTask(scanType jasutils.JasScanType, securityParallelRunner *utils.SecurityParallelRunner, task parallel.TaskFunc, scanResults *results.TargetResults) (err error) { securityParallelRunner.JasScannersWg.Add(1) - if _, err = securityParallelRunner.Runner.AddTaskWithError(task, errHandlerFunc); err != nil { + if _, err = securityParallelRunner.Runner.AddTaskWithError(task, func(err error) { + scanResults.AddError(err) + }); err != nil { err = fmt.Errorf("failed to create %s scan task: %s", scanType, err.Error()) } return } -func runSecretsScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *utils.ExtendedScanResults, +func runSecretsScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *results.JasScansResults, module jfrogappsconfig.Module, secretsScanType secrets.SecretsScanType, scansOutputDir string) parallel.TaskFunc { return func(threadId int) (err error) { defer func() { @@ -111,14 +131,14 @@ func runSecretsScan(securityParallelRunner *utils.SecurityParallelRunner, scanne return fmt.Errorf("%s%s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) } securityParallelRunner.ResultsMu.Lock() + defer securityParallelRunner.ResultsMu.Unlock() extendedScanResults.SecretsScanResults = append(extendedScanResults.SecretsScanResults, results...) err = dumpSarifRunToFileIfNeeded(results, scansOutputDir, jasutils.Secrets) - securityParallelRunner.ResultsMu.Unlock() return } } -func runIacScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *utils.ExtendedScanResults, +func runIacScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *results.JasScansResults, module jfrogappsconfig.Module, scansOutputDir string) parallel.TaskFunc { return func(threadId int) (err error) { defer func() { @@ -129,14 +149,14 @@ func runIacScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *j return fmt.Errorf("%s %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) } securityParallelRunner.ResultsMu.Lock() + defer securityParallelRunner.ResultsMu.Unlock() extendedScanResults.IacScanResults = append(extendedScanResults.IacScanResults, results...) err = dumpSarifRunToFileIfNeeded(results, scansOutputDir, jasutils.IaC) - securityParallelRunner.ResultsMu.Unlock() return } } -func runSastScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *utils.ExtendedScanResults, +func runSastScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, extendedScanResults *results.JasScansResults, module jfrogappsconfig.Module, scansOutputDir string) parallel.TaskFunc { return func(threadId int) (err error) { defer func() { @@ -147,14 +167,14 @@ func runSastScan(securityParallelRunner *utils.SecurityParallelRunner, scanner * return fmt.Errorf("%s %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) } securityParallelRunner.ResultsMu.Lock() + defer securityParallelRunner.ResultsMu.Unlock() extendedScanResults.SastScanResults = append(extendedScanResults.SastScanResults, results...) err = dumpSarifRunToFileIfNeeded(results, scansOutputDir, jasutils.Sast) - securityParallelRunner.ResultsMu.Unlock() return } } -func runContextualScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, scanResults *utils.Results, +func runContextualScan(securityParallelRunner *utils.SecurityParallelRunner, scanner *jas.JasScanner, scanResults *results.TargetResults, module jfrogappsconfig.Module, directDependencies *[]string, thirdPartyApplicabilityScan bool, scanType applicability.ApplicabilityScanType, scansOutputDir string) parallel.TaskFunc { return func(threadId int) (err error) { defer func() { @@ -167,9 +187,9 @@ func runContextualScan(securityParallelRunner *utils.SecurityParallelRunner, sca return fmt.Errorf("%s %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) } securityParallelRunner.ResultsMu.Lock() - scanResults.ExtendedScanResults.ApplicabilityScanResults = append(scanResults.ExtendedScanResults.ApplicabilityScanResults, results...) + defer securityParallelRunner.ResultsMu.Unlock() + scanResults.JasResults.ApplicabilityScanResults = append(scanResults.JasResults.ApplicabilityScanResults, results...) err = dumpSarifRunToFileIfNeeded(results, scansOutputDir, jasutils.Applicability) - securityParallelRunner.ResultsMu.Unlock() return } } diff --git a/jas/runner/jasrunner_test.go b/jas/runner/jasrunner_test.go index a6c2f446..cdddf403 100644 --- a/jas/runner/jasrunner_test.go +++ b/jas/runner/jasrunner_test.go @@ -11,6 +11,7 @@ import ( "github.com/jfrog/jfrog-cli-security/jas/applicability" "github.com/jfrog/jfrog-cli-security/jas/secrets" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/stretchr/testify/assert" @@ -26,7 +27,7 @@ func TestJasRunner_AnalyzerManagerNotExist(t *testing.T) { defer func() { assert.NoError(t, os.Unsetenv(coreutils.HomeDir)) }() - scanner, err := jas.CreateJasScanner(nil, &jas.FakeServerDetails, "", jas.GetAnalyzerManagerXscEnvVars("", false)) + scanner, err := jas.CreateJasScanner(&jas.FakeServerDetails, false, "", jas.GetAnalyzerManagerXscEnvVars("")) assert.NoError(t, err) if scanner.AnalyzerManager.AnalyzerManagerFullPath, err = jas.GetAnalyzerManagerExecutable(); err != nil { return @@ -37,14 +38,24 @@ func TestJasRunner_AnalyzerManagerNotExist(t *testing.T) { } func TestJasRunner(t *testing.T) { + assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded(0)) securityParallelRunnerForTest := utils.CreateSecurityParallelRunner(cliutils.Threads) - scanResults := &utils.Results{ScaResults: []*utils.ScaScanResult{{Technology: techutils.Pip, XrayResults: jas.FakeBasicXrayResults}}, ExtendedScanResults: &utils.ExtendedScanResults{}} + targetResults := results.NewCommandResults(utils.SourceCode, "", true, true).NewScanResults(results.ScanTarget{Target: "target", Technology: techutils.Pip}) - jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig(nil) - assert.NoError(t, err) - jasScanner, err := jas.CreateJasScanner(jfrogAppsConfigForTest, &jas.FakeServerDetails, "", jas.GetAnalyzerManagerXscEnvVars("", false, scanResults.GetScaScannedTechnologies()...)) + jasScanner, err := jas.CreateJasScanner(&jas.FakeServerDetails, false, "", jas.GetAnalyzerManagerXscEnvVars("", targetResults.GetTechnologies()...)) assert.NoError(t, err) - err = AddJasScannersTasks(securityParallelRunnerForTest, scanResults, &[]string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, false, jasScanner, applicability.ApplicabilityScannerType, secrets.SecretsScannerType, securityParallelRunnerForTest.AddErrorToChan, utils.GetAllSupportedScans(), nil, "") + + targetResults.NewScaScanResults(jas.FakeBasicXrayResults[0]) + testParams := JasRunnerParams{ + Runner: securityParallelRunnerForTest, + Scanner: jasScanner, + ScanResults: targetResults, + ScansToPreform: utils.GetAllSupportedScans(), + ApplicableScanType: applicability.ApplicabilityScannerType, + SecretsScanType: secrets.SecretsScannerType, + DirectDependencies: &[]string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, + } + err = AddJasScannersTasks(testParams) assert.NoError(t, err) } @@ -52,9 +63,8 @@ func TestJasRunner_AnalyzerManagerReturnsError(t *testing.T) { assert.NoError(t, jas.DownloadAnalyzerManagerIfNeeded(0)) jfrogAppsConfigForTest, _ := jas.CreateJFrogAppsConfig(nil) - scanner, _ := jas.CreateJasScanner(nil, &jas.FakeServerDetails, "", jas.GetAnalyzerManagerXscEnvVars("", false)) - _, err := applicability.RunApplicabilityScan(jas.FakeBasicXrayResults, []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"}, - scanner, false, applicability.ApplicabilityScannerType, jfrogAppsConfigForTest.Modules[0], 0) + scanner, _ := jas.CreateJasScanner(&jas.FakeServerDetails, false, "", jas.GetAnalyzerManagerXscEnvVars("")) + _, err := applicability.RunApplicabilityScan(jas.FakeBasicXrayResults, []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"}, scanner, false, applicability.ApplicabilityScannerType, jfrogAppsConfigForTest.Modules[0], 0) // Expect error: assert.ErrorContains(t, err, "failed to run Applicability scan") } diff --git a/jas/sast/sastscanner.go b/jas/sast/sastscanner.go index e5747179..fd9f083f 100644 --- a/jas/sast/sastscanner.go +++ b/jas/sast/sastscanner.go @@ -5,8 +5,8 @@ import ( "path/filepath" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/jas" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -139,13 +139,6 @@ func getResultLocationStr(result *sarif.Result) string { sarifutils.GetLocationEndColumn(location)) } -func getResultRuleId(result *sarif.Result) string { - if result.RuleID == nil { - return "" - } - return *result.RuleID -} - func getResultId(result *sarif.Result) string { - return getResultRuleId(result) + sarifutils.GetResultLevel(result) + sarifutils.GetResultMsgText(result) + getResultLocationStr(result) + return sarifutils.GetResultRuleId(result) + sarifutils.GetResultLevel(result) + sarifutils.GetResultMsgText(result) + getResultLocationStr(result) } diff --git a/jas/sast/sastscanner_test.go b/jas/sast/sastscanner_test.go index af154e74..ccc2c838 100644 --- a/jas/sast/sastscanner_test.go +++ b/jas/sast/sastscanner_test.go @@ -4,16 +4,18 @@ import ( "path/filepath" "testing" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/jas" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" ) func TestNewSastScanManager(t *testing.T) { - scanner, cleanUp := jas.InitJasTest(t, "currentDir") + scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{"currentDir"}) + assert.NoError(t, err) // Act sastScanManager := newSastScanManager(scanner, "temoDirPath") @@ -21,7 +23,7 @@ func TestNewSastScanManager(t *testing.T) { if assert.NotNil(t, sastScanManager) { assert.NotEmpty(t, sastScanManager.configFileName) assert.NotEmpty(t, sastScanManager.resultsFileName) - assert.NotEmpty(t, sastScanManager.scanner.JFrogAppsConfig.Modules[0].SourceRoot) + assert.NotEmpty(t, jfrogAppsConfigForTest.Modules[0].SourceRoot) assert.Equal(t, &jas.FakeServerDetails, sastScanManager.scanner.ServerDetails) } } @@ -29,14 +31,15 @@ func TestNewSastScanManager(t *testing.T) { func TestSastParseResults_EmptyResults(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) // Arrange sastScanManager := newSastScanManager(scanner, "temoDirPath") sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "no-violations.sarif") // Act - var err error - sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, sastDocsUrlSuffix, scanner.MinSeverity) + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, sastDocsUrlSuffix, scanner.MinSeverity) // Assert if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { @@ -51,13 +54,14 @@ func TestSastParseResults_EmptyResults(t *testing.T) { func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) // Arrange sastScanManager := newSastScanManager(scanner, "temoDirPath") sastScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "contains-sast-violations.sarif") // Act - var err error - sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, sastDocsUrlSuffix, scanner.MinSeverity) + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, sastDocsUrlSuffix, scanner.MinSeverity) // Assert if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { diff --git a/jas/secrets/secretsscanner.go b/jas/secrets/secretsscanner.go index eaa29334..7a0e5290 100644 --- a/jas/secrets/secretsscanner.go +++ b/jas/secrets/secretsscanner.go @@ -7,8 +7,8 @@ import ( clientutils "github.com/jfrog/jfrog-client-go/utils" jfrogappsconfig "github.com/jfrog/jfrog-apps-config/go" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/jas" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/owenrumney/go-sarif/v2/sarif" @@ -127,7 +127,7 @@ func processSecretScanRuns(sarifRuns []*sarif.Run) []*sarif.Run { // Hide discovered secrets value for _, secretResult := range secretRun.Results { for _, location := range secretResult.Locations { - sarifutils.SetLocationSnippet(location, maskSecret(sarifutils.GetLocationSnippet(location))) + sarifutils.SetLocationSnippet(location, maskSecret(sarifutils.GetLocationSnippetText(location))) } } } diff --git a/jas/secrets/secretsscanner_test.go b/jas/secrets/secretsscanner_test.go index 37745a6b..b98c172e 100644 --- a/jas/secrets/secretsscanner_test.go +++ b/jas/secrets/secretsscanner_test.go @@ -67,13 +67,14 @@ func TestRunAnalyzerManager_ReturnsGeneralError(t *testing.T) { func TestParseResults_EmptyResults(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) // Arrange secretScanManager := newSecretsScanManager(scanner, SecretsScannerType, "temoDirPath") secretScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "secrets-scan", "no-secrets.sarif") // Act - var err error - secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, secretsDocsUrlSuffix, scanner.MinSeverity) + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, secretsDocsUrlSuffix, scanner.MinSeverity) // Assert if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { @@ -90,13 +91,14 @@ func TestParseResults_ResultsContainSecrets(t *testing.T) { // Arrange scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) secretScanManager := newSecretsScanManager(scanner, SecretsScannerType, "temoDirPath") secretScanManager.resultsFileName = filepath.Join(jas.GetTestDataPath(), "secrets-scan", "contain-secrets.sarif") // Act - var err error - secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, secretsDocsUrlSuffix, severityutils.Medium) + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.resultsFileName, jfrogAppsConfigForTest.Modules[0].SourceRoot, secretsDocsUrlSuffix, severityutils.Medium) // Assert if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { @@ -113,7 +115,9 @@ func TestParseResults_ResultsContainSecrets(t *testing.T) { func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - scanResults, err := RunSecretsScan(scanner, SecretsScannerType, scanner.JFrogAppsConfig.Modules[0], 0) + jfrogAppsConfigForTest, err := jas.CreateJFrogAppsConfig([]string{}) + assert.NoError(t, err) + scanResults, err := RunSecretsScan(scanner, SecretsScannerType, jfrogAppsConfigForTest.Modules[0], 0) assert.Error(t, err) assert.ErrorContains(t, err, "failed to run Secrets scan") assert.Nil(t, scanResults) diff --git a/jfrogclisecurity_test.go b/jfrogclisecurity_test.go index 94253546..d8be3833 100644 --- a/jfrogclisecurity_test.go +++ b/jfrogclisecurity_test.go @@ -6,6 +6,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/log" + "github.com/jfrog/jfrog-cli-security/cli" "github.com/jfrog/jfrog-cli-security/tests/utils" configTests "github.com/jfrog/jfrog-cli-security/tests" @@ -38,7 +39,7 @@ func setupIntegrationTests() { flag.Parse() log.SetDefaultLogger() // Init - utils.InitTestCliDetails() + utils.InitTestCliDetails(cli.GetJfrogCliSecurityApp()) utils.AuthenticateArtifactory() utils.AuthenticateXsc() utils.CreateRequiredRepositories() diff --git a/scans_test.go b/scans_test.go index 1cba58c1..939a6e67 100644 --- a/scans_test.go +++ b/scans_test.go @@ -12,16 +12,19 @@ import ( "sync" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/validations" + "github.com/jfrog/jfrog-cli-security/cli" "github.com/jfrog/jfrog-cli-security/commands/curation" "github.com/jfrog/jfrog-cli-security/commands/scan" - "github.com/jfrog/jfrog-cli-security/formats" securityTests "github.com/jfrog/jfrog-cli-security/tests" securityTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils" - "github.com/jfrog/jfrog-cli-security/utils/jasutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/container" containerUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container" @@ -43,26 +46,40 @@ import ( func TestXrayBinaryScanJson(t *testing.T) { output := testXrayBinaryScan(t, string(format.Json), false) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func TestXrayBinaryScanSimpleJson(t *testing.T) { output := testXrayBinaryScan(t, string(format.SimpleJson), true) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 1, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + SecurityViolations: 1, + Licenses: 1, + }) } func TestXrayBinaryScanJsonWithProgress(t *testing.T) { callback := commonTests.MockProgressInitialization() defer callback() output := testXrayBinaryScan(t, string(format.Json), false) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func TestXrayBinaryScanSimpleJsonWithProgress(t *testing.T) { callback := commonTests.MockProgressInitialization() defer callback() output := testXrayBinaryScan(t, string(format.SimpleJson), true) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 1, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + SecurityViolations: 1, + Licenses: 1, + }) } func testXrayBinaryScan(t *testing.T, format string, withViolation bool) string { @@ -92,7 +109,10 @@ func TestXrayBinaryScanWithBypassArchiveLimits(t *testing.T) { // Run with bypass flag and expect it to find vulnerabilities scanArgs = append(scanArgs, "--bypass-archive-limits") output := securityTests.PlatformCli.RunCliCmdWithOutput(t, scanArgs...) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } // Docker scan tests @@ -162,9 +182,9 @@ func runDockerScan(t *testing.T, testCli *coreTests.JfrogCli, imageName, watchNa output := testCli.WithoutCredentials().RunCliCmdWithOutput(t, cmdArgs...) if assert.NotEmpty(t, output) { if validateSecrets { - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, minInactives) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{Inactive: minInactives}) } else { - securityTestUtils.VerifyJsonScanResults(t, output, 0, minVulnerabilities, minLicenses) + validations.VerifyJsonResults(t, output, validations.ValidationParams{Vulnerabilities: minVulnerabilities, Licenses: minLicenses}) } } // Run docker scan on image with watch @@ -174,7 +194,7 @@ func runDockerScan(t *testing.T, testCli *coreTests.JfrogCli, imageName, watchNa cmdArgs = append(cmdArgs, "--watches="+watchName) output = testCli.WithoutCredentials().RunCliCmdWithOutput(t, cmdArgs...) if assert.NotEmpty(t, output) { - securityTestUtils.VerifyJsonScanResults(t, output, minViolations, 0, 0) + validations.VerifyJsonResults(t, output, validations.ValidationParams{SecurityViolations: minViolations}) } } } diff --git a/tests/testdata/other/sast-scan/contains-sast-violations.sarif b/tests/testdata/other/sast-scan/contains-sast-violations.sarif index d8b3c02e..f6251a23 100644 --- a/tests/testdata/other/sast-scan/contains-sast-violations.sarif +++ b/tests/testdata/other/sast-scan/contains-sast-violations.sarif @@ -165,12 +165,12 @@ { "executionSuccessful": true, "arguments": [ - "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "/users/user/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", "scan", "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693477603-3697552683/results.sarif" ], "workingDirectory": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat" + "uri": "file:///Users/user/proj" } } ], @@ -193,7 +193,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 28, @@ -216,7 +216,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 32, @@ -239,7 +239,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 44, @@ -262,7 +262,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 13, @@ -285,7 +285,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 31, @@ -308,7 +308,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 31, @@ -331,7 +331,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 10, @@ -354,7 +354,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 39, @@ -377,7 +377,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + "uri": "file:///Users/user/proj/flask_webgoat/__init__.py" }, "region": { "endColumn": 19, @@ -400,7 +400,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + "uri": "file:///Users/user/proj/flask_webgoat/__init__.py" }, "region": { "endColumn": 49, @@ -429,7 +429,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + "uri": "file:///Users/user/proj/flask_webgoat/__init__.py" }, "region": { "endColumn": 49, @@ -463,7 +463,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 28, @@ -486,7 +486,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 32, @@ -509,7 +509,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 44, @@ -532,7 +532,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 13, @@ -555,7 +555,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 31, @@ -578,7 +578,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 31, @@ -601,7 +601,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 10, @@ -624,7 +624,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 39, @@ -647,7 +647,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + "uri": "file:///Users/user/proj/flask_webgoat/__init__.py" }, "region": { "endColumn": 19, @@ -670,7 +670,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + "uri": "file:///Users/user/proj/flask_webgoat/__init__.py" }, "region": { "endColumn": 49, @@ -699,7 +699,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + "uri": "file:///Users/user/proj/flask_webgoat/__init__.py" }, "region": { "endColumn": 49, @@ -733,7 +733,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 23, @@ -756,7 +756,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 27, @@ -779,7 +779,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 34, @@ -802,7 +802,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 8, @@ -825,7 +825,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 29, @@ -854,7 +854,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + "uri": "file:///Users/user/proj/flask_webgoat/auth.py" }, "region": { "endColumn": 29, @@ -883,7 +883,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/flask-webgoat/run.py" + "uri": "file:///Users/user/proj/run.py" }, "region": { "endColumn": 24, diff --git a/tests/testdata/other/sast-scan/no-violations.sarif b/tests/testdata/other/sast-scan/no-violations.sarif index ed129e6e..f9534d60 100644 --- a/tests/testdata/other/sast-scan/no-violations.sarif +++ b/tests/testdata/other/sast-scan/no-violations.sarif @@ -11,12 +11,12 @@ { "executionSuccessful": true, "arguments": [ - "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "/users/user/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", "scan", "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693477603-3697552683/results.sarif" ], "workingDirectory": { - "uri": "file:///Users/assafa/Documents/code/terraform" + "uri": "file:///Users/user/testdata/terraform" } } ], diff --git a/tests/testdata/output/audit/audit_results.json b/tests/testdata/output/audit/audit_results.json new file mode 100644 index 00000000..65811aa2 --- /dev/null +++ b/tests/testdata/output/audit/audit_results.json @@ -0,0 +1,2505 @@ +{ + "xray_version": "3.104.8", + "jas_entitled": true, + "command_type": "source_code", + "multi_scan_id": "7d5e4733-3f93-11ef-8147-e610d09d7daa", + "targets": [ + { + "target": "/Users/user/ejs-frog-demo", + "technology": "npm", + "sca_scans": { + "is_multiple_root_project": false, + "descriptors": [ + "/Users/user/ejs-frog-demo/package.json" + ], + "xray_scan": [ + { + "scan_id": "711851ce-68c4-4dfd-7afb-c29737ebcb96", + "violations": [ + { + "summary": "Prototype pollution attack when using _.zipObjectDeep in lodash before 4.17.20.", + "severity": "High", + "type": "security", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.19]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-114089", + "cves": [ + { + "cve": "CVE-2020-8203", + "cvss_v2_score": "5.8", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:M/Au:N/C:N/I:P/A:P", + "cvss_v3_score": "7.4", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H" + } + ], + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2020-8203", + "https://www.oracle.com/security-alerts/cpuapr2022.html", + "https://hackerone.com/reports/864701", + "https://hackerone.com/reports/712065", + "https://github.com/advisories/GHSA-p6mc-m468-83gw", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://github.com/github/advisory-database/pull/2884", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/c84fe82760fb2d3e03a63379b297a1cc1a2fce12", + "https://security.netapp.com/advisory/ntap-20200724-0006/", + "https://web.archive.org/web/20210914001339/https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://github.com/lodash/lodash/issues/4874", + "https://github.com/lodash/lodash/wiki/Changelog#v41719" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-114089\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "Prototype pollution in lodash object merging and zipping functions leads to code injection.", + "full_description": "[lodash](https://lodash.com/) is a JavaScript library which provides utility functions for common programming tasks.\n\nJavaScript frontend and Node.js-based backend applications that merge or zip objects using the lodash functions `mergeWith`, `merge` and `zipObjectDeep` are vulnerable to [prototype pollution](https://medium.com/node-modules/what-is-prototype-pollution-and-why-is-it-such-a-big-deal-2dd8d89a93c) if one or more of the objects it receives as arguments are obtained from user input. \nAn attacker controlling this input given to the vulnerable functions can inject properties to JavaScript special objects such as [Object.prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) from which all JavaScript objects inherit properties and methods. Any change on `Object.prototype` properties will then propagate through the prototype chain inheritance to all of the objects in a JavaScript application. This in turn would allow an attacker to add new properties or modify existing properties which will have application specific implications that could lead to DoS (denial of service), authentication bypass, privilege escalation and even RCE (remote code execution) in [some cases](https://youtu.be/LUsiFV3dsK8?t=1152). \nAs an example for privilege escalation, consider a JavaScript application that has a `user` object which has a Boolean property of `user.isAdmin` which is used to decide which actions the user may take. If an attacker can modify or add the `isAdmin` property through prototype pollution, it can escalate the privileges of its own user to those of an admin. \nAs exploitation is usually application specific, successful exploitation is much more likely if an attacker have access to the JavaScript application code. As such, frontend applications are more vulnerable to this vulnerability than Node.js backend applications.", + "jfrog_research_severity": "Critical", + "jfrog_research_severity_reasons": [ + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "is_positive": true + }, + { + "name": "The issue can be exploited by attackers over the network" + }, + { + "name": "The issue is trivial to exploit and does not require a published writeup or PoC" + } + ], + "remediation": "##### Deployment mitigations\n\nAs general guidelines against prototype pollution, first consider not merging objects originating from user input or using a Map structure instead of an object. If merging objects is needed, look into creating objects without a prototype with `Object.create(null)` or into freezing `Object.prototype` with `Object.freeze()`. Finally, it is always best to perform input validation with a a [JSON schema validator](https://github.com/ajv-validator/ajv), which could mitigate this issue entirely in many cases." + } + }, + { + "summary": "lodash node module before 4.17.5 suffers from a Modification of Assumed-Immutable Data (MAID) vulnerability via defaultsDeep, merge, and mergeWith functions, which allows a malicious user to modify the prototype of \"Object\" via __proto__, causing the addition or modification of an existing property that will exist on all objects.", + "severity": "Medium", + "type": "security", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.5]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-72918", + "cves": [ + { + "cve": "CVE-2018-3721", + "cvss_v2_score": "4.0", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:S/C:N/I:P/A:N", + "cvss_v3_score": "6.5", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N" + } + ], + "references": [ + "https://www.npmjs.com/advisories/577", + "https://hackerone.com/reports/310443", + "https://github.com/advisories/GHSA-fvqr-27wr-82fm", + "https://nvd.nist.gov/vuln/detail/CVE-2018-3721", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/commit/d8e069cc3410082e44eb18fcf8e7f3d08ebe1d4a" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-72918\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1" + }, + { + "summary": "Express.js minimalist web framework for node. Versions of Express.js prior to 4.19.0 and all pre-release alpha and beta versions of 5.0 are affected by an open redirect vulnerability using malformed URLs. When a user of Express performs a redirect using a user-provided URL Express performs an encode [using `encodeurl`](https://github.com/pillarjs/encodeurl) on the contents before passing it to the `location` header. This can cause malformed URLs to be evaluated in unexpected ways by common redirect allow list implementations in Express applications, leading to an Open Redirect via bypass of a properly implemented allow list. The main method impacted is `res.location()` but this is also called from within `res.redirect()`. The vulnerability is fixed in 4.19.2 and 5.0.0-beta.3.", + "severity": "Medium", + "type": "security", + "components": { + "npm://express:4.18.2": { + "fixed_versions": [ + "[4.19.2]", + "[5.0.0-beta.3]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://express:4.18.2" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-594935", + "cves": [ + { + "cve": "CVE-2024-29041", + "cvss_v3_score": "6.1", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N" + } + ], + "references": [ + "https://github.com/koajs/koa/issues/1800", + "https://github.com/expressjs/express/pull/5539", + "https://github.com/expressjs/express/commit/0b746953c4bd8e377123527db11f9cd866e39f94", + "https://github.com/expressjs/express/commit/0867302ddbde0e9463d0564fea5861feb708c2dd", + "https://github.com/advisories/GHSA-rv95-896h-c2vc", + "https://expressjs.com/en/4x/api.html#res.location", + "https://nvd.nist.gov/vuln/detail/CVE-2024-29041", + "https://github.com/expressjs/express/security/advisories/GHSA-rv95-896h-c2vc" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-594935\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1" + }, + { + "summary": "A prototype pollution vulnerability was found in lodash \u003c4.17.11 where the functions merge, mergeWith, and defaultsDeep can be tricked into adding or modifying properties of Object.prototype.", + "severity": "Medium", + "type": "security", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.11]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-75300", + "cves": [ + { + "cve": "CVE-2018-16487", + "cvss_v2_score": "6.8", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:M/Au:N/C:P/I:P/A:P", + "cvss_v3_score": "5.6", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L" + } + ], + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2018-16487", + "https://www.npmjs.com/advisories/782", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/advisories/GHSA-4xc9-xhrj-v574", + "https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://hackerone.com/reports/380873" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-75300\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "Insufficient input validation in the Lodash library leads to prototype pollution.", + "full_description": "The [Lodash](https://lodash.com/) library is an open-source JavaScript project that simplifies operations on string, arrays, numbers, and other objects. It is widely used in connected devices. \n\nThe `merge`, `mergeWith`, and `defaultsDeep` methods in Lodash are vulnerable to [prototype pollution](https://shieldfy.io/security-wiki/prototype-pollution/introduction-to-prototype-pollution/). Attackers can exploit this vulnerability by specifying a crafted `sources` parameter to any of these methods, which can modify the prototype properties of the `Object`, `Function`, `Array`, `String`, `Number`, and `Boolean` objects. A public [exploit](https://hackerone.com/reports/380873) exists which performs the prototype pollution with an arbitrary key and value.\n\nThe library implementation has a bug in the `safeGet()` function in the `lodash.js` module that allows for adding or modifying `prototype` properties of various objects. The official [solution](https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad) fixes the bug by explicitly forbidding the addition or modification of `prototype` properties.\n\nA related CVE (CVE-2018-3721) covers the same issue prior to Lodash version 4.17.5, but the fix for that was incomplete.", + "jfrog_research_severity": "High", + "jfrog_research_severity_reasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into one of the following methods - \n* `merge` - 2nd argument\n* `mergeWith` - 2nd argument\n* `defaultsDeep` - 2nd argument", + "is_positive": true + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "is_positive": true + }, + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrated exploitation by injecting an attacker controlled key and value into the prototype" + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "summary": "The ejs (aka Embedded JavaScript templates) package 3.1.6 for Node.js allows server-side template injection in settings[view options][outputFunctionName]. This is parsed as an internal option, and overwrites the outputFunctionName option with an arbitrary OS command (which is executed upon template compilation).", + "severity": "Critical", + "type": "security", + "components": { + "npm://ejs:3.1.6": { + "fixed_versions": [ + "[3.1.7]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://ejs:3.1.6" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-209002", + "cves": [ + { + "cve": "CVE-2022-29078", + "cvss_v2_score": "7.5", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P", + "cvss_v3_score": "9.8", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "references": [ + "https://github.com/mde/ejs/commit/15ee698583c98dadc456639d6245580d17a24baf", + "https://eslam.io/posts/ejs-server-side-template-injection-rce/", + "https://security.netapp.com/advisory/ntap-20220804-0001", + "https://github.com/mde/ejs/releases", + "https://nvd.nist.gov/vuln/detail/CVE-2022-29078", + "https://eslam.io/posts/ejs-server-side-template-injection-rce", + "https://github.com/mde/ejs", + "https://security.netapp.com/advisory/ntap-20220804-0001/" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-209002\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "Insufficient input validation in EJS enables attackers to perform template injection when attacker can control the rendering options.", + "full_description": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to perform template injection on the `opts.outputFunctionName` variable, since the variable is injected into the template body without any escaping. Although it is unlikely that the attacker can directly control the `outputFunctionName` property, it is possible that it can be influenced in conjunction with a prototype pollution vulnerability.\n\nOnce template injection is achieved, the attacker can immediately perform remote code execution since the template engine (EJS) allows executing arbitrary JavaScript code.\n\nExample of a vulnerable Node.js application -\n```js\nconst express = require('express');\nconst bodyParser = require('body-parser');\nconst lodash = require('lodash');\nconst ejs = require('ejs');\n\nconst app = express();\n\napp\n .use(bodyParser.urlencoded({extended: true}))\n .use(bodyParser.json());\n\napp.set('views', './');\napp.set('view engine', 'ejs');\n\napp.get(\"/\", (req, res) =\u003e {\n res.render('index');\n});\n\napp.post(\"/\", (req, res) =\u003e {\n let data = {};\n let input = JSON.parse(req.body.content);\n lodash.defaultsDeep(data, input);\n res.json({message: \"OK\"});\n});\n\nlet server = app.listen(8086, '0.0.0.0', function() {\n console.log('Listening on port %d', server.address().port);\n});\n```\n\nExploiting the above example for RCE -\n`curl 127.0.0.1:8086 -v --data 'content={\"constructor\": {\"prototype\": {\"outputFunctionName\": \"a; return global.process.mainModule.constructor._load(\\\"child_process\\\").execSync(\\\"whoami\\\"); //\"}}}'\n`\n\nDue to the prototype pollution in the `lodash.defaultsDeep` call, an attacker can inject the `outputFunctionName` property with an arbitrary value. The chosen value executes an arbitrary process via the `child_process` module.", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The attacker has to find a way to get their malicious input to `opts.outputFunctionName`, which will usually require exploitation of a prototype pollution vulnerability somewhere else in the code. However, there could be cases where the attacker can pass malicious data to the render function directly because of design problems in other code using EJS.", + "is_positive": true + }, + { + "name": "The issue has an exploit published", + "description": "There are multiple examples of exploits for this vulnerability online." + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Successful exploitation of this vulnerability leads to remote code execution." + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.\n\nNote that this mitigation is supposed to stop any prototype pollution attacks which can allow an attacker to control the `opts.outputFunctionName` parameter indirectly.\n\nThe mitigation will not stop any (extremely unlikely) scenarios where the JavaScript code allows external input to directly affect `opts.outputFunctionName`." + } + }, + { + "summary": "The ejs (aka Embedded JavaScript templates) package before 3.1.10 for Node.js lacks certain pollution protection.", + "severity": "Medium", + "type": "security", + "components": { + "npm://ejs:3.1.6": { + "fixed_versions": [ + "[3.1.10]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://ejs:3.1.6" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-599735", + "cves": [ + { + "cve": "CVE-2024-33883", + "cvss_v3_score": "4.0", + "cvss_v3_vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + ], + "references": [ + "https://security.netapp.com/advisory/ntap-20240605-0003/", + "https://security.netapp.com/advisory/ntap-20240605-0003", + "https://github.com/mde/ejs/commit/e469741dca7df2eb400199e1cdb74621e3f89aa5", + "https://github.com/mde/ejs/compare/v3.1.9...v3.1.10", + "https://github.com/advisories/GHSA-ghr5-ch3p-vcr6", + "https://nvd.nist.gov/vuln/detail/CVE-2024-33883" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-599735\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "Insufficient input validation in EJS may lead to prototype pollution.", + "full_description": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as `EJS`, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nA prototype pollution gadget within the EJS template engine could potentially be leveraged by attackers to achieve remote code execution or DoS via prototype pollution.\n\n```\nfunction Template(text, opts) {\n opts = opts || utils.createNullProtoObjWherePossible();\n```\n\nWhen checking for the presence of a property within an object variable, the lookup scope isn't explicitly defined. In JavaScript, the absence of a defined lookup scope prompts a search up to the root prototype (`Object.prototype`). This could potentially be under the control of an attacker if another prototype pollution vulnerability is present within the application.\n\nIf the application server is using the EJS as the backend template engine, and there is another prototype pollution vulnerability in the application, then the attacker could leverage the found gadgets in the EJS template engine to escalate the prototype pollution to remote code execution or DoS.\n\nThe following code will execute a command on the server by polluting `opts.escapeFunction`:\n \n```\nconst express = require('express');\nconst app = express();\nconst port = 8008;\nconst ejs = require('ejs');\n\n// Set EJS as the view engine\napp.set('view engine', 'ejs');\n\napp.get('/', (req, res) =\u003e {\n \n const data = {title: 'Welcome', message: 'Hello'};\n\n // Sample EJS template string\n const templateString = `\u003chtml\u003e\u003chead\u003e\u003ctitle\u003e\u003c%= title %\u003e\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003ch1\u003e\u003c%= message %\u003e\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e`;\n\n const { exec } = require('child_process');\n\n function myFunc() {\n exec('bash -c \"echo 123\"', (error, stdout, stderr) =\u003e {\n if (error) {\n console.error(`exec error: ${error}`);\n return;\n }\n if (stderr){\n console.log(`stderr : ${stderr}`);\n return;\n }\n // Handle success\n console.log(`Command executed successfully. Output: ${stdout}`);\n });\n }\n\n const options = {client:false};\n\n Object.prototype.escapeFunction = myFunc;\n \n const compiledTemplate = ejs.compile(templateString, options);\n const renderedHtml = compiledTemplate(data);\n res.send(renderedHtml);\n});\n\n// Start the server\napp.listen(port, () =\u003e {\n console.log(`Server is running on http://localhost:${port}`);\n});\n```", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "Attackers can only leverage this vulnerability when the application server is using the EJS as the backend template engine. Moreover, there must be a second prototype pollution vulnerability in the application.", + "is_positive": true + }, + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "CVSS does not take into account the unlikely prerequisites necessary for exploitation.", + "is_positive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "A prototype pollution attack allows the attacker to inject new properties into all JavaScript objects.\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable." + } + ] + } + }, + { + "summary": "ejs v3.1.9 is vulnerable to server-side template injection. If the ejs file is controllable, template injection can be implemented through the configuration settings of the closeDelimiter parameter. NOTE: this is disputed by the vendor because the render function is not intended to be used with untrusted input.", + "severity": "Critical", + "type": "security", + "components": { + "npm://ejs:3.1.6": { + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://ejs:3.1.6" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-520200", + "cves": [ + { + "cve": "CVE-2023-29827", + "cvss_v3_score": "9.8", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2023-29827", + "https://github.com/mde/ejs/issues/720", + "https://github.com/mde/ejs/blob/main/SECURITY.md#out-of-scope-vulnerabilities" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-520200\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "Insufficient input validation can lead to template injection in ejs when attackers can control both the rendered template and rendering options.", + "full_description": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to bypass ejs' template injection restrictions, by abusing the `closeDelimiter` rendering option, in the case when -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\n\nThe vulnerability was **rightfully disputed** due to the fact that a vulnerable configuration is extremely unlikely to exist in any real-world setup. As such, the maintainers will not provide a fix for this (non-)issue.\n\nExample of a vulnerable application -\n```js\nconst express = require('express')\nconst app = express()\nconst port = 3000\n\napp.set('view engine', 'ejs');\n\napp.get('/page', (req,res) =\u003e {\n res.render('page', req.query); // OPTS (2nd parameter) IS ATTACKER-CONTROLLED\n})\n\napp.listen(port, () =\u003e {\n console.log(\"Example app listening on port ${port}\")\n})\n```\n\nContents of `page.ejs` (very unlikely to be attacker controlled) -\n```js\n%%1\");process.mainModule.require('child_process').execSync('calc');//\n```\n\nIn this case, sending `closeDelimiter` with the same malicious code that already exists at `page.ejs` will trigger the injection -\n`http://127.0.0.1:3000/page?settings[view%20options][closeDelimiter]=1\")%3bprocess.mainModule.require('child_process').execSync('calc')%3b//`", + "jfrog_research_severity": "Low", + "jfrog_research_severity_reasons": [ + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The CVSS does not take into account the rarity of a vulnerable configuration to exist", + "is_positive": true + }, + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The vulnerability can be exploited only under the following conditions -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\nThis vulnerable configuration is extremely unlikely to exist in any real-world setup.", + "is_positive": true + }, + { + "name": "The issue has been disputed by the vendor", + "is_positive": true + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates template injection" + } + ] + } + }, + { + "summary": "Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload.", + "severity": "Critical", + "type": "security", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.12]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-85679", + "cves": [ + { + "cve": "CVE-2019-10744", + "cvss_v2_score": "6.4", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:N/C:N/I:P/A:P", + "cvss_v3_score": "9.1", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H" + } + ], + "references": [ + "https://www.npmjs.com/advisories/1065", + "https://github.com/lodash/lodash/pull/4336", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://security.netapp.com/advisory/ntap-20191004-0005/", + "https://snyk.io/vuln/SNYK-JS-LODASH-450202", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp%3Butm_medium=RSS", + "https://github.com/advisories/GHSA-jf85-cpcp-j695", + "https://nvd.nist.gov/vuln/detail/CVE-2019-10744" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-85679\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "Insufficient input validation in lodash defaultsDeep() leads to prototype pollution.", + "full_description": "[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, \u0026 extras.\n\nThe function `defaultsDeep` was found to be vulnerable to prototype pollution, when accepting arbitrary source objects from untrusted input\n\nExample of code vulnerable to this issue - \n```js\nconst lodash = require('lodash'); \nconst evilsrc = {constructor: {prototype: {evilkey: \"evilvalue\"}}};\nlodash.defaultsDeep({}, evilsrc)\n```", + "jfrog_research_severity": "High", + "jfrog_research_severity_reasons": [ + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrates exploitation of this issue" + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "is_positive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into the `defaultsDeep` method (2nd arg)", + "is_positive": true + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "summary": "lodash prior to 4.17.11 is affected by: CWE-400: Uncontrolled Resource Consumption. The impact is: Denial of service. The component is: Date handler. The attack vector is: Attacker provides very long strings, which the library attempts to match using a regular expression. The fixed version is: 4.17.11.", + "severity": "Medium", + "type": "security", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.11]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-85049", + "cves": [ + { + "cve": "CVE-2019-1010266", + "cvss_v2_score": "4.0", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:S/C:N/I:N/A:P", + "cvss_v3_score": "6.5", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H" + } + ], + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2019-1010266", + "https://github.com/lodash/lodash/wiki/Changelog", + "https://snyk.io/vuln/SNYK-JS-LODASH-73639", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/issues/3359", + "https://github.com/lodash/lodash/commit/5c08f18d365b64063bfbfa686cbb97cdd6267347" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-85049\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1" + }, + { + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions.", + "severity": "Medium", + "type": "security", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.21]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-140562", + "cves": [ + { + "cve": "CVE-2020-28500", + "cvss_v2_score": "5.0", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:N/C:N/I:N/A:P", + "cvss_v3_score": "5.3", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + ], + "references": [ + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074894", + "https://github.com/lodash/lodash/blob/npm/trimEnd.js%23L8", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074893", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074892", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28500", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074895", + "https://github.com/lodash/lodash/pull/5065/commits/02906b8191d3c100c193fe6f7b27d1c40f200bb7", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/advisories/GHSA-29mw-wpgm-hmr9", + "https://github.com/lodash/lodash/pull/5065", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074896", + "https://snyk.io/vuln/SNYK-JS-LODASH-1018905" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-140562\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "ReDoS in lodash could lead to a denial of service when handling untrusted strings.", + "full_description": "JavaScript-based applications that use [lodash](https://github.com/lodash/lodash) and specifically the [_.toNumber](https://lodash.com/docs/4.17.15#toNumber), [_.trim](https://lodash.com/docs/4.17.15#trim) and [_.trimEnd](https://lodash.com/docs/4.17.15#trimEnd) functions, could be vulnerable to DoS (Denial of Service) through a faulty regular expression that introduces a ReDoS (Regular Expression DoS) vulnerability. This vulnerability is only triggered if untrusted user input flows into these vulnerable functions and the attacker can supply arbitrary long strings (over 50kB) that contain whitespaces. \n\nOn a modern Core i7-based system, calling the vulnerable functions with a 50kB string could take between 2 to 3 seconds to execute and 4.5 minutes for a longer 500kB string. The fix improved the regular expression performance so it took only a few milliseconds on the same Core i7-based system. This vulnerability is easily exploitable as all is required is to build a string that triggers it as can be seen in this PoC reproducing code - \n\n```js\nvar untrusted_user_input_50k = \"a\" + ' '.repeat(50000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_50k); // should take a few seconds to run\nvar untrusted_user_input_500k = \"a\" + ' '.repeat(500000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_500k); // should take a few minutes to run\n```", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The issue has an exploit published", + "description": "Public exploit demonstrated ReDoS" + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "Exploitation depends on parsing user input by the `.toNumber`, `.trim` or `.trimEnd` `lodash` functions, and requires the input to contain whitespaces and be very long (over 50KB)", + "is_positive": true + } + ], + "remediation": "##### Deployment mitigations\n\nTrim untrusted strings based on size before providing it to the vulnerable functions by using the `substring` function to with a fixed maximum size like so - ```js untrusted_user_input.substring(0, max_string_size_less_than_50kB); ```" + } + }, + { + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.", + "severity": "High", + "type": "security", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.21]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "watch_name": "Security_watch_1", + "issue_id": "XRAY-140575", + "cves": [ + { + "cve": "CVE-2021-23337", + "cvss_v2_score": "6.5", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:S/C:P/I:P/A:P", + "cvss_v3_score": "7.2", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H" + } + ], + "references": [ + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074929", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JS-LODASH-1040724", + "https://security.netapp.com/advisory/ntap-20210312-0006", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c", + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074928", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074932", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js%23L14851", + "https://github.com/advisories/GHSA-35jh-r3h4-6jhm", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074930", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074931", + "https://nvd.nist.gov/vuln/detail/CVE-2021-23337", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L14851" + ], + "ignore_url": "https://platform.jfrog.io/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=711851ce-68c4-4dfd-7afb-c29737ebcb96\u0026issue_id=XRAY-140575\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=Security_watch_1", + "extended_information": { + "short_description": "Improper sanitization in the lodash template function leads to JavaScript code injection through the options argument.", + "full_description": "JavaScript-based applications (both frontend and backend) that use the [template function](https://lodash.com/docs/4.17.15#template) -`_.template([string=''], [options={}])` from the [lodash](https://lodash.com/) utility library and provide the `options` argument (specifically the `variable` option) from untrusted user input, are vulnerable to JavaScript code injection. This issue can be easily exploited, and an exploitation example is [publicly available](https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c#diff-a561630bb56b82342bc66697aee2ad96efddcbc9d150665abd6fb7ecb7c0ab2fR22303) in the fix tests that was introduced in version 4.17.21 - \n```js\nlodash.template('', { variable: '){console.log(process.env)}; with(obj' })()\n```", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "It is highly unlikely that a JS program will accept arbitrary remote input into the template's `options` argument", + "is_positive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The attacker must find remote input that propagates into the `options` argument of a `template` call", + "is_positive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Leads to remote code execution through JS code injection" + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates arbitrary JS code execution" + } + ] + } + } + ], + "vulnerabilities": [ + { + "cves": [ + { + "cve": "CVE-2024-39249" + } + ], + "summary": "Async \u003c= 2.6.4 and \u003c= 3.2.5 are vulnerable to ReDoS (Regular Expression Denial of Service) while parsing function in autoinject function. NOTE: this is disputed by the supplier because there is no realistic threat model: regular expressions are not used with untrusted input.", + "severity": "Unknown", + "components": { + "npm://async:3.2.4": { + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://ejs:3.1.6" + }, + { + "component_id": "npm://jake:10.8.7" + }, + { + "component_id": "npm://async:3.2.4" + } + ] + ] + } + }, + "issue_id": "XRAY-609848", + "references": [ + "https://github.com/zunak/CVE-2024-39249", + "https://github.com/caolan/async/blob/v3.2.5/lib/autoInject.js#L41", + "https://nvd.nist.gov/vuln/detail/CVE-2024-39249", + "https://github.com/caolan/async/blob/v3.2.5/lib/autoInject.js#L6", + "https://github.com/caolan/async/issues/1975#issuecomment-2204528153", + "https://github.com/zunak/CVE-2024-39249/issues/1" + ], + "extended_information": { + "short_description": "ReDoS in Async may lead to denial of service while parsing malformed source code.", + "jfrog_research_severity": "Low", + "jfrog_research_severity_reasons": [ + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The reported CVSS does not reflect the severity of the vulnerability.", + "is_positive": true + }, + { + "name": "The issue cannot result in a severe impact (such as remote code execution)", + "description": "To exploit this issue an attacker must change the source code of the application. In cases where an attacker can already modify (or fully control) the source code, the attacker can immediately achieve arbitrary code execution - thus this issue has almost no security impact.", + "is_positive": true + }, + { + "name": "The issue has an exploit published", + "description": "A proof-of-concept has been published in the advisory." + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The issue requires the use of the `async.autoInject` function to be vulnerable.", + "is_positive": true + } + ] + } + }, + { + "cves": [ + { + "cve": "CVE-2020-8203", + "cvss_v2_score": "5.8", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:M/Au:N/C:N/I:P/A:P", + "cvss_v3_score": "7.4", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H", + "cwe": [ + "CWE-770", + "CWE-1321" + ], + "cwe_details": { + "CWE-1321": { + "name": "Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')", + "description": "The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype." + }, + "CWE-770": { + "name": "Allocation of Resources Without Limits or Throttling", + "description": "The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor." + } + } + } + ], + "summary": "Prototype pollution attack when using _.zipObjectDeep in lodash before 4.17.20.", + "severity": "High", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.19]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "issue_id": "XRAY-114089", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2020-8203", + "https://www.oracle.com/security-alerts/cpuapr2022.html", + "https://hackerone.com/reports/864701", + "https://hackerone.com/reports/712065", + "https://github.com/advisories/GHSA-p6mc-m468-83gw", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://github.com/github/advisory-database/pull/2884", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/c84fe82760fb2d3e03a63379b297a1cc1a2fce12", + "https://security.netapp.com/advisory/ntap-20200724-0006/", + "https://web.archive.org/web/20210914001339/https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://github.com/lodash/lodash/issues/4874", + "https://github.com/lodash/lodash/wiki/Changelog#v41719" + ], + "extended_information": { + "short_description": "Prototype pollution in lodash object merging and zipping functions leads to code injection.", + "full_description": "[lodash](https://lodash.com/) is a JavaScript library which provides utility functions for common programming tasks.\n\nJavaScript frontend and Node.js-based backend applications that merge or zip objects using the lodash functions `mergeWith`, `merge` and `zipObjectDeep` are vulnerable to [prototype pollution](https://medium.com/node-modules/what-is-prototype-pollution-and-why-is-it-such-a-big-deal-2dd8d89a93c) if one or more of the objects it receives as arguments are obtained from user input. \nAn attacker controlling this input given to the vulnerable functions can inject properties to JavaScript special objects such as [Object.prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) from which all JavaScript objects inherit properties and methods. Any change on `Object.prototype` properties will then propagate through the prototype chain inheritance to all of the objects in a JavaScript application. This in turn would allow an attacker to add new properties or modify existing properties which will have application specific implications that could lead to DoS (denial of service), authentication bypass, privilege escalation and even RCE (remote code execution) in [some cases](https://youtu.be/LUsiFV3dsK8?t=1152). \nAs an example for privilege escalation, consider a JavaScript application that has a `user` object which has a Boolean property of `user.isAdmin` which is used to decide which actions the user may take. If an attacker can modify or add the `isAdmin` property through prototype pollution, it can escalate the privileges of its own user to those of an admin. \nAs exploitation is usually application specific, successful exploitation is much more likely if an attacker have access to the JavaScript application code. As such, frontend applications are more vulnerable to this vulnerability than Node.js backend applications.", + "jfrog_research_severity": "Critical", + "jfrog_research_severity_reasons": [ + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "is_positive": true + }, + { + "name": "The issue can be exploited by attackers over the network" + }, + { + "name": "The issue is trivial to exploit and does not require a published writeup or PoC" + } + ], + "remediation": "##### Deployment mitigations\n\nAs general guidelines against prototype pollution, first consider not merging objects originating from user input or using a Map structure instead of an object. If merging objects is needed, look into creating objects without a prototype with `Object.create(null)` or into freezing `Object.prototype` with `Object.freeze()`. Finally, it is always best to perform input validation with a a [JSON schema validator](https://github.com/ajv-validator/ajv), which could mitigate this issue entirely in many cases." + } + }, + { + "cves": [ + { + "cve": "CVE-2019-10744", + "cvss_v2_score": "6.4", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:N/C:N/I:P/A:P", + "cvss_v3_score": "9.1", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H", + "cwe": [ + "CWE-1321", + "CWE-20" + ], + "cwe_details": { + "CWE-1321": { + "name": "Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')", + "description": "The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype." + }, + "CWE-20": { + "name": "Improper Input Validation", + "description": "The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.", + "categories": [ + { + "category": "2023 CWE Top 25", + "rank": "6" + } + ] + } + } + } + ], + "summary": "Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload.", + "severity": "Critical", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.12]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "issue_id": "XRAY-85679", + "references": [ + "https://www.npmjs.com/advisories/1065", + "https://github.com/lodash/lodash/pull/4336", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://security.netapp.com/advisory/ntap-20191004-0005/", + "https://snyk.io/vuln/SNYK-JS-LODASH-450202", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp%3Butm_medium=RSS", + "https://github.com/advisories/GHSA-jf85-cpcp-j695", + "https://nvd.nist.gov/vuln/detail/CVE-2019-10744" + ], + "extended_information": { + "short_description": "Insufficient input validation in lodash defaultsDeep() leads to prototype pollution.", + "full_description": "[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, \u0026 extras.\n\nThe function `defaultsDeep` was found to be vulnerable to prototype pollution, when accepting arbitrary source objects from untrusted input\n\nExample of code vulnerable to this issue - \n```js\nconst lodash = require('lodash'); \nconst evilsrc = {constructor: {prototype: {evilkey: \"evilvalue\"}}};\nlodash.defaultsDeep({}, evilsrc)\n```", + "jfrog_research_severity": "High", + "jfrog_research_severity_reasons": [ + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrates exploitation of this issue" + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "is_positive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into the `defaultsDeep` method (2nd arg)", + "is_positive": true + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "cves": [ + { + "cve": "CVE-2019-1010266", + "cvss_v2_score": "4.0", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:S/C:N/I:N/A:P", + "cvss_v3_score": "6.5", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H", + "cwe": [ + "CWE-400", + "CWE-770" + ], + "cwe_details": { + "CWE-400": { + "name": "Uncontrolled Resource Consumption", + "description": "The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources." + }, + "CWE-770": { + "name": "Allocation of Resources Without Limits or Throttling", + "description": "The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor." + } + } + } + ], + "summary": "lodash prior to 4.17.11 is affected by: CWE-400: Uncontrolled Resource Consumption. The impact is: Denial of service. The component is: Date handler. The attack vector is: Attacker provides very long strings, which the library attempts to match using a regular expression. The fixed version is: 4.17.11.", + "severity": "Medium", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.11]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "issue_id": "XRAY-85049", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2019-1010266", + "https://github.com/lodash/lodash/wiki/Changelog", + "https://snyk.io/vuln/SNYK-JS-LODASH-73639", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/issues/3359", + "https://github.com/lodash/lodash/commit/5c08f18d365b64063bfbfa686cbb97cdd6267347" + ] + }, + { + "cves": [ + { + "cve": "CVE-2020-28500", + "cvss_v2_score": "5.0", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:N/C:N/I:N/A:P", + "cvss_v3_score": "5.3", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "cwe": [ + "CWE-400", + "CWE-1333", + "NVD-CWE-Other" + ], + "cwe_details": { + "CWE-1333": { + "name": "Inefficient Regular Expression Complexity", + "description": "The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles." + }, + "CWE-400": { + "name": "Uncontrolled Resource Consumption", + "description": "The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources." + } + } + } + ], + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions.", + "severity": "Medium", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.21]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "issue_id": "XRAY-140562", + "references": [ + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074894", + "https://github.com/lodash/lodash/blob/npm/trimEnd.js%23L8", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074893", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074892", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28500", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074895", + "https://github.com/lodash/lodash/pull/5065/commits/02906b8191d3c100c193fe6f7b27d1c40f200bb7", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/advisories/GHSA-29mw-wpgm-hmr9", + "https://github.com/lodash/lodash/pull/5065", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074896", + "https://snyk.io/vuln/SNYK-JS-LODASH-1018905" + ], + "extended_information": { + "short_description": "ReDoS in lodash could lead to a denial of service when handling untrusted strings.", + "full_description": "JavaScript-based applications that use [lodash](https://github.com/lodash/lodash) and specifically the [_.toNumber](https://lodash.com/docs/4.17.15#toNumber), [_.trim](https://lodash.com/docs/4.17.15#trim) and [_.trimEnd](https://lodash.com/docs/4.17.15#trimEnd) functions, could be vulnerable to DoS (Denial of Service) through a faulty regular expression that introduces a ReDoS (Regular Expression DoS) vulnerability. This vulnerability is only triggered if untrusted user input flows into these vulnerable functions and the attacker can supply arbitrary long strings (over 50kB) that contain whitespaces. \n\nOn a modern Core i7-based system, calling the vulnerable functions with a 50kB string could take between 2 to 3 seconds to execute and 4.5 minutes for a longer 500kB string. The fix improved the regular expression performance so it took only a few milliseconds on the same Core i7-based system. This vulnerability is easily exploitable as all is required is to build a string that triggers it as can be seen in this PoC reproducing code - \n\n```js\nvar untrusted_user_input_50k = \"a\" + ' '.repeat(50000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_50k); // should take a few seconds to run\nvar untrusted_user_input_500k = \"a\" + ' '.repeat(500000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_500k); // should take a few minutes to run\n```", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The issue has an exploit published", + "description": "Public exploit demonstrated ReDoS" + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "Exploitation depends on parsing user input by the `.toNumber`, `.trim` or `.trimEnd` `lodash` functions, and requires the input to contain whitespaces and be very long (over 50KB)", + "is_positive": true + } + ], + "remediation": "##### Deployment mitigations\n\nTrim untrusted strings based on size before providing it to the vulnerable functions by using the `substring` function to with a fixed maximum size like so - ```js untrusted_user_input.substring(0, max_string_size_less_than_50kB); ```" + } + }, + { + "cves": [ + { + "cve": "CVE-2018-3721", + "cvss_v2_score": "4.0", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:S/C:N/I:P/A:N", + "cvss_v3_score": "6.5", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N", + "cwe": [ + "CWE-1321", + "CWE-471" + ], + "cwe_details": { + "CWE-1321": { + "name": "Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')", + "description": "The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype." + }, + "CWE-471": { + "name": "Modification of Assumed-Immutable Data (MAID)", + "description": "The product does not properly protect an assumed-immutable element from being modified by an attacker." + } + } + } + ], + "summary": "lodash node module before 4.17.5 suffers from a Modification of Assumed-Immutable Data (MAID) vulnerability via defaultsDeep, merge, and mergeWith functions, which allows a malicious user to modify the prototype of \"Object\" via __proto__, causing the addition or modification of an existing property that will exist on all objects.", + "severity": "Medium", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.5]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "issue_id": "XRAY-72918", + "references": [ + "https://www.npmjs.com/advisories/577", + "https://hackerone.com/reports/310443", + "https://github.com/advisories/GHSA-fvqr-27wr-82fm", + "https://nvd.nist.gov/vuln/detail/CVE-2018-3721", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/commit/d8e069cc3410082e44eb18fcf8e7f3d08ebe1d4a" + ] + }, + { + "cves": [ + { + "cve": "CVE-2021-23337", + "cvss_v2_score": "6.5", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:S/C:P/I:P/A:P", + "cvss_v3_score": "7.2", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "cwe": [ + "CWE-77", + "CWE-94" + ], + "cwe_details": { + "CWE-77": { + "name": "Improper Neutralization of Special Elements used in a Command ('Command Injection')", + "description": "The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.", + "categories": [ + { + "category": "2023 CWE Top 25", + "rank": "16" + } + ] + }, + "CWE-94": { + "name": "Improper Control of Generation of Code ('Code Injection')", + "description": "The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.", + "categories": [ + { + "category": "2023 CWE Top 25", + "rank": "23" + } + ] + } + } + } + ], + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.", + "severity": "High", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.21]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "issue_id": "XRAY-140575", + "references": [ + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074929", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JS-LODASH-1040724", + "https://security.netapp.com/advisory/ntap-20210312-0006", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c", + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074928", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074932", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js%23L14851", + "https://github.com/advisories/GHSA-35jh-r3h4-6jhm", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074930", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074931", + "https://nvd.nist.gov/vuln/detail/CVE-2021-23337", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L14851" + ], + "extended_information": { + "short_description": "Improper sanitization in the lodash template function leads to JavaScript code injection through the options argument.", + "full_description": "JavaScript-based applications (both frontend and backend) that use the [template function](https://lodash.com/docs/4.17.15#template) -`_.template([string=''], [options={}])` from the [lodash](https://lodash.com/) utility library and provide the `options` argument (specifically the `variable` option) from untrusted user input, are vulnerable to JavaScript code injection. This issue can be easily exploited, and an exploitation example is [publicly available](https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c#diff-a561630bb56b82342bc66697aee2ad96efddcbc9d150665abd6fb7ecb7c0ab2fR22303) in the fix tests that was introduced in version 4.17.21 - \n```js\nlodash.template('', { variable: '){console.log(process.env)}; with(obj' })()\n```", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "It is highly unlikely that a JS program will accept arbitrary remote input into the template's `options` argument", + "is_positive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The attacker must find remote input that propagates into the `options` argument of a `template` call", + "is_positive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Leads to remote code execution through JS code injection" + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates arbitrary JS code execution" + } + ] + } + }, + { + "cves": [ + { + "cve": "CVE-2018-16487", + "cvss_v2_score": "6.8", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:M/Au:N/C:P/I:P/A:P", + "cvss_v3_score": "5.6", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "cwe": [ + "CWE-400", + "NVD-CWE-noinfo" + ], + "cwe_details": { + "CWE-400": { + "name": "Uncontrolled Resource Consumption", + "description": "The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources." + } + } + } + ], + "summary": "A prototype pollution vulnerability was found in lodash \u003c4.17.11 where the functions merge, mergeWith, and defaultsDeep can be tricked into adding or modifying properties of Object.prototype.", + "severity": "Medium", + "components": { + "npm://lodash:4.17.0": { + "fixed_versions": [ + "[4.17.11]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://lodash:4.17.0" + } + ] + ] + } + }, + "issue_id": "XRAY-75300", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2018-16487", + "https://www.npmjs.com/advisories/782", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/advisories/GHSA-4xc9-xhrj-v574", + "https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://hackerone.com/reports/380873" + ], + "extended_information": { + "short_description": "Insufficient input validation in the Lodash library leads to prototype pollution.", + "full_description": "The [Lodash](https://lodash.com/) library is an open-source JavaScript project that simplifies operations on string, arrays, numbers, and other objects. It is widely used in connected devices. \n\nThe `merge`, `mergeWith`, and `defaultsDeep` methods in Lodash are vulnerable to [prototype pollution](https://shieldfy.io/security-wiki/prototype-pollution/introduction-to-prototype-pollution/). Attackers can exploit this vulnerability by specifying a crafted `sources` parameter to any of these methods, which can modify the prototype properties of the `Object`, `Function`, `Array`, `String`, `Number`, and `Boolean` objects. A public [exploit](https://hackerone.com/reports/380873) exists which performs the prototype pollution with an arbitrary key and value.\n\nThe library implementation has a bug in the `safeGet()` function in the `lodash.js` module that allows for adding or modifying `prototype` properties of various objects. The official [solution](https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad) fixes the bug by explicitly forbidding the addition or modification of `prototype` properties.\n\nA related CVE (CVE-2018-3721) covers the same issue prior to Lodash version 4.17.5, but the fix for that was incomplete.", + "jfrog_research_severity": "High", + "jfrog_research_severity_reasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into one of the following methods - \n* `merge` - 2nd argument\n* `mergeWith` - 2nd argument\n* `defaultsDeep` - 2nd argument", + "is_positive": true + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "is_positive": true + }, + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrated exploitation by injecting an attacker controlled key and value into the prototype" + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "cves": [ + { + "cve": "CVE-2024-29041", + "cvss_v3_score": "6.1", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "cwe": [ + "CWE-601", + "CWE-1286" + ], + "cwe_details": { + "CWE-1286": { + "name": "Improper Validation of Syntactic Correctness of Input", + "description": "The product receives input that is expected to be well-formed - i.e., to comply with a certain syntax - but it does not validate or incorrectly validates that the input complies with the syntax." + }, + "CWE-601": { + "name": "URL Redirection to Untrusted Site ('Open Redirect')", + "description": "A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks." + } + } + } + ], + "summary": "Express.js minimalist web framework for node. Versions of Express.js prior to 4.19.0 and all pre-release alpha and beta versions of 5.0 are affected by an open redirect vulnerability using malformed URLs. When a user of Express performs a redirect using a user-provided URL Express performs an encode [using `encodeurl`](https://github.com/pillarjs/encodeurl) on the contents before passing it to the `location` header. This can cause malformed URLs to be evaluated in unexpected ways by common redirect allow list implementations in Express applications, leading to an Open Redirect via bypass of a properly implemented allow list. The main method impacted is `res.location()` but this is also called from within `res.redirect()`. The vulnerability is fixed in 4.19.2 and 5.0.0-beta.3.", + "severity": "Medium", + "components": { + "npm://express:4.18.2": { + "fixed_versions": [ + "[4.19.2]", + "[5.0.0-beta.3]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://express:4.18.2" + } + ] + ] + } + }, + "issue_id": "XRAY-594935", + "references": [ + "https://github.com/koajs/koa/issues/1800", + "https://github.com/expressjs/express/pull/5539", + "https://github.com/expressjs/express/commit/0b746953c4bd8e377123527db11f9cd866e39f94", + "https://github.com/expressjs/express/commit/0867302ddbde0e9463d0564fea5861feb708c2dd", + "https://github.com/advisories/GHSA-rv95-896h-c2vc", + "https://expressjs.com/en/4x/api.html#res.location", + "https://nvd.nist.gov/vuln/detail/CVE-2024-29041", + "https://github.com/expressjs/express/security/advisories/GHSA-rv95-896h-c2vc" + ] + }, + { + "cves": [ + { + "cve": "CVE-2022-29078", + "cvss_v2_score": "7.5", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P", + "cvss_v3_score": "9.8", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "cwe": [ + "CWE-94", + "CWE-74" + ], + "cwe_details": { + "CWE-74": { + "name": "Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')", + "description": "The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component." + }, + "CWE-94": { + "name": "Improper Control of Generation of Code ('Code Injection')", + "description": "The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.", + "categories": [ + { + "category": "2023 CWE Top 25", + "rank": "23" + } + ] + } + } + } + ], + "summary": "The ejs (aka Embedded JavaScript templates) package 3.1.6 for Node.js allows server-side template injection in settings[view options][outputFunctionName]. This is parsed as an internal option, and overwrites the outputFunctionName option with an arbitrary OS command (which is executed upon template compilation).", + "severity": "Critical", + "components": { + "npm://ejs:3.1.6": { + "fixed_versions": [ + "[3.1.7]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://ejs:3.1.6" + } + ] + ] + } + }, + "issue_id": "XRAY-209002", + "references": [ + "https://github.com/mde/ejs/commit/15ee698583c98dadc456639d6245580d17a24baf", + "https://eslam.io/posts/ejs-server-side-template-injection-rce/", + "https://security.netapp.com/advisory/ntap-20220804-0001", + "https://github.com/mde/ejs/releases", + "https://nvd.nist.gov/vuln/detail/CVE-2022-29078", + "https://eslam.io/posts/ejs-server-side-template-injection-rce", + "https://github.com/mde/ejs", + "https://security.netapp.com/advisory/ntap-20220804-0001/" + ], + "extended_information": { + "short_description": "Insufficient input validation in EJS enables attackers to perform template injection when attacker can control the rendering options.", + "full_description": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to perform template injection on the `opts.outputFunctionName` variable, since the variable is injected into the template body without any escaping. Although it is unlikely that the attacker can directly control the `outputFunctionName` property, it is possible that it can be influenced in conjunction with a prototype pollution vulnerability.\n\nOnce template injection is achieved, the attacker can immediately perform remote code execution since the template engine (EJS) allows executing arbitrary JavaScript code.\n\nExample of a vulnerable Node.js application -\n```js\nconst express = require('express');\nconst bodyParser = require('body-parser');\nconst lodash = require('lodash');\nconst ejs = require('ejs');\n\nconst app = express();\n\napp\n .use(bodyParser.urlencoded({extended: true}))\n .use(bodyParser.json());\n\napp.set('views', './');\napp.set('view engine', 'ejs');\n\napp.get(\"/\", (req, res) =\u003e {\n res.render('index');\n});\n\napp.post(\"/\", (req, res) =\u003e {\n let data = {};\n let input = JSON.parse(req.body.content);\n lodash.defaultsDeep(data, input);\n res.json({message: \"OK\"});\n});\n\nlet server = app.listen(8086, '0.0.0.0', function() {\n console.log('Listening on port %d', server.address().port);\n});\n```\n\nExploiting the above example for RCE -\n`curl 127.0.0.1:8086 -v --data 'content={\"constructor\": {\"prototype\": {\"outputFunctionName\": \"a; return global.process.mainModule.constructor._load(\\\"child_process\\\").execSync(\\\"whoami\\\"); //\"}}}'\n`\n\nDue to the prototype pollution in the `lodash.defaultsDeep` call, an attacker can inject the `outputFunctionName` property with an arbitrary value. The chosen value executes an arbitrary process via the `child_process` module.", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The attacker has to find a way to get their malicious input to `opts.outputFunctionName`, which will usually require exploitation of a prototype pollution vulnerability somewhere else in the code. However, there could be cases where the attacker can pass malicious data to the render function directly because of design problems in other code using EJS.", + "is_positive": true + }, + { + "name": "The issue has an exploit published", + "description": "There are multiple examples of exploits for this vulnerability online." + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Successful exploitation of this vulnerability leads to remote code execution." + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.\n\nNote that this mitigation is supposed to stop any prototype pollution attacks which can allow an attacker to control the `opts.outputFunctionName` parameter indirectly.\n\nThe mitigation will not stop any (extremely unlikely) scenarios where the JavaScript code allows external input to directly affect `opts.outputFunctionName`." + } + }, + { + "cves": [ + { + "cve": "CVE-2024-33883", + "cvss_v3_score": "4.0", + "cvss_v3_vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "cwe": [ + "CWE-1321", + "CWE-693" + ], + "cwe_details": { + "CWE-1321": { + "name": "Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')", + "description": "The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype." + }, + "CWE-693": { + "name": "Protection Mechanism Failure", + "description": "The product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product." + } + } + } + ], + "summary": "The ejs (aka Embedded JavaScript templates) package before 3.1.10 for Node.js lacks certain pollution protection.", + "severity": "Medium", + "components": { + "npm://ejs:3.1.6": { + "fixed_versions": [ + "[3.1.10]" + ], + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://ejs:3.1.6" + } + ] + ] + } + }, + "issue_id": "XRAY-599735", + "references": [ + "https://security.netapp.com/advisory/ntap-20240605-0003/", + "https://security.netapp.com/advisory/ntap-20240605-0003", + "https://github.com/mde/ejs/commit/e469741dca7df2eb400199e1cdb74621e3f89aa5", + "https://github.com/mde/ejs/compare/v3.1.9...v3.1.10", + "https://github.com/advisories/GHSA-ghr5-ch3p-vcr6", + "https://nvd.nist.gov/vuln/detail/CVE-2024-33883" + ], + "extended_information": { + "short_description": "Insufficient input validation in EJS may lead to prototype pollution.", + "full_description": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as `EJS`, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nA prototype pollution gadget within the EJS template engine could potentially be leveraged by attackers to achieve remote code execution or DoS via prototype pollution.\n\n```\nfunction Template(text, opts) {\n opts = opts || utils.createNullProtoObjWherePossible();\n```\n\nWhen checking for the presence of a property within an object variable, the lookup scope isn't explicitly defined. In JavaScript, the absence of a defined lookup scope prompts a search up to the root prototype (`Object.prototype`). This could potentially be under the control of an attacker if another prototype pollution vulnerability is present within the application.\n\nIf the application server is using the EJS as the backend template engine, and there is another prototype pollution vulnerability in the application, then the attacker could leverage the found gadgets in the EJS template engine to escalate the prototype pollution to remote code execution or DoS.\n\nThe following code will execute a command on the server by polluting `opts.escapeFunction`:\n \n```\nconst express = require('express');\nconst app = express();\nconst port = 8008;\nconst ejs = require('ejs');\n\n// Set EJS as the view engine\napp.set('view engine', 'ejs');\n\napp.get('/', (req, res) =\u003e {\n \n const data = {title: 'Welcome', message: 'Hello'};\n\n // Sample EJS template string\n const templateString = `\u003chtml\u003e\u003chead\u003e\u003ctitle\u003e\u003c%= title %\u003e\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003ch1\u003e\u003c%= message %\u003e\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e`;\n\n const { exec } = require('child_process');\n\n function myFunc() {\n exec('bash -c \"echo 123\"', (error, stdout, stderr) =\u003e {\n if (error) {\n console.error(`exec error: ${error}`);\n return;\n }\n if (stderr){\n console.log(`stderr : ${stderr}`);\n return;\n }\n // Handle success\n console.log(`Command executed successfully. Output: ${stdout}`);\n });\n }\n\n const options = {client:false};\n\n Object.prototype.escapeFunction = myFunc;\n \n const compiledTemplate = ejs.compile(templateString, options);\n const renderedHtml = compiledTemplate(data);\n res.send(renderedHtml);\n});\n\n// Start the server\napp.listen(port, () =\u003e {\n console.log(`Server is running on http://localhost:${port}`);\n});\n```", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "Attackers can only leverage this vulnerability when the application server is using the EJS as the backend template engine. Moreover, there must be a second prototype pollution vulnerability in the application.", + "is_positive": true + }, + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "CVSS does not take into account the unlikely prerequisites necessary for exploitation.", + "is_positive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "A prototype pollution attack allows the attacker to inject new properties into all JavaScript objects.\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable." + } + ] + } + }, + { + "cves": [ + { + "cve": "CVE-2023-29827", + "cvss_v3_score": "9.8", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "cwe": [ + "CWE-74" + ], + "cwe_details": { + "CWE-74": { + "name": "Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')", + "description": "The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component." + } + } + } + ], + "summary": "ejs v3.1.9 is vulnerable to server-side template injection. If the ejs file is controllable, template injection can be implemented through the configuration settings of the closeDelimiter parameter. NOTE: this is disputed by the vendor because the render function is not intended to be used with untrusted input.", + "severity": "Critical", + "components": { + "npm://ejs:3.1.6": { + "impact_paths": [ + [ + { + "component_id": "npm://froghome:1.0.0" + }, + { + "component_id": "npm://ejs:3.1.6" + } + ] + ] + } + }, + "issue_id": "XRAY-520200", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2023-29827", + "https://github.com/mde/ejs/issues/720", + "https://github.com/mde/ejs/blob/main/SECURITY.md#out-of-scope-vulnerabilities" + ], + "extended_information": { + "short_description": "Insufficient input validation can lead to template injection in ejs when attackers can control both the rendered template and rendering options.", + "full_description": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to bypass ejs' template injection restrictions, by abusing the `closeDelimiter` rendering option, in the case when -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\n\nThe vulnerability was **rightfully disputed** due to the fact that a vulnerable configuration is extremely unlikely to exist in any real-world setup. As such, the maintainers will not provide a fix for this (non-)issue.\n\nExample of a vulnerable application -\n```js\nconst express = require('express')\nconst app = express()\nconst port = 3000\n\napp.set('view engine', 'ejs');\n\napp.get('/page', (req,res) =\u003e {\n res.render('page', req.query); // OPTS (2nd parameter) IS ATTACKER-CONTROLLED\n})\n\napp.listen(port, () =\u003e {\n console.log(\"Example app listening on port ${port}\")\n})\n```\n\nContents of `page.ejs` (very unlikely to be attacker controlled) -\n```js\n%%1\");process.mainModule.require('child_process').execSync('calc');//\n```\n\nIn this case, sending `closeDelimiter` with the same malicious code that already exists at `page.ejs` will trigger the injection -\n`http://127.0.0.1:3000/page?settings[view%20options][closeDelimiter]=1\")%3bprocess.mainModule.require('child_process').execSync('calc')%3b//`", + "jfrog_research_severity": "Low", + "jfrog_research_severity_reasons": [ + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The CVSS does not take into account the rarity of a vulnerable configuration to exist", + "is_positive": true + }, + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The vulnerability can be exploited only under the following conditions -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\nThis vulnerable configuration is extremely unlikely to exist in any real-world setup.", + "is_positive": true + }, + { + "name": "The issue has been disputed by the vendor", + "is_positive": true + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates template injection" + } + ] + } + } + ], + "component_id": "root", + "package_type": "generic", + "status": "completed" + } + ] + }, + "jas_scans": { + "contextual_analysis": [ + { + "tool": { + "driver": { + "informationUri": "https://jfrog.com/help/r/jfrog-security-documentation/jfrog-advanced-security", + "name": "JFrog Applicability Scanner", + "rules": [ + { + "id": "applic_CVE-2018-16487", + "name": "CVE-2018-16487", + "shortDescription": { + "text": "Scanner for CVE-2018-16487" + }, + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.merge` with external input to its 2nd (`sources`) argument.\n* `lodash.mergeWith` with external input to its 2nd (`sources`) argument.\n* `lodash.defaultsDeep` with external input to its 2nd (`sources`) argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present.", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.merge` with external input to its 2nd (`sources`) argument.\n* `lodash.mergeWith` with external input to its 2nd (`sources`) argument.\n* `lodash.defaultsDeep` with external input to its 2nd (`sources`) argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2019-10744", + "name": "CVE-2019-10744", + "shortDescription": { + "text": "Scanner for CVE-2019-10744" + }, + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `defaultsDeep` is called with external input to its 2nd (`sources`) argument, and the `Object.freeze()` remediation is not present.", + "markdown": "The scanner checks whether the vulnerable function `defaultsDeep` is called with external input to its 2nd (`sources`) argument, and the `Object.freeze()` remediation is not present." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2020-28500", + "name": "CVE-2020-28500", + "shortDescription": { + "text": "Scanner for CVE-2020-28500" + }, + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.trim` with external input to its 1st (`string`) argument.\n* `lodash.toNumber` with external input to its 1st (`value`) argument.\n* `lodash.trimEnd` with external input to its 1st (`string`) argument.", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.trim` with external input to its 1st (`string`) argument.\n* `lodash.toNumber` with external input to its 1st (`value`) argument.\n* `lodash.trimEnd` with external input to its 1st (`string`) argument." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2020-8203", + "name": "CVE-2020-8203", + "shortDescription": { + "text": "Scanner for CVE-2020-8203" + }, + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `zipObjectDeep` is called with external input to its 1st (`props`) and 2nd (`values`) arguments, and the `Object.freeze()` remediation is not present.", + "markdown": "The scanner checks whether the vulnerable function `zipObjectDeep` is called with external input to its 1st (`props`) and 2nd (`values`) arguments, and the `Object.freeze()` remediation is not present." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2021-23337", + "name": "CVE-2021-23337", + "shortDescription": { + "text": "Scanner for CVE-2021-23337" + }, + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `lodash.template` is called with external input to its 2nd (`options`) argument.", + "markdown": "The scanner checks whether the vulnerable function `lodash.template` is called with external input to its 2nd (`options`) argument." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2022-29078", + "name": "CVE-2022-29078", + "shortDescription": { + "text": "Scanner for CVE-2022-29078" + }, + "fullDescription": { + "text": "The scanner checks for two vulnerable flows:\n\n1. Whether the `express.set` function is called with the arguments: `view engine` and `ejs`, or external input and if it's followed by a call to the vulnerable function `render` with an unknown second argument.\n\n2. Whether the `renderFile` function is called with an unknown second argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present.", + "markdown": "The scanner checks for two vulnerable flows:\n\n1. Whether the `express.set` function is called with the arguments: `view engine` and `ejs`, or external input and if it's followed by a call to the vulnerable function `render` with an unknown second argument.\n\n2. Whether the `renderFile` function is called with an unknown second argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2024-33883", + "name": "CVE-2024-33883", + "shortDescription": { + "text": "Scanner for CVE-2024-33883" + }, + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `ejs.compile()` is called.", + "markdown": "The scanner checks whether the vulnerable function `ejs.compile()` is called." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2023-29827", + "name": "CVE-2023-29827", + "shortDescription": { + "text": "Scanner for CVE-2023-29827" + }, + "fullDescription": { + "text": "The scanner checks whether any of the following conditions are met:\n\n1. The `ejs.renderFile` function is called with an unknown third argument.\n\n2. The `ejs.compile` function is called with an unknown second argument.\n\n3. The `express.set` function is called with any of the following arguments:\n\n* `express.set(\"view engine\", \"ejs\")`\n* `express.set(\"view engine\", {USER_INPUT})`\n* `express.set({USER_INPUT}, \"ejs\")`\n* `express.set({USER_INPUT}, {USER_INPUT})`", + "markdown": "The scanner checks whether any of the following conditions are met:\n\n1. The `ejs.renderFile` function is called with an unknown third argument.\n\n2. The `ejs.compile` function is called with an unknown second argument.\n\n3. The `express.set` function is called with any of the following arguments:\n\n* `express.set(\"view engine\", \"ejs\")`\n* `express.set(\"view engine\", {USER_INPUT})`\n* `express.set({USER_INPUT}, \"ejs\")`\n* `express.set({USER_INPUT}, {USER_INPUT})`" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2018-3721", + "name": "CVE-2018-3721", + "shortDescription": { + "text": "Scanner for uncovered CVE-2018-3721" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "not_covered" + } + }, + { + "id": "applic_CVE-2019-1010266", + "name": "CVE-2019-1010266", + "shortDescription": { + "text": "Scanner for uncovered CVE-2019-1010266" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "not_covered" + } + }, + { + "id": "applic_CVE-2024-39249", + "name": "CVE-2024-39249", + "shortDescription": { + "text": "Scanner for uncovered CVE-2024-39249" + }, + "fullDescription": { + "text": "Never applicable. The vulnerability is exploitable only if an attacker has access to the source code.", + "markdown": "Never applicable. The vulnerability is exploitable only if an attacker has access to the source code." + }, + "properties": { + "applicability": "not_covered", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2024-29041", + "name": "CVE-2024-29041", + "shortDescription": { + "text": "Scanner for uncovered CVE-2024-29041" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "not_covered" + } + }, + { + "id": "applic_CVE-2024-39249", + "name": "CVE-2024-39249", + "shortDescription": { + "text": "Scanner for indirect dependency CVE-2024-39249" + }, + "fullDescription": { + "text": "Never applicable. The vulnerability is exploitable only if an attacker has access to the source code.", + "markdown": "Never applicable. The vulnerability is exploitable only if an attacker has access to the source code." + }, + "properties": { + "applicability": "not_applicable" + } + } + ], + "version": "1.0" + } + }, + "invocations": [ + { + "arguments": [ + "analyzerManager/jas_scanner/jas_scanner", + "scan", + "/Applicability_1725803037/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [ + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2018-16487", + "message": { + "text": "Prototype pollution `Object.freeze` remediation was detected" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": { + "text": "Object.freeze(Object.prototype)" + } + } + } + } + ] + }, + { + "ruleId": "applic_CVE-2018-16487", + "kind": "pass", + "message": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.merge` with external input to its 2nd (`sources`) argument.\n* `lodash.mergeWith` with external input to its 2nd (`sources`) argument.\n* `lodash.defaultsDeep` with external input to its 2nd (`sources`) argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present." + } + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2019-10744", + "message": { + "text": "Prototype pollution `Object.freeze` remediation was detected" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": { + "text": "Object.freeze(Object.prototype)" + } + } + } + } + ] + }, + { + "ruleId": "applic_CVE-2019-10744", + "kind": "pass", + "message": { + "text": "The scanner checks whether the vulnerable function `defaultsDeep` is called with external input to its 2nd (`sources`) argument, and the `Object.freeze()` remediation is not present." + } + }, + { + "ruleId": "applic_CVE-2020-28500", + "kind": "pass", + "message": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.trim` with external input to its 1st (`string`) argument.\n* `lodash.toNumber` with external input to its 1st (`value`) argument.\n* `lodash.trimEnd` with external input to its 1st (`string`) argument." + } + }, + { + "ruleId": "applic_CVE-2020-8203", + "kind": "pass", + "message": { + "text": "The scanner checks whether the vulnerable function `zipObjectDeep` is called with external input to its 1st (`props`) and 2nd (`values`) arguments, and the `Object.freeze()` remediation is not present." + } + }, + { + "ruleId": "applic_CVE-2021-23337", + "kind": "pass", + "message": { + "text": "The scanner checks whether the vulnerable function `lodash.template` is called with external input to its 2nd (`options`) argument." + } + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2022-29078", + "message": { + "text": "Prototype pollution `Object.freeze` remediation was detected" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": { + "text": "Object.freeze(Object.prototype)" + } + } + } + } + ] + }, + { + "ruleId": "applic_CVE-2022-29078", + "kind": "pass", + "message": { + "text": "The scanner checks for two vulnerable flows:\n\n1. Whether the `express.set` function is called with the arguments: `view engine` and `ejs`, or external input and if it's followed by a call to the vulnerable function `render` with an unknown second argument.\n\n2. Whether the `renderFile` function is called with an unknown second argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present." + } + }, + { + "ruleId": "applic_CVE-2024-33883", + "kind": "pass", + "message": { + "text": "The scanner checks whether the vulnerable function `ejs.compile()` is called." + } + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2023-29827", + "message": { + "text": "The vulnerable functionality is triggered since express.set is called with 'view engine' as the first argument and 'ejs' as the second argument or both arguments with external input" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 14, + "startColumn": 1, + "endLine": 14, + "endColumn": 30, + "snippet": { + "text": "app.set('view engine', 'ejs')" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2023-29827", + "message": { + "text": "The vulnerable function render is called" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 26, + "startColumn": 3, + "endLine": 26, + "endColumn": 38, + "snippet": { + "text": "res.render('pages/index',req.query)" + } + } + } + } + ] + }, + { + "ruleId": "applic_CVE-2024-39249", + "kind": "pass", + "message": { + "text": "Never applicable. The vulnerability is exploitable only if an attacker has access to the source code." + } + } + ] + } + ], + "secrets": [ + { + "tool": { + "driver": { + "informationUri": "https://jfrog.com/help/r/jfrog-security-documentation/jfrog-advanced-security", + "name": "JFrog Secrets scanner", + "rules": [ + { + "id": "REQ.SECRET.GENERIC.TEXT", + "name": "REQ.SECRET.GENERIC.TEXT", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.TEXT" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + }, + { + "id": "REQ.SECRET.GENERIC.CODE", + "name": "REQ.SECRET.GENERIC.CODE", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.CODE" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + }, + { + "id": "REQ.SECRET.KEYS", + "name": "REQ.SECRET.KEYS", + "shortDescription": { + "text": "Scanner for REQ.SECRET.KEYS" + }, + "fullDescription": { + "text": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\u0026tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n", + "markdown": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\u0026tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "REQ.CRED.PUBLIC-ONLY", + "name": "REQ.CRED.PUBLIC-ONLY", + "shortDescription": { + "text": "Scanner for REQ.CRED.PUBLIC-ONLY" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "undetermined", + "conclusion": "private" + } + }, + { + "id": "REQ.SECRET.GENERIC.URL-TEXT", + "name": "REQ.SECRET.GENERIC.URL-TEXT", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.URL-TEXT" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + } + ], + "version": "1.0" + } + }, + "invocations": [ + { + "arguments": [ + "analyzerManager/jas_scanner/jas_scanner", + "scan", + "/Secrets_1725803029/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [ + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/fake-creds.txt" + }, + "region": { + "startLine": 2, + "startColumn": 1, + "endLine": 2, + "endColumn": 11, + "snippet": { + "text": "Sqc************" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/fake-creds.txt" + }, + "region": { + "startLine": 3, + "startColumn": 1, + "endLine": 3, + "endColumn": 11, + "snippet": { + "text": "gho************" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 11, + "startColumn": 14, + "endLine": 11, + "endColumn": 24, + "snippet": { + "text": "Sqc************" + } + } + } + } + ] + } + ] + } + ], + "iac": [ + { + "tool": { + "driver": { + "informationUri": "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/infrastructure-as-code-iac", + "name": "JFrog Terraform scanner", + "rules": [], + "version": "1.8.14" + } + }, + "invocations": [ + { + "arguments": [ + "analyzerManager/iac_scanner/tf_scanner", + "scan", + "/IaC_1725803037/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [] + } + ], + "sast": [ + { + "tool": { + "driver": { + "informationUri": "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/sast", + "name": "USAF", + "rules": [ + { + "id": "js-express-without-helmet", + "shortDescription": { + "text": "Express Not Using Helmet" + }, + "fullDescription": { + "text": "\n### Overview\nHelmet library should be used when using Express in order to properly configure\nHTTP header settings to mitigate a range of well-known vulnerabilities.\n\n### Remediation\n```javascript\nconst helmet = require(\"helmet\");\nconst app = express()\n\napp.use(helmet())\n```\n\n### References\n[Best practices for Express](https://expressjs.com/en/advanced/best-practice-security.html)\n", + "markdown": "\n### Overview\nHelmet library should be used when using Express in order to properly configure\nHTTP header settings to mitigate a range of well-known vulnerabilities.\n\n### Remediation\n```javascript\nconst helmet = require(\"helmet\");\nconst app = express()\n\napp.use(helmet())\n```\n\n### References\n[Best practices for Express](https://expressjs.com/en/advanced/best-practice-security.html)\n" + }, + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "693" + } + } + }, + "properties": { + "security-severity": "3.9" + } + }, + { + "id": "js-insecure-random", + "shortDescription": { + "text": "Use of Insecure Random" + }, + "fullDescription": { + "text": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n", + "markdown": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n" + }, + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "338" + } + } + }, + "properties": { + "security-severity": "3.9" + } + }, + { + "id": "js-template-injection", + "shortDescription": { + "text": "Template Object Injection" + }, + "fullDescription": { + "text": "\n### Overview\nTemplate Object Injection (TOI) is a vulnerability that can occur in\nweb applications that use template engines to render dynamic content.\nTemplate engines are commonly used to generate HTML pages, emails, or\nother types of documents that include variable data. TOI happens when\nuntrusted user input is included as part of the template rendering\nprocess, and the template engine evaluates the input as a code\nexpression, leading to potential code injection or data tampering\nattacks. To prevent TOI vulnerabilities, it's important to sanitize and\nvalidate all user input that is used as part of the template rendering\nprocess.\n\n### Query operation\nIn this query we look for user inputs that flow directly to a\nrequest render.\n\n### Vulnerable example\n```javascript\nvar app = require('express')();\napp.set('view engine', 'hbs');\n\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n res.render('template', bodyParameter);\n});\n```\nIn this example, a user-provided data is injected directly into the\n`render` command, leading to potential code injection or data\ntampering attacks.\n\n### Remediation\n```diff\n+ const sanitizeHtml = require('sanitize-html');\nvar app = require('express')();\napp.set('view engine', 'hbs');\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n\n- res.render('template', bodyParameter);\n+ res.render('template', sanitizeHtml(bodyParameter));\n});\nUsing `sanitize-html`, the user-provided data is sanitized, before\nrendering to the response.\n```\n", + "markdown": "\n### Overview\nTemplate Object Injection (TOI) is a vulnerability that can occur in\nweb applications that use template engines to render dynamic content.\nTemplate engines are commonly used to generate HTML pages, emails, or\nother types of documents that include variable data. TOI happens when\nuntrusted user input is included as part of the template rendering\nprocess, and the template engine evaluates the input as a code\nexpression, leading to potential code injection or data tampering\nattacks. To prevent TOI vulnerabilities, it's important to sanitize and\nvalidate all user input that is used as part of the template rendering\nprocess.\n\n### Query operation\nIn this query we look for user inputs that flow directly to a\nrequest render.\n\n### Vulnerable example\n```javascript\nvar app = require('express')();\napp.set('view engine', 'hbs');\n\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n res.render('template', bodyParameter);\n});\n```\nIn this example, a user-provided data is injected directly into the\n`render` command, leading to potential code injection or data\ntampering attacks.\n\n### Remediation\n```diff\n+ const sanitizeHtml = require('sanitize-html');\nvar app = require('express')();\napp.set('view engine', 'hbs');\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n\n- res.render('template', bodyParameter);\n+ res.render('template', sanitizeHtml(bodyParameter));\n});\nUsing `sanitize-html`, the user-provided data is sanitized, before\nrendering to the response.\n```\n" + }, + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "73" + } + } + }, + "properties": { + "security-severity": "8.9" + } + } + ], + "version": "1.8.14" + } + }, + "invocations": [ + { + "arguments": [ + "analyzerManager/zd_scanner/scanner", + "scan", + "/Sast_1725803040/results.sarif", + "/Sast_1725803040/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [ + { + "ruleId": "js-template-injection", + "level": "error", + "message": { + "text": "Template Object Injection" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 37, + "snippet": { + "text": "req.query" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "a549106dc43cdc0d36b0f81d0465a5d2" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 21, + "startColumn": 23, + "endLine": 21, + "endColumn": 26, + "snippet": { + "text": "req" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 31, + "snippet": { + "text": "req" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 37, + "snippet": { + "text": "req.query" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + } + ] + } + ] + } + ] + }, + { + "ruleId": "js-insecure-random", + "level": "note", + "message": { + "text": "Use of Insecure Random" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/public/js/bootstrap.js" + }, + "region": { + "startLine": 136, + "startColumn": 22, + "endLine": 136, + "endColumn": 35, + "snippet": { + "text": "Math.random()" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "public.js.bootstrap.^_0.Util.getUID" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "34331455b0d5edf4c232dd288225780e" + } + }, + { + "ruleId": "js-insecure-random", + "level": "note", + "message": { + "text": "Use of Insecure Random" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/public/js/bootstrap.bundle.js" + }, + "region": { + "startLine": 135, + "startColumn": 22, + "endLine": 135, + "endColumn": 35, + "snippet": { + "text": "Math.random()" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "public.js.bootstrap\u003cdot\u003ebundle.^_0.Util.getUID" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "281a027677521fa64de4ce1fe14e01ab" + } + }, + { + "ruleId": "js-express-without-helmet", + "level": "note", + "message": { + "text": "Express Not Using Helmet" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/user/ejs-frog-demo/server.js" + }, + "region": { + "startLine": 8, + "startColumn": 11, + "endLine": 8, + "endColumn": 20, + "snippet": { + "text": "express()" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "f8caf6a43a2c1eb41369843ca3c7d94c" + } + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/testdata/output/audit/audit_sarif.json b/tests/testdata/output/audit/audit_sarif.json new file mode 100644 index 00000000..107ecbdf --- /dev/null +++ b/tests/testdata/output/audit/audit_sarif.json @@ -0,0 +1,1212 @@ +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "informationUri": "https://jfrog.com/help/r/jfrog-security-documentation/jfrog-advanced-security", + "name": "JFrog Secrets scanner", + "rules": [ + { + "id": "REQ.SECRET.GENERIC.TEXT", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.TEXT" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "help": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + }, + { + "id": "REQ.SECRET.GENERIC.CODE", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.CODE" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "help": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + }, + { + "id": "REQ.SECRET.KEYS", + "shortDescription": { + "text": "Scanner for REQ.SECRET.KEYS" + }, + "fullDescription": { + "text": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n", + "markdown": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + "help": { + "text": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n", + "markdown": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "REQ.CRED.PUBLIC-ONLY", + "shortDescription": { + "text": "Scanner for REQ.CRED.PUBLIC-ONLY" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "help": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "undetermined", + "conclusion": "private" + } + }, + { + "id": "REQ.SECRET.GENERIC.URL-TEXT", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.URL-TEXT" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "help": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + } + ], + "version": "1.0" + } + }, + "invocations": [ + { + "arguments": [ + "analyzerManager/jas_scanner/jas_scanner", + "scan", + "/Secrets_1725867313/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [ + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "fake-creds.txt" + }, + "region": { + "startLine": 2, + "startColumn": 1, + "endLine": 2, + "endColumn": 11, + "snippet": { + "text": "Sqc************" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "fake-creds.txt" + }, + "region": { + "startLine": 3, + "startColumn": 1, + "endLine": 3, + "endColumn": 11, + "snippet": { + "text": "gho************" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "server.js" + }, + "region": { + "startLine": 11, + "startColumn": 14, + "endLine": 11, + "endColumn": 24, + "snippet": { + "text": "Sqc************" + } + } + } + } + ] + } + ] + }, + { + "tool": { + "driver": { + "informationUri": "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/infrastructure-as-code-iac", + "name": "JFrog Terraform scanner", + "rules": [], + "version": "1.8.14" + } + }, + "invocations": [ + { + "arguments": [ + "analyzerManager/iac_scanner/tf_scanner", + "scan", + "/IaC_1725867328/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [] + }, + { + "tool": { + "driver": { + "informationUri": "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/sast", + "name": "USAF", + "rules": [ + { + "id": "js-express-without-helmet", + "shortDescription": { + "text": "Express Not Using Helmet" + }, + "fullDescription": { + "text": "\n### Overview\nHelmet library should be used when using Express in order to properly configure\nHTTP header settings to mitigate a range of well-known vulnerabilities.\n\n### Remediation\n```javascript\nconst helmet = require(\"helmet\");\nconst app = express()\n\napp.use(helmet())\n```\n\n### References\n[Best practices for Express](https://expressjs.com/en/advanced/best-practice-security.html)\n", + "markdown": "\n### Overview\nHelmet library should be used when using Express in order to properly configure\nHTTP header settings to mitigate a range of well-known vulnerabilities.\n\n### Remediation\n```javascript\nconst helmet = require(\"helmet\");\nconst app = express()\n\napp.use(helmet())\n```\n\n### References\n[Best practices for Express](https://expressjs.com/en/advanced/best-practice-security.html)\n" + }, + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "693" + } + } + }, + "help": { + "text": "\n### Overview\nHelmet library should be used when using Express in order to properly configure\nHTTP header settings to mitigate a range of well-known vulnerabilities.\n\n### Remediation\n```javascript\nconst helmet = require(\"helmet\");\nconst app = express()\n\napp.use(helmet())\n```\n\n### References\n[Best practices for Express](https://expressjs.com/en/advanced/best-practice-security.html)\n", + "markdown": "\n### Overview\nHelmet library should be used when using Express in order to properly configure\nHTTP header settings to mitigate a range of well-known vulnerabilities.\n\n### Remediation\n```javascript\nconst helmet = require(\"helmet\");\nconst app = express()\n\napp.use(helmet())\n```\n\n### References\n[Best practices for Express](https://expressjs.com/en/advanced/best-practice-security.html)\n" + }, + "properties": { + "security-severity": "3.9" + } + }, + { + "id": "js-insecure-random", + "shortDescription": { + "text": "Use of Insecure Random" + }, + "fullDescription": { + "text": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n", + "markdown": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n" + }, + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "338" + } + } + }, + "help": { + "text": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n", + "markdown": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n" + }, + "properties": { + "security-severity": "3.9" + } + }, + { + "id": "js-template-injection", + "shortDescription": { + "text": "Template Object Injection" + }, + "fullDescription": { + "text": "\n### Overview\nTemplate Object Injection (TOI) is a vulnerability that can occur in\nweb applications that use template engines to render dynamic content.\nTemplate engines are commonly used to generate HTML pages, emails, or\nother types of documents that include variable data. TOI happens when\nuntrusted user input is included as part of the template rendering\nprocess, and the template engine evaluates the input as a code\nexpression, leading to potential code injection or data tampering\nattacks. To prevent TOI vulnerabilities, it's important to sanitize and\nvalidate all user input that is used as part of the template rendering\nprocess.\n\n### Query operation\nIn this query we look for user inputs that flow directly to a\nrequest render.\n\n### Vulnerable example\n```javascript\nvar app = require('express')();\napp.set('view engine', 'hbs');\n\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n res.render('template', bodyParameter);\n});\n```\nIn this example, a user-provided data is injected directly into the\n`render` command, leading to potential code injection or data\ntampering attacks.\n\n### Remediation\n```diff\n+ const sanitizeHtml = require('sanitize-html');\nvar app = require('express')();\napp.set('view engine', 'hbs');\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n\n- res.render('template', bodyParameter);\n+ res.render('template', sanitizeHtml(bodyParameter));\n});\nUsing `sanitize-html`, the user-provided data is sanitized, before\nrendering to the response.\n```\n", + "markdown": "\n### Overview\nTemplate Object Injection (TOI) is a vulnerability that can occur in\nweb applications that use template engines to render dynamic content.\nTemplate engines are commonly used to generate HTML pages, emails, or\nother types of documents that include variable data. TOI happens when\nuntrusted user input is included as part of the template rendering\nprocess, and the template engine evaluates the input as a code\nexpression, leading to potential code injection or data tampering\nattacks. To prevent TOI vulnerabilities, it's important to sanitize and\nvalidate all user input that is used as part of the template rendering\nprocess.\n\n### Query operation\nIn this query we look for user inputs that flow directly to a\nrequest render.\n\n### Vulnerable example\n```javascript\nvar app = require('express')();\napp.set('view engine', 'hbs');\n\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n res.render('template', bodyParameter);\n});\n```\nIn this example, a user-provided data is injected directly into the\n`render` command, leading to potential code injection or data\ntampering attacks.\n\n### Remediation\n```diff\n+ const sanitizeHtml = require('sanitize-html');\nvar app = require('express')();\napp.set('view engine', 'hbs');\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n\n- res.render('template', bodyParameter);\n+ res.render('template', sanitizeHtml(bodyParameter));\n});\nUsing `sanitize-html`, the user-provided data is sanitized, before\nrendering to the response.\n```\n" + }, + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "73" + } + } + }, + "help": { + "text": "\n### Overview\nTemplate Object Injection (TOI) is a vulnerability that can occur in\nweb applications that use template engines to render dynamic content.\nTemplate engines are commonly used to generate HTML pages, emails, or\nother types of documents that include variable data. TOI happens when\nuntrusted user input is included as part of the template rendering\nprocess, and the template engine evaluates the input as a code\nexpression, leading to potential code injection or data tampering\nattacks. To prevent TOI vulnerabilities, it's important to sanitize and\nvalidate all user input that is used as part of the template rendering\nprocess.\n\n### Query operation\nIn this query we look for user inputs that flow directly to a\nrequest render.\n\n### Vulnerable example\n```javascript\nvar app = require('express')();\napp.set('view engine', 'hbs');\n\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n res.render('template', bodyParameter);\n});\n```\nIn this example, a user-provided data is injected directly into the\n`render` command, leading to potential code injection or data\ntampering attacks.\n\n### Remediation\n```diff\n+ const sanitizeHtml = require('sanitize-html');\nvar app = require('express')();\napp.set('view engine', 'hbs');\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n\n- res.render('template', bodyParameter);\n+ res.render('template', sanitizeHtml(bodyParameter));\n});\nUsing `sanitize-html`, the user-provided data is sanitized, before\nrendering to the response.\n```\n", + "markdown": "\n### Overview\nTemplate Object Injection (TOI) is a vulnerability that can occur in\nweb applications that use template engines to render dynamic content.\nTemplate engines are commonly used to generate HTML pages, emails, or\nother types of documents that include variable data. TOI happens when\nuntrusted user input is included as part of the template rendering\nprocess, and the template engine evaluates the input as a code\nexpression, leading to potential code injection or data tampering\nattacks. To prevent TOI vulnerabilities, it's important to sanitize and\nvalidate all user input that is used as part of the template rendering\nprocess.\n\n### Query operation\nIn this query we look for user inputs that flow directly to a\nrequest render.\n\n### Vulnerable example\n```javascript\nvar app = require('express')();\napp.set('view engine', 'hbs');\n\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n res.render('template', bodyParameter);\n});\n```\nIn this example, a user-provided data is injected directly into the\n`render` command, leading to potential code injection or data\ntampering attacks.\n\n### Remediation\n```diff\n+ const sanitizeHtml = require('sanitize-html');\nvar app = require('express')();\napp.set('view engine', 'hbs');\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n\n- res.render('template', bodyParameter);\n+ res.render('template', sanitizeHtml(bodyParameter));\n});\nUsing `sanitize-html`, the user-provided data is sanitized, before\nrendering to the response.\n```\n" + }, + "properties": { + "security-severity": "8.9" + } + } + ], + "version": "1.8.14" + } + }, + "invocations": [ + { + "arguments": [ + "analyzerManager/zd_scanner/scanner", + "scan", + "/Sast_1725867332/results.sarif", + "/Sast_1725867332/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [ + { + "ruleId": "js-insecure-random", + "level": "note", + "message": { + "text": "Use of Insecure Random" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "public/js/bootstrap.js" + }, + "region": { + "startLine": 136, + "startColumn": 22, + "endLine": 136, + "endColumn": 35, + "snippet": { + "text": "Math.random()" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "public.js.bootstrap.^_0.Util.getUID" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "3cb8327f723c9d1b6664949748868899" + } + }, + { + "ruleId": "js-insecure-random", + "level": "note", + "message": { + "text": "Use of Insecure Random" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "public/js/bootstrap.bundle.js" + }, + "region": { + "startLine": 135, + "startColumn": 22, + "endLine": 135, + "endColumn": 35, + "snippet": { + "text": "Math.random()" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "public.js.bootstrapbundle.^_0.Util.getUID" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "ec68d229b6bdd85b67dd2ddce27337bd" + } + }, + { + "ruleId": "js-express-without-helmet", + "level": "note", + "message": { + "text": "Express Not Using Helmet" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "server.js" + }, + "region": { + "startLine": 8, + "startColumn": 11, + "endLine": 8, + "endColumn": 20, + "snippet": { + "text": "express()" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "f8caf6a43a2c1eb41369843ca3c7d94c" + } + }, + { + "ruleId": "js-template-injection", + "level": "error", + "message": { + "text": "Template Object Injection" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "server.js" + }, + "region": { + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 37, + "snippet": { + "text": "req.query" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + ], + "fingerprints": { + "precise_sink_and_sink_function": "a549106dc43cdc0d36b0f81d0465a5d2" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "server.js" + }, + "region": { + "startLine": 21, + "startColumn": 23, + "endLine": 21, + "endColumn": 26, + "snippet": { + "text": "req" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "server.js" + }, + "region": { + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 31, + "snippet": { + "text": "req" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "server.js" + }, + "region": { + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 37, + "snippet": { + "text": "req.query" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "server.^_4" + } + ] + } + } + ] + } + ] + } + ] + } + ] + }, + { + "tool": { + "driver": { + "informationUri": "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/sca", + "name": "JFrog Xray Scanner", + "rules": [ + { + "id": "CVE-2018-3721_lodash_4.17.0", + "shortDescription": { + "text": "[CVE-2018-3721] lodash 4.17.0" + }, + "help": { + "text": "lodash node module before 4.17.5 suffers from a Modification of Assumed-Immutable Data (MAID) vulnerability via defaultsDeep, merge, and mergeWith functions, which allows a malicious user to modify the prototype of \"Object\" via __proto__, causing the addition or modification of an existing property that will exist on all objects.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 6.5 | Not Covered | `lodash 4.17.0` | [4.17.5] |" + }, + "properties": { + "security-severity": "6.5" + } + }, + { + "id": "CVE-2021-23337_lodash_4.17.0", + "shortDescription": { + "text": "[CVE-2021-23337] lodash 4.17.0" + }, + "help": { + "text": "Lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.2 | Not Applicable | `lodash 4.17.0` | [4.17.21] |" + }, + "properties": { + "security-severity": "7.2" + } + }, + { + "id": "CVE-2019-1010266_lodash_4.17.0", + "shortDescription": { + "text": "[CVE-2019-1010266] lodash 4.17.0" + }, + "help": { + "text": "lodash prior to 4.17.11 is affected by: CWE-400: Uncontrolled Resource Consumption. The impact is: Denial of service. The component is: Date handler. The attack vector is: Attacker provides very long strings, which the library attempts to match using a regular expression. The fixed version is: 4.17.11.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 6.5 | Not Covered | `lodash 4.17.0` | [4.17.11] |" + }, + "properties": { + "security-severity": "6.5" + } + }, + { + "id": "CVE-2024-33883_ejs_3.1.6", + "shortDescription": { + "text": "[CVE-2024-33883] ejs 3.1.6" + }, + "help": { + "text": "The ejs (aka Embedded JavaScript templates) package before 3.1.10 for Node.js lacks certain pollution protection.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 4.0 | Not Applicable | `ejs 3.1.6` | [3.1.10] |" + }, + "properties": { + "security-severity": "4.0" + } + }, + { + "id": "CVE-2023-29827_ejs_3.1.6", + "shortDescription": { + "text": "[CVE-2023-29827] ejs 3.1.6" + }, + "help": { + "text": "ejs v3.1.9 is vulnerable to server-side template injection. If the ejs file is controllable, template injection can be implemented through the configuration settings of the closeDelimiter parameter. NOTE: this is disputed by the vendor because the render function is not intended to be used with untrusted input.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 9.8 | Applicable | `ejs 3.1.6` | No fix available |" + }, + "properties": { + "security-severity": "9.8" + } + }, + { + "id": "CVE-2024-39249_async_3.2.4", + "shortDescription": { + "text": "[CVE-2024-39249] async 3.2.4" + }, + "help": { + "text": "Async <= 2.6.4 and <= 3.2.5 are vulnerable to ReDoS (Regular Expression Denial of Service) while parsing function in autoinject function. NOTE: this is disputed by the supplier because there is no realistic threat model: regular expressions are not used with untrusted input.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 0.0 | Not Covered | `ejs 3.1.6` | No fix available |" + }, + "properties": { + "security-severity": "0.0" + } + }, + { + "id": "CVE-2020-28500_lodash_4.17.0", + "shortDescription": { + "text": "[CVE-2020-28500] lodash 4.17.0" + }, + "help": { + "text": "Lodash versions prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 5.3 | Not Applicable | `lodash 4.17.0` | [4.17.21] |" + }, + "properties": { + "security-severity": "5.3" + } + }, + { + "id": "CVE-2020-8203_lodash_4.17.0", + "shortDescription": { + "text": "[CVE-2020-8203] lodash 4.17.0" + }, + "help": { + "text": "Prototype pollution attack when using _.zipObjectDeep in lodash before 4.17.20.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.4 | Not Applicable | `lodash 4.17.0` | [4.17.19] |" + }, + "properties": { + "security-severity": "7.4" + } + }, + { + "id": "CVE-2019-10744_lodash_4.17.0", + "shortDescription": { + "text": "[CVE-2019-10744] lodash 4.17.0" + }, + "help": { + "text": "Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 9.1 | Not Applicable | `lodash 4.17.0` | [4.17.12] |" + }, + "properties": { + "security-severity": "9.1" + } + }, + { + "id": "CVE-2022-29078_ejs_3.1.6", + "shortDescription": { + "text": "[CVE-2022-29078] ejs 3.1.6" + }, + "help": { + "text": "The ejs (aka Embedded JavaScript templates) package 3.1.6 for Node.js allows server-side template injection in settings[view options][outputFunctionName]. This is parsed as an internal option, and overwrites the outputFunctionName option with an arbitrary OS command (which is executed upon template compilation).", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 9.8 | Not Applicable | `ejs 3.1.6` | [3.1.7] |" + }, + "properties": { + "security-severity": "9.8" + } + }, + { + "id": "CVE-2024-29041_express_4.18.2", + "shortDescription": { + "text": "[CVE-2024-29041] express 4.18.2" + }, + "help": { + "text": "Express.js minimalist web framework for node. Versions of Express.js prior to 4.19.0 and all pre-release alpha and beta versions of 5.0 are affected by an open redirect vulnerability using malformed URLs. When a user of Express performs a redirect using a user-provided URL Express performs an encode [using `encodeurl`](https://github.com/pillarjs/encodeurl) on the contents before passing it to the `location` header. This can cause malformed URLs to be evaluated in unexpected ways by common redirect allow list implementations in Express applications, leading to an Open Redirect via bypass of a properly implemented allow list. The main method impacted is `res.location()` but this is also called from within `res.redirect()`. The vulnerability is fixed in 4.19.2 and 5.0.0-beta.3.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 6.1 | Not Covered | `express 4.18.2` | [4.19.2], [5.0.0-beta.3] |" + }, + "properties": { + "security-severity": "6.1" + } + }, + { + "id": "CVE-2018-16487_lodash_4.17.0", + "shortDescription": { + "text": "[CVE-2018-16487] lodash 4.17.0" + }, + "help": { + "text": "A prototype pollution vulnerability was found in lodash <4.17.11 where the functions merge, mergeWith, and defaultsDeep can be tricked into adding or modifying properties of Object.prototype.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 5.6 | Not Applicable | `lodash 4.17.0` | [4.17.11] |" + }, + "properties": { + "security-severity": "5.6" + } + } + ], + "version": "3.104.8" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "workingDirectory": { + "uri": "/Users/user/ejs-frog-demo" + } + } + ], + "results": [ + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "[4.19.2], [5.0.0-beta.3]" + }, + "ruleId": "CVE-2024-29041_express_4.18.2", + "ruleIndex": 10, + "level": "warning", + "message": { + "text": "[CVE-2024-29041] express 4.18.2" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2024-39249_async_3.2.4", + "ruleIndex": 5, + "level": "none", + "message": { + "text": "[CVE-2024-39249] ejs 3.1.6" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.21]" + }, + "ruleId": "CVE-2020-28500_lodash_4.17.0", + "ruleIndex": 6, + "level": "warning", + "message": { + "text": "[CVE-2020-28500] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "[4.17.5]" + }, + "ruleId": "CVE-2018-3721_lodash_4.17.0", + "ruleIndex": 0, + "level": "warning", + "message": { + "text": "[CVE-2018-3721] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.21]" + }, + "ruleId": "CVE-2021-23337_lodash_4.17.0", + "ruleIndex": 1, + "level": "error", + "message": { + "text": "[CVE-2021-23337] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.11]" + }, + "ruleId": "CVE-2018-16487_lodash_4.17.0", + "ruleIndex": 11, + "level": "warning", + "message": { + "text": "[CVE-2018-16487] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.19]" + }, + "ruleId": "CVE-2020-8203_lodash_4.17.0", + "ruleIndex": 7, + "level": "error", + "message": { + "text": "[CVE-2020-8203] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.12]" + }, + "ruleId": "CVE-2019-10744_lodash_4.17.0", + "ruleIndex": 8, + "level": "error", + "message": { + "text": "[CVE-2019-10744] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "[4.17.11]" + }, + "ruleId": "CVE-2019-1010266_lodash_4.17.0", + "ruleIndex": 2, + "level": "warning", + "message": { + "text": "[CVE-2019-1010266] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[3.1.7]" + }, + "ruleId": "CVE-2022-29078_ejs_3.1.6", + "ruleIndex": 9, + "level": "error", + "message": { + "text": "[CVE-2022-29078] ejs 3.1.6" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[3.1.10]" + }, + "ruleId": "CVE-2024-33883_ejs_3.1.6", + "ruleIndex": 3, + "level": "warning", + "message": { + "text": "[CVE-2024-33883] ejs 3.1.6" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2023-29827_ejs_3.1.6", + "ruleIndex": 4, + "level": "error", + "message": { + "text": "[CVE-2023-29827] ejs 3.1.6" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "[4.19.2], [5.0.0-beta.3]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2024-29041_express_4.18.2", + "ruleIndex": 10, + "level": "warning", + "message": { + "text": "[CVE-2024-29041] express 4.18.2" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.11]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2018-16487_lodash_4.17.0", + "ruleIndex": 11, + "level": "warning", + "message": { + "text": "[CVE-2018-16487] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "[4.17.11]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2019-1010266_lodash_4.17.0", + "ruleIndex": 2, + "level": "warning", + "message": { + "text": "[CVE-2019-1010266] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[3.1.10]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2024-33883_ejs_3.1.6", + "ruleIndex": 3, + "level": "warning", + "message": { + "text": "[CVE-2024-33883] ejs 3.1.6" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Applicable", + "fixedVersion": "No fix available", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2023-29827_ejs_3.1.6", + "ruleIndex": 4, + "level": "error", + "message": { + "text": "[CVE-2023-29827] ejs 3.1.6" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.12]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2019-10744_lodash_4.17.0", + "ruleIndex": 8, + "level": "error", + "message": { + "text": "[CVE-2019-10744] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.21]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2020-28500_lodash_4.17.0", + "ruleIndex": 6, + "level": "warning", + "message": { + "text": "[CVE-2020-28500] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "[4.17.5]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2018-3721_lodash_4.17.0", + "ruleIndex": 0, + "level": "warning", + "message": { + "text": "[CVE-2018-3721] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.21]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2021-23337_lodash_4.17.0", + "ruleIndex": 1, + "level": "error", + "message": { + "text": "[CVE-2021-23337] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[4.17.19]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2020-8203_lodash_4.17.0", + "ruleIndex": 7, + "level": "error", + "message": { + "text": "[CVE-2020-8203] lodash 4.17.0" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "[3.1.7]", + "watch": "Security_watch_1" + }, + "ruleId": "CVE-2022-29078_ejs_3.1.6", + "ruleIndex": 9, + "level": "error", + "message": { + "text": "[CVE-2022-29078] ejs 3.1.6" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "package.json" + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/testdata/output/audit/audit_simple_json.json b/tests/testdata/output/audit/audit_simple_json.json new file mode 100644 index 00000000..e710dc91 --- /dev/null +++ b/tests/testdata/output/audit/audit_simple_json.json @@ -0,0 +1,1862 @@ +{ + "multiScanId": "7d5e4733-3f93-11ef-8147-e610d09d7daa", + "vulnerabilities": [ + { + "severity": "Critical", + "impactedPackageName": "ejs", + "impactedPackageVersion": "3.1.6", + "impactedPackageType": "npm", + "components": [ + { + "name": "ejs", + "version": "3.1.6", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "ejs v3.1.9 is vulnerable to server-side template injection. If the ejs file is controllable, template injection can be implemented through the configuration settings of the closeDelimiter parameter. NOTE: this is disputed by the vendor because the render function is not intended to be used with untrusted input.", + "applicable": "Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2023-29827", + "cvssV2": "", + "cvssV3": "9.8", + "applicability": { + "status": "Applicable", + "scannerDescription": "The scanner checks whether any of the following conditions are met:\n\n1. The `ejs.renderFile` function is called with an unknown third argument.\n\n2. The `ejs.compile` function is called with an unknown second argument.\n\n3. The `express.set` function is called with any of the following arguments:\n\n* `express.set(\"view engine\", \"ejs\")`\n* `express.set(\"view engine\", {USER_INPUT})`\n* `express.set({USER_INPUT}, \"ejs\")`\n* `express.set({USER_INPUT}, {USER_INPUT})`", + "evidence": [ + { + "file": "server.js", + "startLine": 14, + "startColumn": 1, + "endLine": 14, + "endColumn": 30, + "snippet": "app.set('view engine', 'ejs')", + "reason": "The vulnerable functionality is triggered since express.set is called with 'view engine' as the first argument and 'ejs' as the second argument or both arguments with external input" + } + ] + } + } + ], + "issueId": "XRAY-520200", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2023-29827", + "https://github.com/mde/ejs/issues/720", + "https://github.com/mde/ejs/blob/main/SECURITY.md#out-of-scope-vulnerabilities" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "ejs", + "version": "3.1.6" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Low", + "summary": "Insufficient input validation can lead to template injection in ejs when attackers can control both the rendered template and rendering options.", + "details": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to bypass ejs' template injection restrictions, by abusing the `closeDelimiter` rendering option, in the case when -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\n\nThe vulnerability was **rightfully disputed** due to the fact that a vulnerable configuration is extremely unlikely to exist in any real-world setup. As such, the maintainers will not provide a fix for this (non-)issue.\n\nExample of a vulnerable application -\n```js\nconst express = require('express')\nconst app = express()\nconst port = 3000\n\napp.set('view engine', 'ejs');\n\napp.get('/page', (req,res) =\u003e {\n res.render('page', req.query); // OPTS (2nd parameter) IS ATTACKER-CONTROLLED\n})\n\napp.listen(port, () =\u003e {\n console.log(\"Example app listening on port ${port}\")\n})\n```\n\nContents of `page.ejs` (very unlikely to be attacker controlled) -\n```js\n%%1\");process.mainModule.require('child_process').execSync('calc');//\n```\n\nIn this case, sending `closeDelimiter` with the same malicious code that already exists at `page.ejs` will trigger the injection -\n`http://127.0.0.1:3000/page?settings[view%20options][closeDelimiter]=1\")%3bprocess.mainModule.require('child_process').execSync('calc')%3b//`", + "severityReasons": [ + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The CVSS does not take into account the rarity of a vulnerable configuration to exist", + "isPositive": true + }, + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The vulnerability can be exploited only under the following conditions -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\nThis vulnerable configuration is extremely unlikely to exist in any real-world setup.", + "isPositive": true + }, + { + "name": "The issue has been disputed by the vendor", + "isPositive": true + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates template injection" + } + ] + } + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "lodash prior to 4.17.11 is affected by: CWE-400: Uncontrolled Resource Consumption. The impact is: Denial of service. The component is: Date handler. The attack vector is: Attacker provides very long strings, which the library attempts to match using a regular expression. The fixed version is: 4.17.11.", + "applicable": "Not Covered", + "fixedVersions": [ + "[4.17.11]" + ], + "cves": [ + { + "id": "CVE-2019-1010266", + "cvssV2": "4.0", + "cvssV3": "6.5", + "applicability": { + "status": "Not Covered" + } + } + ], + "issueId": "XRAY-85049", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2019-1010266", + "https://github.com/lodash/lodash/wiki/Changelog", + "https://snyk.io/vuln/SNYK-JS-LODASH-73639", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/issues/3359", + "https://github.com/lodash/lodash/commit/5c08f18d365b64063bfbfa686cbb97cdd6267347" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "lodash node module before 4.17.5 suffers from a Modification of Assumed-Immutable Data (MAID) vulnerability via defaultsDeep, merge, and mergeWith functions, which allows a malicious user to modify the prototype of \"Object\" via __proto__, causing the addition or modification of an existing property that will exist on all objects.", + "applicable": "Not Covered", + "fixedVersions": [ + "[4.17.5]" + ], + "cves": [ + { + "id": "CVE-2018-3721", + "cvssV2": "4.0", + "cvssV3": "6.5", + "applicability": { + "status": "Not Covered" + } + } + ], + "issueId": "XRAY-72918", + "references": [ + "https://www.npmjs.com/advisories/577", + "https://hackerone.com/reports/310443", + "https://github.com/advisories/GHSA-fvqr-27wr-82fm", + "https://nvd.nist.gov/vuln/detail/CVE-2018-3721", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/commit/d8e069cc3410082e44eb18fcf8e7f3d08ebe1d4a" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Medium", + "impactedPackageName": "express", + "impactedPackageVersion": "4.18.2", + "impactedPackageType": "npm", + "components": [ + { + "name": "express", + "version": "4.18.2", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Express.js minimalist web framework for node. Versions of Express.js prior to 4.19.0 and all pre-release alpha and beta versions of 5.0 are affected by an open redirect vulnerability using malformed URLs. When a user of Express performs a redirect using a user-provided URL Express performs an encode [using `encodeurl`](https://github.com/pillarjs/encodeurl) on the contents before passing it to the `location` header. This can cause malformed URLs to be evaluated in unexpected ways by common redirect allow list implementations in Express applications, leading to an Open Redirect via bypass of a properly implemented allow list. The main method impacted is `res.location()` but this is also called from within `res.redirect()`. The vulnerability is fixed in 4.19.2 and 5.0.0-beta.3.", + "applicable": "Not Covered", + "fixedVersions": [ + "[4.19.2]", + "[5.0.0-beta.3]" + ], + "cves": [ + { + "id": "CVE-2024-29041", + "cvssV2": "", + "cvssV3": "6.1", + "applicability": { + "status": "Not Covered" + } + } + ], + "issueId": "XRAY-594935", + "references": [ + "https://github.com/koajs/koa/issues/1800", + "https://github.com/expressjs/express/pull/5539", + "https://github.com/expressjs/express/commit/0b746953c4bd8e377123527db11f9cd866e39f94", + "https://github.com/expressjs/express/commit/0867302ddbde0e9463d0564fea5861feb708c2dd", + "https://github.com/advisories/GHSA-rv95-896h-c2vc", + "https://expressjs.com/en/4x/api.html#res.location", + "https://nvd.nist.gov/vuln/detail/CVE-2024-29041", + "https://github.com/expressjs/express/security/advisories/GHSA-rv95-896h-c2vc" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "express", + "version": "4.18.2" + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Unknown", + "impactedPackageName": "async", + "impactedPackageVersion": "3.2.4", + "impactedPackageType": "npm", + "components": [ + { + "name": "ejs", + "version": "3.1.6", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Async \u003c= 2.6.4 and \u003c= 3.2.5 are vulnerable to ReDoS (Regular Expression Denial of Service) while parsing function in autoinject function. NOTE: this is disputed by the supplier because there is no realistic threat model: regular expressions are not used with untrusted input.", + "applicable": "Not Covered", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2024-39249", + "cvssV2": "", + "cvssV3": "", + "applicability": { + "status": "Not Covered", + "scannerDescription": "Never applicable. The vulnerability is exploitable only if an attacker has access to the source code." + } + } + ], + "issueId": "XRAY-609848", + "references": [ + "https://github.com/zunak/CVE-2024-39249", + "https://github.com/caolan/async/blob/v3.2.5/lib/autoInject.js#L41", + "https://nvd.nist.gov/vuln/detail/CVE-2024-39249", + "https://github.com/caolan/async/blob/v3.2.5/lib/autoInject.js#L6", + "https://github.com/caolan/async/issues/1975#issuecomment-2204528153", + "https://github.com/zunak/CVE-2024-39249/issues/1" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "ejs", + "version": "3.1.6" + }, + { + "name": "jake", + "version": "10.8.7" + }, + { + "name": "async", + "version": "3.2.4" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Low", + "summary": "ReDoS in Async may lead to denial of service while parsing malformed source code.", + "severityReasons": [ + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The reported CVSS does not reflect the severity of the vulnerability.", + "isPositive": true + }, + { + "name": "The issue cannot result in a severe impact (such as remote code execution)", + "description": "To exploit this issue an attacker must change the source code of the application. In cases where an attacker can already modify (or fully control) the source code, the attacker can immediately achieve arbitrary code execution - thus this issue has almost no security impact.", + "isPositive": true + }, + { + "name": "The issue has an exploit published", + "description": "A proof-of-concept has been published in the advisory." + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The issue requires the use of the `async.autoInject` function to be vulnerable.", + "isPositive": true + } + ] + } + }, + { + "severity": "Critical", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.12]" + ], + "cves": [ + { + "id": "CVE-2019-10744", + "cvssV2": "6.4", + "cvssV3": "9.1", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `defaultsDeep` is called with external input to its 2nd (`sources`) argument, and the `Object.freeze()` remediation is not present.", + "evidence": [ + { + "file": "server.js", + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": "Object.freeze(Object.prototype)", + "reason": "Prototype pollution `Object.freeze` remediation was detected" + } + ] + } + } + ], + "issueId": "XRAY-85679", + "references": [ + "https://www.npmjs.com/advisories/1065", + "https://github.com/lodash/lodash/pull/4336", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://security.netapp.com/advisory/ntap-20191004-0005/", + "https://snyk.io/vuln/SNYK-JS-LODASH-450202", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp%3Butm_medium=RSS", + "https://github.com/advisories/GHSA-jf85-cpcp-j695", + "https://nvd.nist.gov/vuln/detail/CVE-2019-10744" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "High", + "summary": "Insufficient input validation in lodash defaultsDeep() leads to prototype pollution.", + "details": "[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, \u0026 extras.\n\nThe function `defaultsDeep` was found to be vulnerable to prototype pollution, when accepting arbitrary source objects from untrusted input\n\nExample of code vulnerable to this issue - \n```js\nconst lodash = require('lodash'); \nconst evilsrc = {constructor: {prototype: {evilkey: \"evilvalue\"}}};\nlodash.defaultsDeep({}, evilsrc)\n```", + "severityReasons": [ + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrates exploitation of this issue" + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "isPositive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into the `defaultsDeep` method (2nd arg)", + "isPositive": true + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "severity": "Critical", + "impactedPackageName": "ejs", + "impactedPackageVersion": "3.1.6", + "impactedPackageType": "npm", + "components": [ + { + "name": "ejs", + "version": "3.1.6", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "The ejs (aka Embedded JavaScript templates) package 3.1.6 for Node.js allows server-side template injection in settings[view options][outputFunctionName]. This is parsed as an internal option, and overwrites the outputFunctionName option with an arbitrary OS command (which is executed upon template compilation).", + "applicable": "Not Applicable", + "fixedVersions": [ + "[3.1.7]" + ], + "cves": [ + { + "id": "CVE-2022-29078", + "cvssV2": "7.5", + "cvssV3": "9.8", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks for two vulnerable flows:\n\n1. Whether the `express.set` function is called with the arguments: `view engine` and `ejs`, or external input and if it's followed by a call to the vulnerable function `render` with an unknown second argument.\n\n2. Whether the `renderFile` function is called with an unknown second argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present.", + "evidence": [ + { + "file": "server.js", + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": "Object.freeze(Object.prototype)", + "reason": "Prototype pollution `Object.freeze` remediation was detected" + } + ] + } + } + ], + "issueId": "XRAY-209002", + "references": [ + "https://github.com/mde/ejs/commit/15ee698583c98dadc456639d6245580d17a24baf", + "https://eslam.io/posts/ejs-server-side-template-injection-rce/", + "https://security.netapp.com/advisory/ntap-20220804-0001", + "https://github.com/mde/ejs/releases", + "https://nvd.nist.gov/vuln/detail/CVE-2022-29078", + "https://eslam.io/posts/ejs-server-side-template-injection-rce", + "https://github.com/mde/ejs", + "https://security.netapp.com/advisory/ntap-20220804-0001/" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "ejs", + "version": "3.1.6" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Insufficient input validation in EJS enables attackers to perform template injection when attacker can control the rendering options.", + "details": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to perform template injection on the `opts.outputFunctionName` variable, since the variable is injected into the template body without any escaping. Although it is unlikely that the attacker can directly control the `outputFunctionName` property, it is possible that it can be influenced in conjunction with a prototype pollution vulnerability.\n\nOnce template injection is achieved, the attacker can immediately perform remote code execution since the template engine (EJS) allows executing arbitrary JavaScript code.\n\nExample of a vulnerable Node.js application -\n```js\nconst express = require('express');\nconst bodyParser = require('body-parser');\nconst lodash = require('lodash');\nconst ejs = require('ejs');\n\nconst app = express();\n\napp\n .use(bodyParser.urlencoded({extended: true}))\n .use(bodyParser.json());\n\napp.set('views', './');\napp.set('view engine', 'ejs');\n\napp.get(\"/\", (req, res) =\u003e {\n res.render('index');\n});\n\napp.post(\"/\", (req, res) =\u003e {\n let data = {};\n let input = JSON.parse(req.body.content);\n lodash.defaultsDeep(data, input);\n res.json({message: \"OK\"});\n});\n\nlet server = app.listen(8086, '0.0.0.0', function() {\n console.log('Listening on port %d', server.address().port);\n});\n```\n\nExploiting the above example for RCE -\n`curl 127.0.0.1:8086 -v --data 'content={\"constructor\": {\"prototype\": {\"outputFunctionName\": \"a; return global.process.mainModule.constructor._load(\\\"child_process\\\").execSync(\\\"whoami\\\"); //\"}}}'\n`\n\nDue to the prototype pollution in the `lodash.defaultsDeep` call, an attacker can inject the `outputFunctionName` property with an arbitrary value. The chosen value executes an arbitrary process via the `child_process` module.", + "severityReasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The attacker has to find a way to get their malicious input to `opts.outputFunctionName`, which will usually require exploitation of a prototype pollution vulnerability somewhere else in the code. However, there could be cases where the attacker can pass malicious data to the render function directly because of design problems in other code using EJS.", + "isPositive": true + }, + { + "name": "The issue has an exploit published", + "description": "There are multiple examples of exploits for this vulnerability online." + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Successful exploitation of this vulnerability leads to remote code execution." + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.\n\nNote that this mitigation is supposed to stop any prototype pollution attacks which can allow an attacker to control the `opts.outputFunctionName` parameter indirectly.\n\nThe mitigation will not stop any (extremely unlikely) scenarios where the JavaScript code allows external input to directly affect `opts.outputFunctionName`." + } + }, + { + "severity": "High", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.21]" + ], + "cves": [ + { + "id": "CVE-2021-23337", + "cvssV2": "6.5", + "cvssV3": "7.2", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `lodash.template` is called with external input to its 2nd (`options`) argument." + } + } + ], + "issueId": "XRAY-140575", + "references": [ + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074929", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JS-LODASH-1040724", + "https://security.netapp.com/advisory/ntap-20210312-0006", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c", + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074928", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074932", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js%23L14851", + "https://github.com/advisories/GHSA-35jh-r3h4-6jhm", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074930", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074931", + "https://nvd.nist.gov/vuln/detail/CVE-2021-23337", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L14851" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Improper sanitization in the lodash template function leads to JavaScript code injection through the options argument.", + "details": "JavaScript-based applications (both frontend and backend) that use the [template function](https://lodash.com/docs/4.17.15#template) -`_.template([string=''], [options={}])` from the [lodash](https://lodash.com/) utility library and provide the `options` argument (specifically the `variable` option) from untrusted user input, are vulnerable to JavaScript code injection. This issue can be easily exploited, and an exploitation example is [publicly available](https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c#diff-a561630bb56b82342bc66697aee2ad96efddcbc9d150665abd6fb7ecb7c0ab2fR22303) in the fix tests that was introduced in version 4.17.21 - \n```js\nlodash.template('', { variable: '){console.log(process.env)}; with(obj' })()\n```", + "severityReasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "It is highly unlikely that a JS program will accept arbitrary remote input into the template's `options` argument", + "isPositive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The attacker must find remote input that propagates into the `options` argument of a `template` call", + "isPositive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Leads to remote code execution through JS code injection" + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates arbitrary JS code execution" + } + ] + } + }, + { + "severity": "High", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Prototype pollution attack when using _.zipObjectDeep in lodash before 4.17.20.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.19]" + ], + "cves": [ + { + "id": "CVE-2020-8203", + "cvssV2": "5.8", + "cvssV3": "7.4", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `zipObjectDeep` is called with external input to its 1st (`props`) and 2nd (`values`) arguments, and the `Object.freeze()` remediation is not present." + } + } + ], + "issueId": "XRAY-114089", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2020-8203", + "https://www.oracle.com/security-alerts/cpuapr2022.html", + "https://hackerone.com/reports/864701", + "https://hackerone.com/reports/712065", + "https://github.com/advisories/GHSA-p6mc-m468-83gw", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://github.com/github/advisory-database/pull/2884", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/c84fe82760fb2d3e03a63379b297a1cc1a2fce12", + "https://security.netapp.com/advisory/ntap-20200724-0006/", + "https://web.archive.org/web/20210914001339/https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://github.com/lodash/lodash/issues/4874", + "https://github.com/lodash/lodash/wiki/Changelog#v41719" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Critical", + "summary": "Prototype pollution in lodash object merging and zipping functions leads to code injection.", + "details": "[lodash](https://lodash.com/) is a JavaScript library which provides utility functions for common programming tasks.\n\nJavaScript frontend and Node.js-based backend applications that merge or zip objects using the lodash functions `mergeWith`, `merge` and `zipObjectDeep` are vulnerable to [prototype pollution](https://medium.com/node-modules/what-is-prototype-pollution-and-why-is-it-such-a-big-deal-2dd8d89a93c) if one or more of the objects it receives as arguments are obtained from user input. \nAn attacker controlling this input given to the vulnerable functions can inject properties to JavaScript special objects such as [Object.prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) from which all JavaScript objects inherit properties and methods. Any change on `Object.prototype` properties will then propagate through the prototype chain inheritance to all of the objects in a JavaScript application. This in turn would allow an attacker to add new properties or modify existing properties which will have application specific implications that could lead to DoS (denial of service), authentication bypass, privilege escalation and even RCE (remote code execution) in [some cases](https://youtu.be/LUsiFV3dsK8?t=1152). \nAs an example for privilege escalation, consider a JavaScript application that has a `user` object which has a Boolean property of `user.isAdmin` which is used to decide which actions the user may take. If an attacker can modify or add the `isAdmin` property through prototype pollution, it can escalate the privileges of its own user to those of an admin. \nAs exploitation is usually application specific, successful exploitation is much more likely if an attacker have access to the JavaScript application code. As such, frontend applications are more vulnerable to this vulnerability than Node.js backend applications.", + "severityReasons": [ + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "isPositive": true + }, + { + "name": "The issue can be exploited by attackers over the network" + }, + { + "name": "The issue is trivial to exploit and does not require a published writeup or PoC" + } + ], + "remediation": "##### Deployment mitigations\n\nAs general guidelines against prototype pollution, first consider not merging objects originating from user input or using a Map structure instead of an object. If merging objects is needed, look into creating objects without a prototype with `Object.create(null)` or into freezing `Object.prototype` with `Object.freeze()`. Finally, it is always best to perform input validation with a a [JSON schema validator](https://github.com/ajv-validator/ajv), which could mitigate this issue entirely in many cases." + } + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "A prototype pollution vulnerability was found in lodash \u003c4.17.11 where the functions merge, mergeWith, and defaultsDeep can be tricked into adding or modifying properties of Object.prototype.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.11]" + ], + "cves": [ + { + "id": "CVE-2018-16487", + "cvssV2": "6.8", + "cvssV3": "5.6", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.merge` with external input to its 2nd (`sources`) argument.\n* `lodash.mergeWith` with external input to its 2nd (`sources`) argument.\n* `lodash.defaultsDeep` with external input to its 2nd (`sources`) argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present.", + "evidence": [ + { + "file": "server.js", + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": "Object.freeze(Object.prototype)", + "reason": "Prototype pollution `Object.freeze` remediation was detected" + } + ] + } + } + ], + "issueId": "XRAY-75300", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2018-16487", + "https://www.npmjs.com/advisories/782", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/advisories/GHSA-4xc9-xhrj-v574", + "https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://hackerone.com/reports/380873" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "High", + "summary": "Insufficient input validation in the Lodash library leads to prototype pollution.", + "details": "The [Lodash](https://lodash.com/) library is an open-source JavaScript project that simplifies operations on string, arrays, numbers, and other objects. It is widely used in connected devices. \n\nThe `merge`, `mergeWith`, and `defaultsDeep` methods in Lodash are vulnerable to [prototype pollution](https://shieldfy.io/security-wiki/prototype-pollution/introduction-to-prototype-pollution/). Attackers can exploit this vulnerability by specifying a crafted `sources` parameter to any of these methods, which can modify the prototype properties of the `Object`, `Function`, `Array`, `String`, `Number`, and `Boolean` objects. A public [exploit](https://hackerone.com/reports/380873) exists which performs the prototype pollution with an arbitrary key and value.\n\nThe library implementation has a bug in the `safeGet()` function in the `lodash.js` module that allows for adding or modifying `prototype` properties of various objects. The official [solution](https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad) fixes the bug by explicitly forbidding the addition or modification of `prototype` properties.\n\nA related CVE (CVE-2018-3721) covers the same issue prior to Lodash version 4.17.5, but the fix for that was incomplete.", + "severityReasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into one of the following methods - \n* `merge` - 2nd argument\n* `mergeWith` - 2nd argument\n* `defaultsDeep` - 2nd argument", + "isPositive": true + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "isPositive": true + }, + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrated exploitation by injecting an attacker controlled key and value into the prototype" + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "severity": "Medium", + "impactedPackageName": "ejs", + "impactedPackageVersion": "3.1.6", + "impactedPackageType": "npm", + "components": [ + { + "name": "ejs", + "version": "3.1.6", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "The ejs (aka Embedded JavaScript templates) package before 3.1.10 for Node.js lacks certain pollution protection.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[3.1.10]" + ], + "cves": [ + { + "id": "CVE-2024-33883", + "cvssV2": "", + "cvssV3": "4.0", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `ejs.compile()` is called." + } + } + ], + "issueId": "XRAY-599735", + "references": [ + "https://security.netapp.com/advisory/ntap-20240605-0003/", + "https://security.netapp.com/advisory/ntap-20240605-0003", + "https://github.com/mde/ejs/commit/e469741dca7df2eb400199e1cdb74621e3f89aa5", + "https://github.com/mde/ejs/compare/v3.1.9...v3.1.10", + "https://github.com/advisories/GHSA-ghr5-ch3p-vcr6", + "https://nvd.nist.gov/vuln/detail/CVE-2024-33883" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "ejs", + "version": "3.1.6" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Insufficient input validation in EJS may lead to prototype pollution.", + "details": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as `EJS`, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nA prototype pollution gadget within the EJS template engine could potentially be leveraged by attackers to achieve remote code execution or DoS via prototype pollution.\n\n```\nfunction Template(text, opts) {\n opts = opts || utils.createNullProtoObjWherePossible();\n```\n\nWhen checking for the presence of a property within an object variable, the lookup scope isn't explicitly defined. In JavaScript, the absence of a defined lookup scope prompts a search up to the root prototype (`Object.prototype`). This could potentially be under the control of an attacker if another prototype pollution vulnerability is present within the application.\n\nIf the application server is using the EJS as the backend template engine, and there is another prototype pollution vulnerability in the application, then the attacker could leverage the found gadgets in the EJS template engine to escalate the prototype pollution to remote code execution or DoS.\n\nThe following code will execute a command on the server by polluting `opts.escapeFunction`:\n \n```\nconst express = require('express');\nconst app = express();\nconst port = 8008;\nconst ejs = require('ejs');\n\n// Set EJS as the view engine\napp.set('view engine', 'ejs');\n\napp.get('/', (req, res) =\u003e {\n \n const data = {title: 'Welcome', message: 'Hello'};\n\n // Sample EJS template string\n const templateString = `\u003chtml\u003e\u003chead\u003e\u003ctitle\u003e\u003c%= title %\u003e\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003ch1\u003e\u003c%= message %\u003e\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e`;\n\n const { exec } = require('child_process');\n\n function myFunc() {\n exec('bash -c \"echo 123\"', (error, stdout, stderr) =\u003e {\n if (error) {\n console.error(`exec error: ${error}`);\n return;\n }\n if (stderr){\n console.log(`stderr : ${stderr}`);\n return;\n }\n // Handle success\n console.log(`Command executed successfully. Output: ${stdout}`);\n });\n }\n\n const options = {client:false};\n\n Object.prototype.escapeFunction = myFunc;\n \n const compiledTemplate = ejs.compile(templateString, options);\n const renderedHtml = compiledTemplate(data);\n res.send(renderedHtml);\n});\n\n// Start the server\napp.listen(port, () =\u003e {\n console.log(`Server is running on http://localhost:${port}`);\n});\n```", + "severityReasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "Attackers can only leverage this vulnerability when the application server is using the EJS as the backend template engine. Moreover, there must be a second prototype pollution vulnerability in the application.", + "isPositive": true + }, + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "CVSS does not take into account the unlikely prerequisites necessary for exploitation.", + "isPositive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "A prototype pollution attack allows the attacker to inject new properties into all JavaScript objects.\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable." + } + ] + } + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.21]" + ], + "cves": [ + { + "id": "CVE-2020-28500", + "cvssV2": "5.0", + "cvssV3": "5.3", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.trim` with external input to its 1st (`string`) argument.\n* `lodash.toNumber` with external input to its 1st (`value`) argument.\n* `lodash.trimEnd` with external input to its 1st (`string`) argument." + } + } + ], + "issueId": "XRAY-140562", + "references": [ + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074894", + "https://github.com/lodash/lodash/blob/npm/trimEnd.js%23L8", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074893", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074892", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28500", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074895", + "https://github.com/lodash/lodash/pull/5065/commits/02906b8191d3c100c193fe6f7b27d1c40f200bb7", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/advisories/GHSA-29mw-wpgm-hmr9", + "https://github.com/lodash/lodash/pull/5065", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074896", + "https://snyk.io/vuln/SNYK-JS-LODASH-1018905" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "ReDoS in lodash could lead to a denial of service when handling untrusted strings.", + "details": "JavaScript-based applications that use [lodash](https://github.com/lodash/lodash) and specifically the [_.toNumber](https://lodash.com/docs/4.17.15#toNumber), [_.trim](https://lodash.com/docs/4.17.15#trim) and [_.trimEnd](https://lodash.com/docs/4.17.15#trimEnd) functions, could be vulnerable to DoS (Denial of Service) through a faulty regular expression that introduces a ReDoS (Regular Expression DoS) vulnerability. This vulnerability is only triggered if untrusted user input flows into these vulnerable functions and the attacker can supply arbitrary long strings (over 50kB) that contain whitespaces. \n\nOn a modern Core i7-based system, calling the vulnerable functions with a 50kB string could take between 2 to 3 seconds to execute and 4.5 minutes for a longer 500kB string. The fix improved the regular expression performance so it took only a few milliseconds on the same Core i7-based system. This vulnerability is easily exploitable as all is required is to build a string that triggers it as can be seen in this PoC reproducing code - \n\n```js\nvar untrusted_user_input_50k = \"a\" + ' '.repeat(50000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_50k); // should take a few seconds to run\nvar untrusted_user_input_500k = \"a\" + ' '.repeat(500000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_500k); // should take a few minutes to run\n```", + "severityReasons": [ + { + "name": "The issue has an exploit published", + "description": "Public exploit demonstrated ReDoS" + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "Exploitation depends on parsing user input by the `.toNumber`, `.trim` or `.trimEnd` `lodash` functions, and requires the input to contain whitespaces and be very long (over 50KB)", + "isPositive": true + } + ], + "remediation": "##### Deployment mitigations\n\nTrim untrusted strings based on size before providing it to the vulnerable functions by using the `substring` function to with a fixed maximum size like so - ```js untrusted_user_input.substring(0, max_string_size_less_than_50kB); ```" + } + } + ], + "securityViolations": [ + { + "severity": "Critical", + "impactedPackageName": "ejs", + "impactedPackageVersion": "3.1.6", + "impactedPackageType": "npm", + "components": [ + { + "name": "ejs", + "version": "3.1.6", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "ejs v3.1.9 is vulnerable to server-side template injection. If the ejs file is controllable, template injection can be implemented through the configuration settings of the closeDelimiter parameter. NOTE: this is disputed by the vendor because the render function is not intended to be used with untrusted input.", + "applicable": "Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2023-29827", + "cvssV2": "", + "cvssV3": "9.8", + "applicability": { + "status": "Applicable", + "scannerDescription": "The scanner checks whether any of the following conditions are met:\n\n1. The `ejs.renderFile` function is called with an unknown third argument.\n\n2. The `ejs.compile` function is called with an unknown second argument.\n\n3. The `express.set` function is called with any of the following arguments:\n\n* `express.set(\"view engine\", \"ejs\")`\n* `express.set(\"view engine\", {USER_INPUT})`\n* `express.set({USER_INPUT}, \"ejs\")`\n* `express.set({USER_INPUT}, {USER_INPUT})`", + "evidence": [ + { + "file": "server.js", + "startLine": 14, + "startColumn": 1, + "endLine": 14, + "endColumn": 30, + "snippet": "app.set('view engine', 'ejs')", + "reason": "The vulnerable functionality is triggered since express.set is called with 'view engine' as the first argument and 'ejs' as the second argument or both arguments with external input" + } + ] + } + } + ], + "issueId": "XRAY-520200", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2023-29827", + "https://github.com/mde/ejs/issues/720", + "https://github.com/mde/ejs/blob/main/SECURITY.md#out-of-scope-vulnerabilities" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "ejs", + "version": "3.1.6" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Low", + "summary": "Insufficient input validation can lead to template injection in ejs when attackers can control both the rendered template and rendering options.", + "details": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to bypass ejs' template injection restrictions, by abusing the `closeDelimiter` rendering option, in the case when -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\n\nThe vulnerability was **rightfully disputed** due to the fact that a vulnerable configuration is extremely unlikely to exist in any real-world setup. As such, the maintainers will not provide a fix for this (non-)issue.\n\nExample of a vulnerable application -\n```js\nconst express = require('express')\nconst app = express()\nconst port = 3000\n\napp.set('view engine', 'ejs');\n\napp.get('/page', (req,res) =\u003e {\n res.render('page', req.query); // OPTS (2nd parameter) IS ATTACKER-CONTROLLED\n})\n\napp.listen(port, () =\u003e {\n console.log(\"Example app listening on port ${port}\")\n})\n```\n\nContents of `page.ejs` (very unlikely to be attacker controlled) -\n```js\n%%1\");process.mainModule.require('child_process').execSync('calc');//\n```\n\nIn this case, sending `closeDelimiter` with the same malicious code that already exists at `page.ejs` will trigger the injection -\n`http://127.0.0.1:3000/page?settings[view%20options][closeDelimiter]=1\")%3bprocess.mainModule.require('child_process').execSync('calc')%3b//`", + "severityReasons": [ + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The CVSS does not take into account the rarity of a vulnerable configuration to exist", + "isPositive": true + }, + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The vulnerability can be exploited only under the following conditions -\n1. The template itself can be partially controlled by the attacker\n2. The template rendering options can be fully controlled by the attacker\nThis vulnerable configuration is extremely unlikely to exist in any real-world setup.", + "isPositive": true + }, + { + "name": "The issue has been disputed by the vendor", + "isPositive": true + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates template injection" + } + ] + } + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "lodash prior to 4.17.11 is affected by: CWE-400: Uncontrolled Resource Consumption. The impact is: Denial of service. The component is: Date handler. The attack vector is: Attacker provides very long strings, which the library attempts to match using a regular expression. The fixed version is: 4.17.11.", + "applicable": "Not Covered", + "fixedVersions": [ + "[4.17.11]" + ], + "cves": [ + { + "id": "CVE-2019-1010266", + "cvssV2": "4.0", + "cvssV3": "6.5", + "applicability": { + "status": "Not Covered" + } + } + ], + "issueId": "XRAY-85049", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2019-1010266", + "https://github.com/lodash/lodash/wiki/Changelog", + "https://snyk.io/vuln/SNYK-JS-LODASH-73639", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/issues/3359", + "https://github.com/lodash/lodash/commit/5c08f18d365b64063bfbfa686cbb97cdd6267347" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "lodash node module before 4.17.5 suffers from a Modification of Assumed-Immutable Data (MAID) vulnerability via defaultsDeep, merge, and mergeWith functions, which allows a malicious user to modify the prototype of \"Object\" via __proto__, causing the addition or modification of an existing property that will exist on all objects.", + "applicable": "Not Covered", + "fixedVersions": [ + "[4.17.5]" + ], + "cves": [ + { + "id": "CVE-2018-3721", + "cvssV2": "4.0", + "cvssV3": "6.5", + "applicability": { + "status": "Not Covered" + } + } + ], + "issueId": "XRAY-72918", + "references": [ + "https://www.npmjs.com/advisories/577", + "https://hackerone.com/reports/310443", + "https://github.com/advisories/GHSA-fvqr-27wr-82fm", + "https://nvd.nist.gov/vuln/detail/CVE-2018-3721", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/lodash/lodash/commit/d8e069cc3410082e44eb18fcf8e7f3d08ebe1d4a" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Medium", + "impactedPackageName": "express", + "impactedPackageVersion": "4.18.2", + "impactedPackageType": "npm", + "components": [ + { + "name": "express", + "version": "4.18.2", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Express.js minimalist web framework for node. Versions of Express.js prior to 4.19.0 and all pre-release alpha and beta versions of 5.0 are affected by an open redirect vulnerability using malformed URLs. When a user of Express performs a redirect using a user-provided URL Express performs an encode [using `encodeurl`](https://github.com/pillarjs/encodeurl) on the contents before passing it to the `location` header. This can cause malformed URLs to be evaluated in unexpected ways by common redirect allow list implementations in Express applications, leading to an Open Redirect via bypass of a properly implemented allow list. The main method impacted is `res.location()` but this is also called from within `res.redirect()`. The vulnerability is fixed in 4.19.2 and 5.0.0-beta.3.", + "applicable": "Not Covered", + "fixedVersions": [ + "[4.19.2]", + "[5.0.0-beta.3]" + ], + "cves": [ + { + "id": "CVE-2024-29041", + "cvssV2": "", + "cvssV3": "6.1", + "applicability": { + "status": "Not Covered" + } + } + ], + "issueId": "XRAY-594935", + "references": [ + "https://github.com/koajs/koa/issues/1800", + "https://github.com/expressjs/express/pull/5539", + "https://github.com/expressjs/express/commit/0b746953c4bd8e377123527db11f9cd866e39f94", + "https://github.com/expressjs/express/commit/0867302ddbde0e9463d0564fea5861feb708c2dd", + "https://github.com/advisories/GHSA-rv95-896h-c2vc", + "https://expressjs.com/en/4x/api.html#res.location", + "https://nvd.nist.gov/vuln/detail/CVE-2024-29041", + "https://github.com/expressjs/express/security/advisories/GHSA-rv95-896h-c2vc" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "express", + "version": "4.18.2" + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Critical", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.12]" + ], + "cves": [ + { + "id": "CVE-2019-10744", + "cvssV2": "6.4", + "cvssV3": "9.1", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `defaultsDeep` is called with external input to its 2nd (`sources`) argument, and the `Object.freeze()` remediation is not present.", + "evidence": [ + { + "file": "server.js", + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": "Object.freeze(Object.prototype)", + "reason": "Prototype pollution `Object.freeze` remediation was detected" + } + ] + } + } + ], + "issueId": "XRAY-85679", + "references": [ + "https://www.npmjs.com/advisories/1065", + "https://github.com/lodash/lodash/pull/4336", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://security.netapp.com/advisory/ntap-20191004-0005/", + "https://snyk.io/vuln/SNYK-JS-LODASH-450202", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp%3Butm_medium=RSS", + "https://github.com/advisories/GHSA-jf85-cpcp-j695", + "https://nvd.nist.gov/vuln/detail/CVE-2019-10744" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "High", + "summary": "Insufficient input validation in lodash defaultsDeep() leads to prototype pollution.", + "details": "[lodash](https://www.npmjs.com/package/lodash) is a modern JavaScript utility library delivering modularity, performance, \u0026 extras.\n\nThe function `defaultsDeep` was found to be vulnerable to prototype pollution, when accepting arbitrary source objects from untrusted input\n\nExample of code vulnerable to this issue - \n```js\nconst lodash = require('lodash'); \nconst evilsrc = {constructor: {prototype: {evilkey: \"evilvalue\"}}};\nlodash.defaultsDeep({}, evilsrc)\n```", + "severityReasons": [ + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrates exploitation of this issue" + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "isPositive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into the `defaultsDeep` method (2nd arg)", + "isPositive": true + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "severity": "Critical", + "impactedPackageName": "ejs", + "impactedPackageVersion": "3.1.6", + "impactedPackageType": "npm", + "components": [ + { + "name": "ejs", + "version": "3.1.6", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "The ejs (aka Embedded JavaScript templates) package 3.1.6 for Node.js allows server-side template injection in settings[view options][outputFunctionName]. This is parsed as an internal option, and overwrites the outputFunctionName option with an arbitrary OS command (which is executed upon template compilation).", + "applicable": "Not Applicable", + "fixedVersions": [ + "[3.1.7]" + ], + "cves": [ + { + "id": "CVE-2022-29078", + "cvssV2": "7.5", + "cvssV3": "9.8", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks for two vulnerable flows:\n\n1. Whether the `express.set` function is called with the arguments: `view engine` and `ejs`, or external input and if it's followed by a call to the vulnerable function `render` with an unknown second argument.\n\n2. Whether the `renderFile` function is called with an unknown second argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present.", + "evidence": [ + { + "file": "server.js", + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": "Object.freeze(Object.prototype)", + "reason": "Prototype pollution `Object.freeze` remediation was detected" + } + ] + } + } + ], + "issueId": "XRAY-209002", + "references": [ + "https://github.com/mde/ejs/commit/15ee698583c98dadc456639d6245580d17a24baf", + "https://eslam.io/posts/ejs-server-side-template-injection-rce/", + "https://security.netapp.com/advisory/ntap-20220804-0001", + "https://github.com/mde/ejs/releases", + "https://nvd.nist.gov/vuln/detail/CVE-2022-29078", + "https://eslam.io/posts/ejs-server-side-template-injection-rce", + "https://github.com/mde/ejs", + "https://security.netapp.com/advisory/ntap-20220804-0001/" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "ejs", + "version": "3.1.6" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Insufficient input validation in EJS enables attackers to perform template injection when attacker can control the rendering options.", + "details": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as EJS, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nWhen rendering views using EJS, it is possible to perform template injection on the `opts.outputFunctionName` variable, since the variable is injected into the template body without any escaping. Although it is unlikely that the attacker can directly control the `outputFunctionName` property, it is possible that it can be influenced in conjunction with a prototype pollution vulnerability.\n\nOnce template injection is achieved, the attacker can immediately perform remote code execution since the template engine (EJS) allows executing arbitrary JavaScript code.\n\nExample of a vulnerable Node.js application -\n```js\nconst express = require('express');\nconst bodyParser = require('body-parser');\nconst lodash = require('lodash');\nconst ejs = require('ejs');\n\nconst app = express();\n\napp\n .use(bodyParser.urlencoded({extended: true}))\n .use(bodyParser.json());\n\napp.set('views', './');\napp.set('view engine', 'ejs');\n\napp.get(\"/\", (req, res) =\u003e {\n res.render('index');\n});\n\napp.post(\"/\", (req, res) =\u003e {\n let data = {};\n let input = JSON.parse(req.body.content);\n lodash.defaultsDeep(data, input);\n res.json({message: \"OK\"});\n});\n\nlet server = app.listen(8086, '0.0.0.0', function() {\n console.log('Listening on port %d', server.address().port);\n});\n```\n\nExploiting the above example for RCE -\n`curl 127.0.0.1:8086 -v --data 'content={\"constructor\": {\"prototype\": {\"outputFunctionName\": \"a; return global.process.mainModule.constructor._load(\\\"child_process\\\").execSync(\\\"whoami\\\"); //\"}}}'\n`\n\nDue to the prototype pollution in the `lodash.defaultsDeep` call, an attacker can inject the `outputFunctionName` property with an arbitrary value. The chosen value executes an arbitrary process via the `child_process` module.", + "severityReasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The attacker has to find a way to get their malicious input to `opts.outputFunctionName`, which will usually require exploitation of a prototype pollution vulnerability somewhere else in the code. However, there could be cases where the attacker can pass malicious data to the render function directly because of design problems in other code using EJS.", + "isPositive": true + }, + { + "name": "The issue has an exploit published", + "description": "There are multiple examples of exploits for this vulnerability online." + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Successful exploitation of this vulnerability leads to remote code execution." + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.\n\nNote that this mitigation is supposed to stop any prototype pollution attacks which can allow an attacker to control the `opts.outputFunctionName` parameter indirectly.\n\nThe mitigation will not stop any (extremely unlikely) scenarios where the JavaScript code allows external input to directly affect `opts.outputFunctionName`." + } + }, + { + "severity": "High", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.21]" + ], + "cves": [ + { + "id": "CVE-2021-23337", + "cvssV2": "6.5", + "cvssV3": "7.2", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `lodash.template` is called with external input to its 2nd (`options`) argument." + } + } + ], + "issueId": "XRAY-140575", + "references": [ + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074929", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JS-LODASH-1040724", + "https://security.netapp.com/advisory/ntap-20210312-0006", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c", + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074928", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074932", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js%23L14851", + "https://github.com/advisories/GHSA-35jh-r3h4-6jhm", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074930", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074931", + "https://nvd.nist.gov/vuln/detail/CVE-2021-23337", + "https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/lodash.js#L14851" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Improper sanitization in the lodash template function leads to JavaScript code injection through the options argument.", + "details": "JavaScript-based applications (both frontend and backend) that use the [template function](https://lodash.com/docs/4.17.15#template) -`_.template([string=''], [options={}])` from the [lodash](https://lodash.com/) utility library and provide the `options` argument (specifically the `variable` option) from untrusted user input, are vulnerable to JavaScript code injection. This issue can be easily exploited, and an exploitation example is [publicly available](https://github.com/lodash/lodash/commit/3469357cff396a26c363f8c1b5a91dde28ba4b1c#diff-a561630bb56b82342bc66697aee2ad96efddcbc9d150665abd6fb7ecb7c0ab2fR22303) in the fix tests that was introduced in version 4.17.21 - \n```js\nlodash.template('', { variable: '){console.log(process.env)}; with(obj' })()\n```", + "severityReasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "It is highly unlikely that a JS program will accept arbitrary remote input into the template's `options` argument", + "isPositive": true + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The attacker must find remote input that propagates into the `options` argument of a `template` call", + "isPositive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Leads to remote code execution through JS code injection" + }, + { + "name": "The issue has an exploit published", + "description": "Published exploit demonstrates arbitrary JS code execution" + } + ] + } + }, + { + "severity": "High", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Prototype pollution attack when using _.zipObjectDeep in lodash before 4.17.20.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.19]" + ], + "cves": [ + { + "id": "CVE-2020-8203", + "cvssV2": "5.8", + "cvssV3": "7.4", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `zipObjectDeep` is called with external input to its 1st (`props`) and 2nd (`values`) arguments, and the `Object.freeze()` remediation is not present." + } + } + ], + "issueId": "XRAY-114089", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2020-8203", + "https://www.oracle.com/security-alerts/cpuapr2022.html", + "https://hackerone.com/reports/864701", + "https://hackerone.com/reports/712065", + "https://github.com/advisories/GHSA-p6mc-m468-83gw", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://github.com/github/advisory-database/pull/2884", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/lodash/lodash/commit/c84fe82760fb2d3e03a63379b297a1cc1a2fce12", + "https://security.netapp.com/advisory/ntap-20200724-0006/", + "https://web.archive.org/web/20210914001339/https://github.com/lodash/lodash/issues/4744", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://github.com/lodash/lodash/issues/4874", + "https://github.com/lodash/lodash/wiki/Changelog#v41719" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Critical", + "summary": "Prototype pollution in lodash object merging and zipping functions leads to code injection.", + "details": "[lodash](https://lodash.com/) is a JavaScript library which provides utility functions for common programming tasks.\n\nJavaScript frontend and Node.js-based backend applications that merge or zip objects using the lodash functions `mergeWith`, `merge` and `zipObjectDeep` are vulnerable to [prototype pollution](https://medium.com/node-modules/what-is-prototype-pollution-and-why-is-it-such-a-big-deal-2dd8d89a93c) if one or more of the objects it receives as arguments are obtained from user input. \nAn attacker controlling this input given to the vulnerable functions can inject properties to JavaScript special objects such as [Object.prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) from which all JavaScript objects inherit properties and methods. Any change on `Object.prototype` properties will then propagate through the prototype chain inheritance to all of the objects in a JavaScript application. This in turn would allow an attacker to add new properties or modify existing properties which will have application specific implications that could lead to DoS (denial of service), authentication bypass, privilege escalation and even RCE (remote code execution) in [some cases](https://youtu.be/LUsiFV3dsK8?t=1152). \nAs an example for privilege escalation, consider a JavaScript application that has a `user` object which has a Boolean property of `user.isAdmin` which is used to decide which actions the user may take. If an attacker can modify or add the `isAdmin` property through prototype pollution, it can escalate the privileges of its own user to those of an admin. \nAs exploitation is usually application specific, successful exploitation is much more likely if an attacker have access to the JavaScript application code. As such, frontend applications are more vulnerable to this vulnerability than Node.js backend applications.", + "severityReasons": [ + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "isPositive": true + }, + { + "name": "The issue can be exploited by attackers over the network" + }, + { + "name": "The issue is trivial to exploit and does not require a published writeup or PoC" + } + ], + "remediation": "##### Deployment mitigations\n\nAs general guidelines against prototype pollution, first consider not merging objects originating from user input or using a Map structure instead of an object. If merging objects is needed, look into creating objects without a prototype with `Object.create(null)` or into freezing `Object.prototype` with `Object.freeze()`. Finally, it is always best to perform input validation with a a [JSON schema validator](https://github.com/ajv-validator/ajv), which could mitigate this issue entirely in many cases." + } + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "A prototype pollution vulnerability was found in lodash \u003c4.17.11 where the functions merge, mergeWith, and defaultsDeep can be tricked into adding or modifying properties of Object.prototype.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.11]" + ], + "cves": [ + { + "id": "CVE-2018-16487", + "cvssV2": "6.8", + "cvssV3": "5.6", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.merge` with external input to its 2nd (`sources`) argument.\n* `lodash.mergeWith` with external input to its 2nd (`sources`) argument.\n* `lodash.defaultsDeep` with external input to its 2nd (`sources`) argument.\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present.", + "evidence": [ + { + "file": "server.js", + "startLine": 4, + "startColumn": 1, + "endLine": 4, + "endColumn": 32, + "snippet": "Object.freeze(Object.prototype)", + "reason": "Prototype pollution `Object.freeze` remediation was detected" + } + ] + } + } + ], + "issueId": "XRAY-75300", + "references": [ + "https://nvd.nist.gov/vuln/detail/CVE-2018-16487", + "https://www.npmjs.com/advisories/782", + "https://security.netapp.com/advisory/ntap-20190919-0004/", + "https://github.com/advisories/GHSA-4xc9-xhrj-v574", + "https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad", + "https://security.netapp.com/advisory/ntap-20190919-0004", + "https://hackerone.com/reports/380873" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "High", + "summary": "Insufficient input validation in the Lodash library leads to prototype pollution.", + "details": "The [Lodash](https://lodash.com/) library is an open-source JavaScript project that simplifies operations on string, arrays, numbers, and other objects. It is widely used in connected devices. \n\nThe `merge`, `mergeWith`, and `defaultsDeep` methods in Lodash are vulnerable to [prototype pollution](https://shieldfy.io/security-wiki/prototype-pollution/introduction-to-prototype-pollution/). Attackers can exploit this vulnerability by specifying a crafted `sources` parameter to any of these methods, which can modify the prototype properties of the `Object`, `Function`, `Array`, `String`, `Number`, and `Boolean` objects. A public [exploit](https://hackerone.com/reports/380873) exists which performs the prototype pollution with an arbitrary key and value.\n\nThe library implementation has a bug in the `safeGet()` function in the `lodash.js` module that allows for adding or modifying `prototype` properties of various objects. The official [solution](https://github.com/lodash/lodash/commit/90e6199a161b6445b01454517b40ef65ebecd2ad) fixes the bug by explicitly forbidding the addition or modification of `prototype` properties.\n\nA related CVE (CVE-2018-3721) covers the same issue prior to Lodash version 4.17.5, but the fix for that was incomplete.", + "severityReasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "An attacker must find remote input that propagates into one of the following methods - \n* `merge` - 2nd argument\n* `mergeWith` - 2nd argument\n* `defaultsDeep` - 2nd argument", + "isPositive": true + }, + { + "name": "The impact of exploiting the issue depends on the context of surrounding software. A severe impact such as RCE is not guaranteed.", + "description": "A prototype pollution attack allows the attacker to inject new properties to all JavaScript objects (but not set existing properties).\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable.", + "isPositive": true + }, + { + "name": "The issue has an exploit published", + "description": "A public PoC demonstrated exploitation by injecting an attacker controlled key and value into the prototype" + } + ], + "remediation": "##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks." + } + }, + { + "severity": "Medium", + "impactedPackageName": "ejs", + "impactedPackageVersion": "3.1.6", + "impactedPackageType": "npm", + "components": [ + { + "name": "ejs", + "version": "3.1.6", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "The ejs (aka Embedded JavaScript templates) package before 3.1.10 for Node.js lacks certain pollution protection.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[3.1.10]" + ], + "cves": [ + { + "id": "CVE-2024-33883", + "cvssV2": "", + "cvssV3": "4.0", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `ejs.compile()` is called." + } + } + ], + "issueId": "XRAY-599735", + "references": [ + "https://security.netapp.com/advisory/ntap-20240605-0003/", + "https://security.netapp.com/advisory/ntap-20240605-0003", + "https://github.com/mde/ejs/commit/e469741dca7df2eb400199e1cdb74621e3f89aa5", + "https://github.com/mde/ejs/compare/v3.1.9...v3.1.10", + "https://github.com/advisories/GHSA-ghr5-ch3p-vcr6", + "https://nvd.nist.gov/vuln/detail/CVE-2024-33883" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "ejs", + "version": "3.1.6" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Insufficient input validation in EJS may lead to prototype pollution.", + "details": "[Embedded JavaScript templates](https://github.com/mde/ejs), also known as `EJS`, is one of the most popular Node.js templating engines, which is compiled with the Express JS view system.\n\nA prototype pollution gadget within the EJS template engine could potentially be leveraged by attackers to achieve remote code execution or DoS via prototype pollution.\n\n```\nfunction Template(text, opts) {\n opts = opts || utils.createNullProtoObjWherePossible();\n```\n\nWhen checking for the presence of a property within an object variable, the lookup scope isn't explicitly defined. In JavaScript, the absence of a defined lookup scope prompts a search up to the root prototype (`Object.prototype`). This could potentially be under the control of an attacker if another prototype pollution vulnerability is present within the application.\n\nIf the application server is using the EJS as the backend template engine, and there is another prototype pollution vulnerability in the application, then the attacker could leverage the found gadgets in the EJS template engine to escalate the prototype pollution to remote code execution or DoS.\n\nThe following code will execute a command on the server by polluting `opts.escapeFunction`:\n \n```\nconst express = require('express');\nconst app = express();\nconst port = 8008;\nconst ejs = require('ejs');\n\n// Set EJS as the view engine\napp.set('view engine', 'ejs');\n\napp.get('/', (req, res) =\u003e {\n \n const data = {title: 'Welcome', message: 'Hello'};\n\n // Sample EJS template string\n const templateString = `\u003chtml\u003e\u003chead\u003e\u003ctitle\u003e\u003c%= title %\u003e\u003c/title\u003e\u003c/head\u003e\u003cbody\u003e\u003ch1\u003e\u003c%= message %\u003e\u003c/h1\u003e\u003c/body\u003e\u003c/html\u003e`;\n\n const { exec } = require('child_process');\n\n function myFunc() {\n exec('bash -c \"echo 123\"', (error, stdout, stderr) =\u003e {\n if (error) {\n console.error(`exec error: ${error}`);\n return;\n }\n if (stderr){\n console.log(`stderr : ${stderr}`);\n return;\n }\n // Handle success\n console.log(`Command executed successfully. Output: ${stdout}`);\n });\n }\n\n const options = {client:false};\n\n Object.prototype.escapeFunction = myFunc;\n \n const compiledTemplate = ejs.compile(templateString, options);\n const renderedHtml = compiledTemplate(data);\n res.send(renderedHtml);\n});\n\n// Start the server\napp.listen(port, () =\u003e {\n console.log(`Server is running on http://localhost:${port}`);\n});\n```", + "severityReasons": [ + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "Attackers can only leverage this vulnerability when the application server is using the EJS as the backend template engine. Moreover, there must be a second prototype pollution vulnerability in the application.", + "isPositive": true + }, + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "CVSS does not take into account the unlikely prerequisites necessary for exploitation.", + "isPositive": true + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "A prototype pollution attack allows the attacker to inject new properties into all JavaScript objects.\nTherefore, the impact of a prototype pollution attack depends on the way the JavaScript code uses any object properties after the attack is triggered.\nUsually, a DoS attack is possible since invalid properties quickly lead to an exception being thrown. In more severe cases, RCE may be achievable." + } + ] + } + }, + { + "severity": "Medium", + "impactedPackageName": "lodash", + "impactedPackageVersion": "4.17.0", + "impactedPackageType": "npm", + "components": [ + { + "name": "lodash", + "version": "4.17.0", + "location": { + "file": "/Users/user/ejs-frog-demo/package.json" + } + } + ], + "summary": "Lodash versions prior to 4.17.21 are vulnerable to Regular Expression Denial of Service (ReDoS) via the toNumber, trim and trimEnd functions.", + "applicable": "Not Applicable", + "fixedVersions": [ + "[4.17.21]" + ], + "cves": [ + { + "id": "CVE-2020-28500", + "cvssV2": "5.0", + "cvssV3": "5.3", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `lodash.trim` with external input to its 1st (`string`) argument.\n* `lodash.toNumber` with external input to its 1st (`value`) argument.\n* `lodash.trimEnd` with external input to its 1st (`string`) argument." + } + } + ], + "issueId": "XRAY-140562", + "references": [ + "https://cert-portal.siemens.com/productcert/pdf/ssa-637483.pdf", + "https://github.com/lodash/lodash/commit/c4847ebe7d14540bb28a8b932a9ce1b9ecbfee1a", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARS-1074894", + "https://github.com/lodash/lodash/blob/npm/trimEnd.js%23L8", + "https://security.netapp.com/advisory/ntap-20210312-0006/", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSNPM-1074893", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWER-1074892", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28500", + "https://www.oracle.com/security-alerts/cpujul2022.html", + "https://snyk.io/vuln/SNYK-JAVA-ORGWEBJARSBOWERGITHUBLODASH-1074895", + "https://github.com/lodash/lodash/pull/5065/commits/02906b8191d3c100c193fe6f7b27d1c40f200bb7", + "https://www.oracle.com/security-alerts/cpujan2022.html", + "https://github.com/advisories/GHSA-29mw-wpgm-hmr9", + "https://github.com/lodash/lodash/pull/5065", + "https://snyk.io/vuln/SNYK-JAVA-ORGFUJIONWEBJARS-1074896", + "https://snyk.io/vuln/SNYK-JS-LODASH-1018905" + ], + "impactPaths": [ + [ + { + "name": "froghome", + "version": "1.0.0" + }, + { + "name": "lodash", + "version": "4.17.0" + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "ReDoS in lodash could lead to a denial of service when handling untrusted strings.", + "details": "JavaScript-based applications that use [lodash](https://github.com/lodash/lodash) and specifically the [_.toNumber](https://lodash.com/docs/4.17.15#toNumber), [_.trim](https://lodash.com/docs/4.17.15#trim) and [_.trimEnd](https://lodash.com/docs/4.17.15#trimEnd) functions, could be vulnerable to DoS (Denial of Service) through a faulty regular expression that introduces a ReDoS (Regular Expression DoS) vulnerability. This vulnerability is only triggered if untrusted user input flows into these vulnerable functions and the attacker can supply arbitrary long strings (over 50kB) that contain whitespaces. \n\nOn a modern Core i7-based system, calling the vulnerable functions with a 50kB string could take between 2 to 3 seconds to execute and 4.5 minutes for a longer 500kB string. The fix improved the regular expression performance so it took only a few milliseconds on the same Core i7-based system. This vulnerability is easily exploitable as all is required is to build a string that triggers it as can be seen in this PoC reproducing code - \n\n```js\nvar untrusted_user_input_50k = \"a\" + ' '.repeat(50000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_50k); // should take a few seconds to run\nvar untrusted_user_input_500k = \"a\" + ' '.repeat(500000) + \"z\"; // assume this is provided over the network\nlo.trimEnd(untrusted_user_input_500k); // should take a few minutes to run\n```", + "severityReasons": [ + { + "name": "The issue has an exploit published", + "description": "Public exploit demonstrated ReDoS" + }, + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "Exploitation depends on parsing user input by the `.toNumber`, `.trim` or `.trimEnd` `lodash` functions, and requires the input to contain whitespaces and be very long (over 50KB)", + "isPositive": true + } + ], + "remediation": "##### Deployment mitigations\n\nTrim untrusted strings based on size before providing it to the vulnerable functions by using the `substring` function to with a fixed maximum size like so - ```js untrusted_user_input.substring(0, max_string_size_less_than_50kB); ```" + } + } + ], + "licensesViolations": null, + "licenses": null, + "operationalRiskViolations": null, + "secrets": [ + { + "severity": "Medium", + "file": "server.js", + "startLine": 11, + "startColumn": 14, + "endLine": 11, + "endColumn": 24, + "snippet": "Sqc************", + "finding": "Secret keys were found", + "scannerDescription": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\u0026tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + { + "severity": "Medium", + "file": "fake-creds.txt", + "startLine": 2, + "startColumn": 1, + "endLine": 2, + "endColumn": 11, + "snippet": "Sqc************", + "finding": "Secret keys were found", + "scannerDescription": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\u0026tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + { + "severity": "Medium", + "file": "fake-creds.txt", + "startLine": 3, + "startColumn": 1, + "endLine": 3, + "endColumn": 11, + "snippet": "gho************", + "finding": "Secret keys were found", + "scannerDescription": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\u0026tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + } + ], + "iacViolations": null, + "sastViolations": [ + { + "severity": "High", + "file": "server.js", + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 37, + "snippet": "req.query", + "finding": "Template Object Injection", + "scannerDescription": "\n### Overview\nTemplate Object Injection (TOI) is a vulnerability that can occur in\nweb applications that use template engines to render dynamic content.\nTemplate engines are commonly used to generate HTML pages, emails, or\nother types of documents that include variable data. TOI happens when\nuntrusted user input is included as part of the template rendering\nprocess, and the template engine evaluates the input as a code\nexpression, leading to potential code injection or data tampering\nattacks. To prevent TOI vulnerabilities, it's important to sanitize and\nvalidate all user input that is used as part of the template rendering\nprocess.\n\n### Query operation\nIn this query we look for user inputs that flow directly to a\nrequest render.\n\n### Vulnerable example\n```javascript\nvar app = require('express')();\napp.set('view engine', 'hbs');\n\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n res.render('template', bodyParameter);\n});\n```\nIn this example, a user-provided data is injected directly into the\n`render` command, leading to potential code injection or data\ntampering attacks.\n\n### Remediation\n```diff\n+ const sanitizeHtml = require('sanitize-html');\nvar app = require('express')();\napp.set('view engine', 'hbs');\napp.use(require('body-parser').json());\napp.use(require('body-parser').urlencoded({ extended: false }));\napp.post('/path', function(req, res) {\n var bodyParameter = req.body.bodyParameter;\n var queryParameter = req.query.queryParameter;\n\n- res.render('template', bodyParameter);\n+ res.render('template', sanitizeHtml(bodyParameter));\n});\nUsing `sanitize-html`, the user-provided data is sanitized, before\nrendering to the response.\n```\n", + "codeFlow": [ + [ + { + "file": "server.js", + "startLine": 21, + "startColumn": 23, + "endLine": 21, + "endColumn": 26, + "snippet": "req" + }, + { + "file": "server.js", + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 31, + "snippet": "req" + }, + { + "file": "server.js", + "startLine": 26, + "startColumn": 28, + "endLine": 26, + "endColumn": 37, + "snippet": "req.query" + } + ] + ] + }, + { + "severity": "Low", + "file": "server.js", + "startLine": 8, + "startColumn": 11, + "endLine": 8, + "endColumn": 20, + "snippet": "express()", + "finding": "Express Not Using Helmet", + "scannerDescription": "\n### Overview\nHelmet library should be used when using Express in order to properly configure\nHTTP header settings to mitigate a range of well-known vulnerabilities.\n\n### Remediation\n```javascript\nconst helmet = require(\"helmet\");\nconst app = express()\n\napp.use(helmet())\n```\n\n### References\n[Best practices for Express](https://expressjs.com/en/advanced/best-practice-security.html)\n" + }, + { + "severity": "Low", + "file": "public/js/bootstrap.js", + "startLine": 136, + "startColumn": 22, + "endLine": 136, + "endColumn": 35, + "snippet": "Math.random()", + "finding": "Use of Insecure Random", + "scannerDescription": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n" + }, + { + "severity": "Low", + "file": "public/js/bootstrap.bundle.js", + "startLine": 135, + "startColumn": 22, + "endLine": 135, + "endColumn": 35, + "snippet": "Math.random()", + "finding": "Use of Insecure Random", + "scannerDescription": "\n### Overview\nA use of insecure random vulnerability is a type of security flaw that is\ncaused by the use of inadequate or predictable random numbers in a program\nor system. Random numbers are used in many security-related applications,\nsuch as generating cryptographic keys and if the numbers are not truly\nrandom, an attacker may be able to predict or recreate them, potentially\ncompromising the security of the system.\n\n### Vulnerable example\n```javascript\nvar randomNum = Math.random();\n```\n`Math.random` is not secured, as it creates predictable random numbers.\n\n### Remediation\n```diff\nvar randomNum = crypto.randomInt(0, 100)\n```\n`crypto.randomInt` is secured, and creates much less predictable random\nnumbers.\n" + } + ], + "errors": null +} diff --git a/tests/testdata/output/audit/audit_summary.json b/tests/testdata/output/audit/audit_summary.json new file mode 100644 index 00000000..58a813af --- /dev/null +++ b/tests/testdata/output/audit/audit_summary.json @@ -0,0 +1,67 @@ +{ + "scans": [ + { + "target": "", + "vulnerabilities": { + "sca": { + "scan_ids": [ + "711851ce-68c4-4dfd-7afb-c29737ebcb96" + ], + "security": { + "Critical": { + "Applicable": 1, + "Not Applicable": 2 + }, + "High": { + "Not Applicable": 2 + }, + "Medium": { + "Not Applicable": 3, + "Not Covered": 3 + }, + "Unknown": { + "Not Covered": 1 + } + } + }, + "iac": {}, + "secrets": { + "Medium": { + "": 3 + } + }, + "sast": { + "High": { + "": 1 + }, + "Low": { + "": 3 + } + } + }, + "violations": { + "watches": [ + "Security_watch_1" + ], + "sca": { + "scan_ids": [ + "711851ce-68c4-4dfd-7afb-c29737ebcb96" + ], + "security": { + "Critical": { + "Applicable": 1, + "Not Applicable": 2 + }, + "High": { + "Not Applicable": 2 + }, + "Medium": { + "Not Applicable": 3, + "Not Covered": 3 + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/tests/testdata/output/dockerscan/docker_results.json b/tests/testdata/output/dockerscan/docker_results.json new file mode 100644 index 00000000..0ffdd2a0 --- /dev/null +++ b/tests/testdata/output/dockerscan/docker_results.json @@ -0,0 +1,909 @@ +{ + "xray_version": "3.104.8", + "jas_entitled": true, + "command_type": "docker_image", + "targets": [ + { + "target": "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210535-1985298017/image.tar", + "name": "platform.jfrog.io/swamp-docker/swamp:latest", + "technology": "oci", + "sca_scans": { + "xray_scan": [ + { + "scan_id": "27da9106-88ea-416b-799b-bc7d15783473", + "vulnerabilities": [ + { + "cves": [ + { + "cve": "CVE-2024-6119" + } + ], + "summary": "Issue summary: Applications performing certificate name checks (e.g., TLS\nclients checking server certificates) may attempt to read an invalid memory\naddress resulting in abnormal termination of the application process.\n\nImpact summary: Abnormal termination of an application can a cause a denial of\nservice.\n\nApplications performing certificate name checks (e.g., TLS clients checking\nserver certificates) may attempt to read an invalid memory address when\ncomparing the expected name with an `otherName` subject alternative name of an\nX.509 certificate. This may result in an exception that terminates the\napplication program.\n\nNote that basic certificate chain validation (signatures, dates, ...) is not\naffected, the denial of service can occur only when the application also\nspecifies an expected DNS name, Email address or IP address.\n\nTLS servers rarely solicit client certificates, and even when they do, they\ngenerally don't perform a name check against a reference identifier (expected\nidentity), but rather extract the presented identity after checking the\ncertificate chain. So TLS servers are generally not affected and the severity\nof the issue is Moderate.\n\nThe FIPS modules in 3.3, 3.2, 3.1 and 3.0 are not affected by this issue.", + "severity": "Unknown", + "components": { + "deb://debian:bookworm:libssl3:3.0.13-1~deb12u1": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595/sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "full_path": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + }, + { + "component_id": "deb://debian:bookworm:libssl3:3.0.13-1~deb12u1", + "full_path": "libssl3:3.0.13-1~deb12u1" + } + ] + ] + }, + "deb://debian:bookworm:openssl:3.0.13-1~deb12u1": { + "fixed_versions": [ + "[3.0.14-1~deb12u2]" + ], + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595/sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "full_path": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + }, + { + "component_id": "deb://debian:bookworm:openssl:3.0.13-1~deb12u1", + "full_path": "openssl:3.0.13-1~deb12u1" + } + ] + ] + } + }, + "issue_id": "XRAY-632747", + "references": [ + "https://openssl-library.org/news/secadv/20240903.txt", + "https://github.com/openssl/openssl/commit/621f3729831b05ee828a3203eddb621d014ff2b2", + "https://github.com/openssl/openssl/commit/05f360d9e849a1b277db628f1f13083a7f8dd04f", + "https://security-tracker.debian.org/tracker/CVE-2024-6119", + "https://github.com/openssl/openssl/commit/7dfcee2cd2a63b2c64b9b4b0850be64cb695b0a0", + "https://github.com/openssl/openssl/commit/06d1dc3fa96a2ba5a3e22735a033012aadc9f0d6" + ], + "extended_information": { + "short_description": "Out of bounds read in OpenSSL clients can lead to denial of service when using non-default TLS verification options and connecting to malicious TLS servers", + "jfrog_research_severity": "Medium", + "jfrog_research_severity_reasons": [ + { + "name": "The issue has an exploit published", + "description": "The fix commit contains PoC certificates that trigger the denial of service issue" + }, + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The attacker must make the victim client connect to their malicious TLS server, in order to serve the malformed TLS certificate. The victim client must use OpenSSL and must enable non-default certificate verification options, either -\n\n* DNS verification - by using `X509_VERIFY_PARAM_set1_host` or `X509_check_host`\n* Email verification - by using ` X509_VERIFY_PARAM_set1_email` or `X509_check_email`", + "is_positive": true + }, + { + "name": "The issue cannot result in a severe impact (such as remote code execution)", + "description": "Denial of service of a TLS clients only. This out of bounds read cannot lead to data disclosure.", + "is_positive": true + } + ] + } + }, + { + "cves": [ + { + "cve": "CVE-2024-38428", + "cvss_v3_score": "9.1", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N", + "cwe": [ + "CWE-436" + ], + "cwe_details": { + "CWE-436": { + "name": "Interpretation Conflict", + "description": "Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state." + } + } + } + ], + "summary": "url.c in GNU Wget through 1.24.5 mishandles semicolons in the userinfo subcomponent of a URI, and thus there may be insecure behavior in which data that was supposed to be in the userinfo subcomponent is misinterpreted to be part of the host subcomponent.", + "severity": "Critical", + "components": { + "deb://debian:bookworm:wget:1.21.3-1+b1": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595/sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "full_path": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + }, + { + "component_id": "deb://debian:bookworm:wget:1.21.3-1+b1", + "full_path": "wget:1.21.3-1+b1" + } + ] + ] + } + }, + "issue_id": "XRAY-606103", + "references": [ + "https://git.savannah.gnu.org/cgit/wget.git/commit/?id=ed0c7c7e0e8f7298352646b2fd6e06a11e242ace", + "https://lists.gnu.org/archive/html/bug-wget/2024-06/msg00005.html", + "https://security-tracker.debian.org/tracker/CVE-2024-38428" + ] + }, + { + "summary": "Malicious package cors.js for Node.js", + "severity": "Critical", + "components": { + "npm://cors.js:0.0.1-security": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f/sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar", + "full_path": "sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar" + }, + { + "component_id": "npm://cors.js:0.0.1-security", + "full_path": "usr/src/app/node_modules/cors.js/package.json" + } + ] + ] + } + }, + "issue_id": "XRAY-264729", + "references": [ + "https://registry.npmjs.com" + ], + "extended_information": { + "short_description": "Malicious package cors.js for Node.js", + "full_description": "The package cors.js for Node.js contains malicious code that installs a persistent connectback shell. The package is typosquatting the popular `cors` package. When installed, the package opens a connectback shell to the hardcoded host `107.175.32.229` on TCP port 56173. The malicious payload achieves persistency by installing a cron job that repeats every 10 seconds - `*/10 * * * * *`", + "jfrog_research_severity": "Critical", + "remediation": "As with any malware, the malicious package must be completely removed, and steps must be taken care to remediate the damage that was done by the malicious package -\n\n##### Removing the malicious package\n\nRun `npm uninstall cors.js`\n\n##### Refreshing stolen credentials\n\nMany malicious packages steal stored user credentials, focusing on the following -\n\n* [Browser autocomplete](https://jfrog.com/blog/malicious-pypi-packages-stealing-credit-cards-injecting-code/) data, such as saved passwords and credit cards\n* [Environment variables](https://jfrog.com/blog/malicious-npm-packages-are-after-your-discord-tokens-17-new-packages-disclosed/) passed to the malicious code\n* [Stored Discord tokens](https://jfrog.com/blog/malicious-npm-packages-are-after-your-discord-tokens-17-new-packages-disclosed/)\n* AWS / GitHub credentials stored in cleartext files\n\nIt is highly recommended to change or revoke data that is stored in the infected machine at those locations\n\n##### Stopping malicious processes\n\nMany malicious packages start malicious processes such as [connectback shells](https://jfrog.com/blog/jfrog-discloses-3-remote-access-trojans-in-pypi/) or crypto-miners. Search for any unfamiliar processes that consume a large amount of CPU or a large amount of network traffic, and stop them. On Windows, this can be facilitated with [Sysinternals Process Explorer](https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer).\n\n##### Removing installed backdoors\n\nMany malicious packages install themselves as a [persistent backdoor](https://jfrog.com/blog/npm-supply-chain-attack-targets-german-based-companies/), in order to guarantee the malicious code survives a reboot. Search for any unfamiliar binaries set to be run on startup, and remove them. On Windows, this can be facilitated with [Sysinternals Autoruns](https://docs.microsoft.com/en-us/sysinternals/downloads/autoruns).\n\n##### Defining an Xray policy that blocks downloads of Artifacts with malicious packages\n\nIt is possible to [create an Xray policy](https://www.jfrog.com/confluence/display/JFROG/Creating+Xray+Policies+and+Rules) that will not allow artifacts with identified malicious packages to be downloaded from Artifactory. To create such a policy, add a new `Security` policy and set `Minimal Severity` to `Critical`. Under `Automatic Actions` check the `Block Download` action.\n\n##### Contacting the JFrog Security Research team for additional information\n\nOptionally, if you are unsure of the full impact of the malicious package and wish to get more details, the JFrog Security Research team can help you assess the potential damage from the installed malicious package.\n\nPlease contact us at research@jfrog.com with details of the affected artifact and the name of the identified malicious package." + } + }, + { + "cves": [ + { + "cve": "CVE-2024-45490", + "cvss_v3_score": "9.8", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "cwe": [ + "CWE-611" + ], + "cwe_details": { + "CWE-611": { + "name": "Improper Restriction of XML External Entity Reference", + "description": "The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output." + } + } + } + ], + "summary": "An issue was discovered in libexpat before 2.6.3. xmlparse.c does not reject a negative length for XML_ParseBuffer.", + "severity": "Critical", + "components": { + "deb://debian:bookworm:libexpat1:2.5.0-1": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1/sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "full_path": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + }, + { + "component_id": "deb://debian:bookworm:libexpat1:2.5.0-1", + "full_path": "libexpat1:2.5.0-1" + } + ] + ] + } + }, + "issue_id": "XRAY-632613", + "references": [ + "https://github.com/libexpat/libexpat/issues/887", + "https://security-tracker.debian.org/tracker/CVE-2024-45490", + "https://github.com/libexpat/libexpat/pull/890" + ] + }, + { + "cves": [ + { + "cve": "CVE-2024-45492", + "cvss_v3_score": "9.8", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "cwe": [ + "CWE-190" + ], + "cwe_details": { + "CWE-190": { + "name": "Integer Overflow or Wraparound", + "description": "The product performs a calculation that can produce an integer overflow or wraparound, when the logic assumes that the resulting value will always be larger than the original value. This can introduce other weaknesses when the calculation is used for resource management or execution control.", + "categories": [ + { + "category": "2023 CWE Top 25", + "rank": "14" + } + ] + } + } + } + ], + "summary": "An issue was discovered in libexpat before 2.6.3. nextScaffoldPart in xmlparse.c can have an integer overflow for m_groupSize on 32-bit platforms (where UINT_MAX equals SIZE_MAX).", + "severity": "Critical", + "components": { + "deb://debian:bookworm:libexpat1:2.5.0-1": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1/sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "full_path": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + }, + { + "component_id": "deb://debian:bookworm:libexpat1:2.5.0-1", + "full_path": "libexpat1:2.5.0-1" + } + ] + ] + } + }, + "issue_id": "XRAY-632612", + "references": [ + "https://github.com/libexpat/libexpat/issues/889", + "https://security-tracker.debian.org/tracker/CVE-2024-45492", + "https://github.com/libexpat/libexpat/pull/892" + ] + }, + { + "cves": [ + { + "cve": "CVE-2023-51767", + "cvss_v3_score": "7.0", + "cvss_v3_vector": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", + "cwe": [ + "NVD-CWE-Other" + ] + } + ], + "summary": "OpenSSH through 9.6, when common types of DRAM are used, might allow row hammer attacks (for authentication bypass) because the integer value of authenticated in mm_answer_authpassword does not resist flips of a single bit. NOTE: this is applicable to a certain threat model of attacker-victim co-location in which the attacker has user privileges.", + "severity": "Low", + "components": { + "deb://debian:bookworm:openssh-client:1:9.2p1-2+deb12u3": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1/sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "full_path": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + }, + { + "component_id": "deb://debian:bookworm:openssh-client:1:9.2p1-2+deb12u3", + "full_path": "openssh-client:1:9.2p1-2+deb12u3" + } + ] + ] + } + }, + "issue_id": "XRAY-585612", + "references": [ + "https://arxiv.org/abs/2309.02545", + "https://github.com/openssh/openssh-portable/blob/8241b9c0529228b4b86d88b1a6076fb9f97e4a99/monitor.c#L878", + "https://github.com/openssh/openssh-portable/blob/8241b9c0529228b4b86d88b1a6076fb9f97e4a99/auth-passwd.c#L77", + "https://bugzilla.redhat.com/show_bug.cgi?id=2255850", + "https://security-tracker.debian.org/tracker/CVE-2023-51767", + "https://ubuntu.com/security/CVE-2023-51767", + "https://security.netapp.com/advisory/ntap-20240125-0006/", + "https://access.redhat.com/security/cve/CVE-2023-51767" + ], + "extended_information": { + "short_description": "The RowHammer fault injection attack can theoretically lead to local authentication bypass in OpenSSH.", + "full_description": "[OpenSSH](https://www.openssh.com/) is a popular open-source implementation of the SSH (Secure Shell) protocol, providing encrypted communication over a network.\nIt was discovered that the OpenSSH authentication logic can be susceptible in some cases to a side-channel fault injection attack. The attack can theoretically be carried out by a local attacker which eventually bypass OpenSSH authentication mechanism.\n\nThis vulnerability currently lacks widely known published exploits, and its exploitation is considered highly complex. The intricacies of the attack, combined with the absence of well-documented exploits, contribute to the difficulty in achieving successful exploitation. Furthermore, it's essential to note that the susceptibility to this vulnerability is hardware-dependent, and the success of an attack relies on probabilities associated with the specific hardware configuration. \n\nThe vulnerability is theoretically exploitable by several different ways, the only two published ways are:\n\nIn the OpenSSH function `mm_answer_authpassword()`, a stack variable `authenticated`, is assigned to the value of the function `auth_password()` which returns 1/0 and then returned. If the value of `authenticated` is 1, the SSH connection will be established. Since `authenticated` is stored on the stack, therefore in DRAM, a local attacker could flip this 32-bit integer least significant bit, thus, bypass authentication.\n\nAnother possible exploit is the `result` stack variable in `auth_password()` function. It is initialized to 0 and set to 1 if the password is correct. \nSimilarly to the previous method, this attack requires a single bit flip of the `result` variable in order for the function to return 1 and bypass the authentication.\n\nAttackers can trigger the vulnerability via a RowHammer fault injection. The Rowhammer bug is a hardware reliability issue in which an attacker repeatedly accesses (hammers) DRAM cells to cause unauthorized changes in physically adjacent memory locations.\nSimply put:\n\n* A specific register value(`authenticated`/`result` value) is pushed onto the stack during program execution. \n* The stack, where the register value is stored, is identified to be located in a memory row susceptible to bit flips (flippable row) due to the RowHammer vulnerability in DRAM.\n* The attacker performs a series of rapid and repeated memory accesses to the adjacent rows of the flippable row in the DRAM. This repeated access exploits the RowHammer vulnerability, inducing bit flips in the targeted flippable row.\n* Due to the RowHammer effect, bit flips occur in the flippable row, potentially corrupting the data stored there.\n* After inducing bit flips in the flippable row, the attacker manipulates the program's control flow to pop the corrupted value from the stack into a register.\n* The register now holds a value that has been corrupted through the RowHammer attack. Now the `authenticated`/`result` variables hold this corrupted value thus it can lead to authentication bypass, as it may impact the control flow in a way advantageous to the attacker.", + "jfrog_research_severity": "Low", + "jfrog_research_severity_reasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The vulnerability depends on the OS and hardware. It was only evaluated in one test environment, therefore results for other conditions might differ. The attacker must be extremely familiar with the details of the exploited system (ex. know the exact hardware which is running the OS).", + "is_positive": true + }, + { + "name": "The issue can only be exploited by an attacker that can execute code on the vulnerable machine (excluding exceedingly rare circumstances)", + "is_positive": true + }, + { + "name": "No high-impact exploit or technical writeup were published, and exploitation of the issue with high impact is either non-trivial or completely unproven", + "description": "Exploitation is extremely non-trivial (even theoretically), no public exploits have been published.", + "is_positive": true + }, + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The vulnerability's attack complexity is significantly higher than what the CVSS represents.", + "is_positive": true + } + ] + } + }, + { + "cves": [ + { + "cve": "CVE-2011-3374", + "cvss_v2_score": "4.3", + "cvss_v2_vector": "CVSS:2.0/AV:N/AC:M/Au:N/C:N/I:P/A:N", + "cvss_v3_score": "3.7", + "cvss_v3_vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N", + "cwe": [ + "CWE-347" + ], + "cwe_details": { + "CWE-347": { + "name": "Improper Verification of Cryptographic Signature", + "description": "The product does not verify, or incorrectly verifies, the cryptographic signature for data." + } + } + } + ], + "summary": "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.", + "severity": "Low", + "components": { + "deb://debian:bookworm:apt:2.6.1": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e/sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + "full_path": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + }, + { + "component_id": "deb://debian:bookworm:apt:2.6.1", + "full_path": "apt:2.6.1" + } + ] + ] + }, + "deb://debian:bookworm:libapt-pkg6.0:2.6.1": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e/sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + "full_path": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + }, + { + "component_id": "deb://debian:bookworm:libapt-pkg6.0:2.6.1", + "full_path": "libapt-pkg6.0:2.6.1" + } + ] + ] + } + }, + "issue_id": "XRAY-34417", + "references": [ + "https://people.canonical.com/~ubuntu-security/cve/2011/CVE-2011-3374.html", + "https://seclists.org/fulldisclosure/2011/Sep/221", + "https://ubuntu.com/security/CVE-2011-3374", + "https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=642480", + "https://access.redhat.com/security/cve/cve-2011-3374", + "https://snyk.io/vuln/SNYK-LINUX-APT-116518", + "https://security-tracker.debian.org/tracker/CVE-2011-3374" + ], + "extended_information": { + "short_description": "Improper signature validation in apt-key may enable Man-in-the-Middle attacks and result in code execution.", + "full_description": "`apt-key` is [`apt`](https://github.com/Debian/apt)'s key management utility, and is used to manage the keys that are used by `apt` to authenticate packages.\n\nA vulnerability in `apt-key`'s `net-update` function exists, in which [`GPG`](https://www.gnupg.org/) keys, that are used for signing packages and validating their authenticity, aren't validated correctly. The `net-update` function pulls the signing keys that should be added from an insecure location (`http://...`), exposing it to a Man-in-the-Middle attack in which malicious signing keys could be added to the system's keyring. This issue happens due to a vulnerability in the `add_keys_with_veirfy_against_master_keyring()` function, which allows adding signing keys without proper signature validation. \n\nThis vulnerability then potentially allows a malicious actor to perform a Man-in-the-Middle attack on a target, by making it validate malicious packages that were signed with the `GPG` signing key used by the attacker. Effectively, this means that `apt` can be duped to install malicious services and daemons with root privileges.\n\nThe conditions for this vulnerability to be applicable:\n \n1. A valid URI should be configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`. This is the URI that an attacker would need to target in a Man In The Middle attack.\n2. The command `apt-key net-update` should be executed on the affected system, or alternatively `apt.auth.net_update()` function from [python-apt](https://pypi.org/project/python-apt/) Python module should be called. This is for the malicious keys download.\n3. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine.\n\nDo note that `apt-key` is **deprecated** and shouldn't be used, and in most Debian versions `ARCHIVE_KEYRING_URI` is not defined, making this vulnerability unexploitable in most Debian systems.", + "jfrog_research_severity": "High", + "jfrog_research_severity_reasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The conditions for this vulnerability to be applicable:\n \n1. A valid URI should be configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n2. The command `apt-key net-update` should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the python-apt Python module should be called. This is for the malicious keys download.\n3. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine.", + "is_positive": true + }, + { + "name": "The issue can be exploited by attackers over the network", + "description": "This vulnerability is remotely exploitable when the applicability conditions apply." + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Remote code execution is possible when the applicability conditions apply." + }, + { + "name": "The issue has an exploit published", + "description": "The reporter of this issue has provided a GPG key that can be used for an actual attack, as well as a simple PoC example." + } + ], + "remediation": "##### Deployment mitigations\n\n* Dot not execute `apt-key` command, as it is deprecated.\n* Remove the URI configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`." + } + }, + { + "cves": [ + { + "cve": "CVE-2024-4741" + } + ], + "summary": "CVE-2024-4741", + "severity": "Unknown", + "components": { + "deb://debian:bookworm:libssl3:3.0.13-1~deb12u1": { + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595/sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "full_path": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + }, + { + "component_id": "deb://debian:bookworm:libssl3:3.0.13-1~deb12u1", + "full_path": "libssl3:3.0.13-1~deb12u1" + } + ] + ] + }, + "deb://debian:bookworm:openssl:3.0.13-1~deb12u1": { + "fixed_versions": [ + "[3.0.14-1~deb12u1]" + ], + "impact_paths": [ + [ + { + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest" + }, + { + "component_id": "generic://sha256:f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595/sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "full_path": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + }, + { + "component_id": "deb://debian:bookworm:openssl:3.0.13-1~deb12u1", + "full_path": "openssl:3.0.13-1~deb12u1" + } + ] + ] + } + }, + "issue_id": "XRAY-603657", + "references": [ + "https://security-tracker.debian.org/tracker/CVE-2024-4741" + ] + } + ], + "component_id": "docker://platform.jfrog.io/swamp-docker/swamp:latest", + "package_type": "oci", + "status": "completed" + } + ] + }, + "jas_scans": { + "contextual_analysis": [ + { + "tool": { + "driver": { + "informationUri": "https://jfrog.com/help/r/jfrog-security-documentation/jfrog-advanced-security", + "name": "JFrog Applicability Scanner", + "rules": [ + { + "id": "applic_CVE-2024-6119", + "name": "CVE-2024-6119", + "shortDescription": { + "text": "Scanner for CVE-2024-6119" + }, + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `X509_VERIFY_PARAM_set1_email`\n\n- `X509_check_email`\n\n- `X509_VERIFY_PARAM_set1_host`\n\n- `X509_check_host`", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `X509_VERIFY_PARAM_set1_email`\n\n- `X509_check_email`\n\n- `X509_VERIFY_PARAM_set1_host`\n\n- `X509_check_host`" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2024-45490", + "name": "CVE-2024-45490", + "shortDescription": { + "text": "Scanner for CVE-2024-45490" + }, + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `XML_Parse()`\n- `XML_ParseBuffer()`\n\nAn additional condition, which the scanner currently does not check, is that the `len` parameter which is passed to those functions is user-controlled.", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `XML_Parse()`\n- `XML_ParseBuffer()`\n\nAn additional condition, which the scanner currently does not check, is that the `len` parameter which is passed to those functions is user-controlled." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2024-38428", + "name": "CVE-2024-38428", + "shortDescription": { + "text": "Scanner for CVE-2024-38428" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "undetermined", + "conclusion": "private" + } + }, + { + "id": "applic_CVE-2024-45492", + "name": "CVE-2024-45492", + "shortDescription": { + "text": "Scanner for CVE-2024-45492" + }, + "fullDescription": { + "text": "The scanner checks whether the current binary was compiled with 32-bit architecture and if any of the vulnerable functions are called:\n\n- `XML_ParseBuffer()`\n- `XML_Parse()`\n\nNote - the vulnerability occurs when certain inputs are passed to those functions.", + "markdown": "The scanner checks whether the current binary was compiled with 32-bit architecture and if any of the vulnerable functions are called:\n\n- `XML_ParseBuffer()`\n- `XML_Parse()`\n\nNote - the vulnerability occurs when certain inputs are passed to those functions." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2023-51767", + "name": "CVE-2023-51767", + "shortDescription": { + "text": "Scanner for CVE-2023-51767" + }, + "fullDescription": { + "text": "The CVE is always applicable.\n\nNote - The vulnerability is hardware-dependent.", + "markdown": "The CVE is always applicable.\n\nNote - The vulnerability is hardware-dependent." + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative" + } + }, + { + "id": "applic_CVE-2011-3374", + "name": "CVE-2011-3374", + "shortDescription": { + "text": "Scanner for CVE-2011-3374" + }, + "fullDescription": { + "text": "The scanner checks if the vulnerable variable `ARCHIVE_KEYRING_URI` in `/usr/bin/apt-key` is not empty and not commented out. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n\nThe below prerequisites are also crucial for exploitability but are not checked in the scanner:\n\n1. The command apt-key net-update should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the `python-apt` Python module should be called. This is for the malicious keys download.\n\n2. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine.", + "markdown": "The scanner checks if the vulnerable variable `ARCHIVE_KEYRING_URI` in `/usr/bin/apt-key` is not empty and not commented out. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n\nThe below prerequisites are also crucial for exploitability but are not checked in the scanner:\n\n1. The command apt-key net-update should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the `python-apt` Python module should be called. This is for the malicious keys download.\n\n2. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine." + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive", + "security-severity": "6.9" + } + }, + { + "id": "applic_CVE-2024-4741", + "name": "CVE-2024-4741", + "shortDescription": { + "text": "Scanner for CVE-2024-4741" + }, + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `SSL_free_buffers` is called.", + "markdown": "The scanner checks whether the vulnerable function `SSL_free_buffers` is called." + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + } + ], + "version": "1.0" + } + }, + "invocations": [ + { + "arguments": [ + "/Users/user/.jfrog/dependencies/analyzerManager/jas_scanner/jas_scanner", + "scan", + "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210780-681556384/Applicability_1726210780/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210535-1985298017/image.tar" + } + } + ], + "results": [ + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2024-4741", + "message": { + "text": "References to the vulnerable functions were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///usr/local/bin/node" + }, + "region": { + "snippet": { + "text": "" + } + } + } + } + ] + }, + { + "ruleId": "applic_CVE-2024-45490", + "kind": "pass", + "message": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `XML_Parse()`\n- `XML_ParseBuffer()`\n\nAn additional condition, which the scanner currently does not check, is that the `len` parameter which is passed to those functions is user-controlled." + } + }, + { + "ruleId": "applic_CVE-2011-3374", + "kind": "pass", + "message": { + "text": "The scanner checks if the vulnerable variable `ARCHIVE_KEYRING_URI` in `/usr/bin/apt-key` is not empty and not commented out. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n\nThe below prerequisites are also crucial for exploitability but are not checked in the scanner:\n\n1. The command apt-key net-update should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the `python-apt` Python module should be called. This is for the malicious keys download.\n\n2. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine." + } + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2024-6119", + "message": { + "text": "References to the vulnerable functions were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///usr/local/bin/node" + }, + "region": { + "snippet": { + "text": "" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "applic_CVE-2024-6119", + "message": { + "text": "References to the vulnerable functions were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///usr/local/bin/node" + }, + "region": { + "snippet": { + "text": "" + } + } + } + } + ] + }, + { + "ruleId": "applic_CVE-2024-45492", + "kind": "pass", + "message": { + "text": "The scanner checks whether the current binary was compiled with 32-bit architecture and if any of the vulnerable functions are called:\n\n- `XML_ParseBuffer()`\n- `XML_Parse()`\n\nNote - the vulnerability occurs when certain inputs are passed to those functions." + } + } + ] + } + ], + "secrets": [ + { + "tool": { + "driver": { + "informationUri": "https://jfrog.com/help/r/jfrog-security-documentation/jfrog-advanced-security", + "name": "JFrog Secrets scanner", + "rules": [ + { + "id": "REQ.SECRET.GENERIC.TEXT", + "name": "REQ.SECRET.GENERIC.TEXT", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.TEXT" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + }, + { + "id": "REQ.SECRET.GENERIC.CODE", + "name": "REQ.SECRET.GENERIC.CODE", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.CODE" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "REQ.SECRET.KEYS", + "name": "REQ.SECRET.KEYS", + "shortDescription": { + "text": "Scanner for REQ.SECRET.KEYS" + }, + "fullDescription": { + "text": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\u0026tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n", + "markdown": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops\u0026tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "REQ.CRED.PUBLIC-ONLY", + "name": "REQ.CRED.PUBLIC-ONLY", + "shortDescription": { + "text": "Scanner for REQ.CRED.PUBLIC-ONLY" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "undetermined", + "conclusion": "private" + } + }, + { + "id": "REQ.SECRET.GENERIC.URL", + "name": "REQ.SECRET.GENERIC.URL", + "shortDescription": { + "text": "Scanner for REQ.SECRET.GENERIC.URL" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + } + ], + "version": "1.0" + } + }, + "invocations": [ + { + "arguments": [ + "/Users/user/.jfrog/dependencies/analyzerManager/jas_scanner/jas_scanner", + "scan", + "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210780-681556384/Secrets_1726210839/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210535-1985298017/image.tar" + } + } + ], + "results": [ + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.GENERIC.CODE", + "message": { + "text": "Hardcoded secrets were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///private/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/tmpsfyn_3d1/unpacked/filesystem/blobs/sha256/9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0/usr/src/app/server/index.js" + }, + "region": { + "startLine": 5, + "startColumn": 7, + "endLine": 5, + "endColumn": 57, + "snippet": { + "text": "tok************" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///private/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/tmpsfyn_3d1/unpacked/filesystem/blobs/sha256/9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0/usr/src/app/server/index.js" + }, + "region": { + "startLine": 6, + "startColumn": 14, + "endLine": 6, + "endColumn": 24, + "snippet": { + "text": "eyJ************" + } + } + } + } + ] + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.GENERIC.URL", + "message": { + "text": "Hardcoded secrets were found" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///usr/src/app/server/scripts/__pycache__/fetch_github_repo.cpython-311.pyc" + }, + "region": { + "snippet": { + "text": "htt************" + } + } + } + } + ] + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/testdata/output/dockerscan/docker_sarif.json b/tests/testdata/output/dockerscan/docker_sarif.json new file mode 100644 index 00000000..2ec85075 --- /dev/null +++ b/tests/testdata/output/dockerscan/docker_sarif.json @@ -0,0 +1,778 @@ +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "informationUri": "https://jfrog.com/help/r/jfrog-security-documentation/jfrog-advanced-security", + "name": "JFrog Binary Secrets Scanner", + "rules": [ + { + "id": "REQ.SECRET.GENERIC.TEXT", + "name": "REQ.SECRET.GENERIC.TEXT", + "shortDescription": { + "text": "[Secret in Binary found] Scanner for REQ.SECRET.GENERIC.TEXT" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "help": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "not_applicable", + "conclusion": "positive" + } + }, + { + "id": "REQ.SECRET.GENERIC.CODE", + "name": "REQ.SECRET.GENERIC.CODE", + "shortDescription": { + "text": "[Secret in Binary found] Scanner for REQ.SECRET.GENERIC.CODE" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "help": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "REQ.SECRET.KEYS", + "name": "REQ.SECRET.KEYS", + "shortDescription": { + "text": "[Secret in Binary found] Scanner for REQ.SECRET.KEYS" + }, + "fullDescription": { + "text": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n", + "markdown": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + "help": { + "text": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n", + "markdown": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + }, + { + "id": "REQ.CRED.PUBLIC-ONLY", + "name": "REQ.CRED.PUBLIC-ONLY", + "shortDescription": { + "text": "[Secret in Binary found] Scanner for REQ.CRED.PUBLIC-ONLY" + }, + "fullDescription": { + "text": "", + "markdown": "" + }, + "help": { + "text": "", + "markdown": "" + }, + "properties": { + "applicability": "undetermined", + "conclusion": "private" + } + }, + { + "id": "REQ.SECRET.GENERIC.URL", + "name": "REQ.SECRET.GENERIC.URL", + "shortDescription": { + "text": "[Secret in Binary found] Scanner for REQ.SECRET.GENERIC.URL" + }, + "fullDescription": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "help": { + "text": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n", + "markdown": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + "properties": { + "applicability": "applicable", + "conclusion": "negative", + "security-severity": "6.9" + } + } + ], + "version": "1.0" + } + }, + "invocations": [ + { + "arguments": [ + "/Users/user/.jfrog/dependencies/analyzerManager/jas_scanner/jas_scanner", + "scan", + "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210780-681556384/Secrets_1726210839/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210535-1985298017/image.tar" + } + } + ], + "results": [ + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.GENERIC.CODE", + "message": { + "text": "Hardcoded secrets were found", + "markdown": "🔒 Found Secrets in Binary docker scanning:\nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: usr/src/app/server/index.js\nEvidence: tok************" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "usr/src/app/server/index.js" + }, + "region": { + "startLine": 5, + "startColumn": 7, + "endLine": 5, + "endColumn": 57, + "snippet": { + "text": "tok************" + } + } + }, + "logicalLocations": [ + { + "name": "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "00436fac1d19ea36302f14e892926efb" + } + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.KEYS", + "message": { + "text": "Secret keys were found", + "markdown": "🔒 Found Secrets in Binary docker scanning:\nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: usr/src/app/server/index.js\nEvidence: eyJ************" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "usr/src/app/server/index.js" + }, + "region": { + "startLine": 6, + "startColumn": 14, + "endLine": 6, + "endColumn": 24, + "snippet": { + "text": "eyJ************" + } + } + }, + "logicalLocations": [ + { + "name": "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "2550dbdb124696ae8fcc5cfd6f2b65b8" + } + }, + { + "properties": { + "metadata": "", + "tokenValidation": "" + }, + "ruleId": "REQ.SECRET.GENERIC.URL", + "message": { + "text": "Hardcoded secrets were found", + "markdown": "🔒 Found Secrets in Binary docker scanning:\nImage: platform.jfrog.io/swamp-docker/swamp:latest\nFilepath: usr/src/app/server/scripts/__pycache__/fetch_github_repo.cpython-311.pyc\nEvidence: htt************" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "usr/src/app/server/scripts/__pycache__/fetch_github_repo.cpython-311.pyc" + }, + "region": { + "snippet": { + "text": "htt************" + } + } + } + } + ], + "fingerprints": { + "jfrogFingerprintHash": "9164423e88bbec9d1216bc5600eb7f9b" + } + } + ] + }, + { + "tool": { + "driver": { + "informationUri": "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/sca", + "name": "JFrog Xray Scanner", + "rules": [ + { + "id": "CVE-2024-45490_debian:bookworm:libexpat1_2.5.0-1", + "shortDescription": { + "text": "[CVE-2024-45490] debian:bookworm:libexpat1 2.5.0-1" + }, + "help": { + "text": "An issue was discovered in libexpat before 2.6.3. xmlparse.c does not reject a negative length for XML_ParseBuffer.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 9.8 | Not Applicable | `sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar ` | No fix available |" + }, + "properties": { + "security-severity": "9.8" + } + }, + { + "id": "CVE-2011-3374_debian:bookworm:apt_2.6.1", + "shortDescription": { + "text": "[CVE-2011-3374] debian:bookworm:apt 2.6.1" + }, + "help": { + "text": "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.7 | Not Applicable | `sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar ` | No fix available |" + }, + "properties": { + "security-severity": "3.7" + } + }, + { + "id": "CVE-2024-38428_debian:bookworm:wget_1.21.3-1+b1", + "shortDescription": { + "text": "[CVE-2024-38428] debian:bookworm:wget 1.21.3-1+b1" + }, + "help": { + "text": "url.c in GNU Wget through 1.24.5 mishandles semicolons in the userinfo subcomponent of a URI, and thus there may be insecure behavior in which data that was supposed to be in the userinfo subcomponent is misinterpreted to be part of the host subcomponent.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 9.1 | Undetermined | `sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ` | No fix available |" + }, + "properties": { + "security-severity": "9.1" + } + }, + { + "id": "XRAY-264729_cors.js_0.0.1-security", + "shortDescription": { + "text": "[XRAY-264729] cors.js 0.0.1-security" + }, + "help": { + "text": "Malicious package cors.js for Node.js", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 10.0 | Not Covered | `sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar ` | No fix available |" + }, + "properties": { + "security-severity": "10.0" + } + }, + { + "id": "CVE-2024-45492_debian:bookworm:libexpat1_2.5.0-1", + "shortDescription": { + "text": "[CVE-2024-45492] debian:bookworm:libexpat1 2.5.0-1" + }, + "help": { + "text": "An issue was discovered in libexpat before 2.6.3. nextScaffoldPart in xmlparse.c can have an integer overflow for m_groupSize on 32-bit platforms (where UINT_MAX equals SIZE_MAX).", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 9.8 | Not Applicable | `sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar ` | No fix available |" + }, + "properties": { + "security-severity": "9.8" + } + }, + { + "id": "CVE-2023-51767_debian:bookworm:openssh-client:1_9.2p1-2+deb12u3", + "shortDescription": { + "text": "[CVE-2023-51767] debian:bookworm:openssh-client:1 9.2p1-2+deb12u3" + }, + "help": { + "text": "OpenSSH through 9.6, when common types of DRAM are used, might allow row hammer attacks (for authentication bypass) because the integer value of authenticated in mm_answer_authpassword does not resist flips of a single bit. NOTE: this is applicable to a certain threat model of attacker-victim co-location in which the attacker has user privileges.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.0 | Applicable | `sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar ` | No fix available |" + }, + "properties": { + "security-severity": "7.0" + } + }, + { + "id": "CVE-2011-3374_debian:bookworm:libapt-pkg6.0_2.6.1", + "shortDescription": { + "text": "[CVE-2011-3374] debian:bookworm:libapt-pkg6.0 2.6.1" + }, + "help": { + "text": "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.7 | Not Applicable | `sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar ` | No fix available |" + }, + "properties": { + "security-severity": "3.7" + } + }, + { + "id": "CVE-2024-4741_debian:bookworm:openssl_3.0.13-1~deb12u1", + "shortDescription": { + "text": "[CVE-2024-4741] debian:bookworm:openssl 3.0.13-1~deb12u1" + }, + "help": { + "text": "CVE-2024-4741", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 0.0 | Applicable | `sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ` | [3.0.14-1~deb12u1] |" + }, + "properties": { + "security-severity": "0.0" + } + }, + { + "id": "CVE-2024-6119_debian:bookworm:libssl3_3.0.13-1~deb12u1", + "shortDescription": { + "text": "[CVE-2024-6119] debian:bookworm:libssl3 3.0.13-1~deb12u1" + }, + "help": { + "text": "Issue summary: Applications performing certificate name checks (e.g., TLS\nclients checking server certificates) may attempt to read an invalid memory\naddress resulting in abnormal termination of the application process.\n\nImpact summary: Abnormal termination of an application can a cause a denial of\nservice.\n\nApplications performing certificate name checks (e.g., TLS clients checking\nserver certificates) may attempt to read an invalid memory address when\ncomparing the expected name with an `otherName` subject alternative name of an\nX.509 certificate. This may result in an exception that terminates the\napplication program.\n\nNote that basic certificate chain validation (signatures, dates, ...) is not\naffected, the denial of service can occur only when the application also\nspecifies an expected DNS name, Email address or IP address.\n\nTLS servers rarely solicit client certificates, and even when they do, they\ngenerally don't perform a name check against a reference identifier (expected\nidentity), but rather extract the presented identity after checking the\ncertificate chain. So TLS servers are generally not affected and the severity\nof the issue is Moderate.\n\nThe FIPS modules in 3.3, 3.2, 3.1 and 3.0 are not affected by this issue.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 0.0 | Applicable | `sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ` | No fix available |" + }, + "properties": { + "security-severity": "0.0" + } + }, + { + "id": "CVE-2024-6119_debian:bookworm:openssl_3.0.13-1~deb12u1", + "shortDescription": { + "text": "[CVE-2024-6119] debian:bookworm:openssl 3.0.13-1~deb12u1" + }, + "help": { + "text": "Issue summary: Applications performing certificate name checks (e.g., TLS\nclients checking server certificates) may attempt to read an invalid memory\naddress resulting in abnormal termination of the application process.\n\nImpact summary: Abnormal termination of an application can a cause a denial of\nservice.\n\nApplications performing certificate name checks (e.g., TLS clients checking\nserver certificates) may attempt to read an invalid memory address when\ncomparing the expected name with an `otherName` subject alternative name of an\nX.509 certificate. This may result in an exception that terminates the\napplication program.\n\nNote that basic certificate chain validation (signatures, dates, ...) is not\naffected, the denial of service can occur only when the application also\nspecifies an expected DNS name, Email address or IP address.\n\nTLS servers rarely solicit client certificates, and even when they do, they\ngenerally don't perform a name check against a reference identifier (expected\nidentity), but rather extract the presented identity after checking the\ncertificate chain. So TLS servers are generally not affected and the severity\nof the issue is Moderate.\n\nThe FIPS modules in 3.3, 3.2, 3.1 and 3.0 are not affected by this issue.", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 0.0 | Applicable | `sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ` | [3.0.14-1~deb12u2] |" + }, + "properties": { + "security-severity": "0.0" + } + }, + { + "id": "CVE-2024-4741_debian:bookworm:libssl3_3.0.13-1~deb12u1", + "shortDescription": { + "text": "[CVE-2024-4741] debian:bookworm:libssl3 3.0.13-1~deb12u1" + }, + "help": { + "text": "CVE-2024-4741", + "markdown": "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 0.0 | Applicable | `sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ` | No fix available |" + }, + "properties": { + "security-severity": "0.0" + } + } + ], + "version": "3.104.8" + } + }, + "invocations": [ + { + "executionSuccessful": true, + "workingDirectory": { + "uri": "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210535-1985298017/image.tar" + } + } + ], + "results": [ + { + "properties": { + "applicability": "Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2024-6119_debian:bookworm:libssl3_3.0.13-1~deb12u1", + "ruleIndex": 8, + "level": "none", + "message": { + "text": "[CVE-2024-6119] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ", + "markdown": "[CVE-2024-6119] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + "logicalLocations": [ + { + "name": "f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "5b5d2ba57a2eddf58f4579b7ebe42599" + } + }, + { + "properties": { + "applicability": "Applicable", + "fixedVersion": "[3.0.14-1~deb12u2]" + }, + "ruleId": "CVE-2024-6119_debian:bookworm:openssl_3.0.13-1~deb12u1", + "ruleIndex": 9, + "level": "none", + "message": { + "text": "[CVE-2024-6119] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ", + "markdown": "[CVE-2024-6119] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + "logicalLocations": [ + { + "name": "f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "bd5908946de9c082f96e15217590eebc" + } + }, + { + "properties": { + "applicability": "Undetermined", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2024-38428_debian:bookworm:wget_1.21.3-1+b1", + "ruleIndex": 2, + "level": "error", + "message": { + "text": "[CVE-2024-38428] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ", + "markdown": "[CVE-2024-38428] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + "logicalLocations": [ + { + "name": "f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "db89861310f80a270a0a81f48d7dc974" + } + }, + { + "properties": { + "applicability": "Not Covered", + "fixedVersion": "No fix available" + }, + "ruleId": "XRAY-264729_cors.js_0.0.1-security", + "ruleIndex": 3, + "level": "error", + "message": { + "text": "[XRAY-264729] sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar ", + "markdown": "[XRAY-264729] sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar" + } + }, + "logicalLocations": [ + { + "name": "ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "d653c414ef56560432b122358961104a" + } + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2024-45490_debian:bookworm:libexpat1_2.5.0-1", + "ruleIndex": 0, + "level": "error", + "message": { + "text": "[CVE-2024-45490] sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar ", + "markdown": "[CVE-2024-45490] sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): 20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + }, + "logicalLocations": [ + { + "name": "20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "61be5170151428187e85ff7b27fd65b4" + } + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2024-45492_debian:bookworm:libexpat1_2.5.0-1", + "ruleIndex": 4, + "level": "error", + "message": { + "text": "[CVE-2024-45492] sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar ", + "markdown": "[CVE-2024-45492] sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): 20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + }, + "logicalLocations": [ + { + "name": "20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "e47bb0a94451ed5111fabcf0ccaaeee6" + } + }, + { + "properties": { + "applicability": "Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2023-51767_debian:bookworm:openssh-client:1_9.2p1-2+deb12u3", + "ruleIndex": 5, + "level": "note", + "message": { + "text": "[CVE-2023-51767] sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar ", + "markdown": "[CVE-2023-51767] sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): 20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + }, + "logicalLocations": [ + { + "name": "20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "fe7c1c90b3e7d340890027344468b42d" + } + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2011-3374_debian:bookworm:apt_2.6.1", + "ruleIndex": 1, + "level": "note", + "message": { + "text": "[CVE-2011-3374] sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar ", + "markdown": "[CVE-2011-3374] sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + } + }, + "logicalLocations": [ + { + "name": "cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "81f98a6fd77d17d7647c0ae81410b506" + } + }, + { + "properties": { + "applicability": "Not Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2011-3374_debian:bookworm:libapt-pkg6.0_2.6.1", + "ruleIndex": 6, + "level": "note", + "message": { + "text": "[CVE-2011-3374] sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar ", + "markdown": "[CVE-2011-3374] sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + } + }, + "logicalLocations": [ + { + "name": "cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "7933bf1c7b4635012e7571e82e619db6" + } + }, + { + "properties": { + "applicability": "Applicable", + "fixedVersion": "[3.0.14-1~deb12u1]" + }, + "ruleId": "CVE-2024-4741_debian:bookworm:openssl_3.0.13-1~deb12u1", + "ruleIndex": 7, + "level": "none", + "message": { + "text": "[CVE-2024-4741] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ", + "markdown": "[CVE-2024-4741] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + "logicalLocations": [ + { + "name": "f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "a374e04992f42ee827634927edd7e8d4" + } + }, + { + "properties": { + "applicability": "Applicable", + "fixedVersion": "No fix available" + }, + "ruleId": "CVE-2024-4741_debian:bookworm:libssl3_3.0.13-1~deb12u1", + "ruleIndex": 10, + "level": "none", + "message": { + "text": "[CVE-2024-4741] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar ", + "markdown": "[CVE-2024-4741] sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar \nImage: platform.jfrog.io/swamp-docker/swamp:latest\nLayer (sha256): f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + "logicalLocations": [ + { + "name": "f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595", + "kind": "layer", + "properties": { + "algorithm": "sha256" + } + } + ] + } + ], + "fingerprints": { + "jfrogFingerprintHash": "2c34553d9c75460bf14243ff13ba84c8" + } + } + ] + } + ] +} diff --git a/tests/testdata/output/dockerscan/docker_simple_json.json b/tests/testdata/output/dockerscan/docker_simple_json.json new file mode 100644 index 00000000..0fbd8a5a --- /dev/null +++ b/tests/testdata/output/dockerscan/docker_simple_json.json @@ -0,0 +1,826 @@ +{ + "vulnerabilities": [ + { + "severity": "Critical", + "impactedPackageName": "debian:bookworm:wget", + "impactedPackageVersion": "1.21.3-1+b1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + } + ], + "summary": "url.c in GNU Wget through 1.24.5 mishandles semicolons in the userinfo subcomponent of a URI, and thus there may be insecure behavior in which data that was supposed to be in the userinfo subcomponent is misinterpreted to be part of the host subcomponent.", + "applicable": "Undetermined", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2024-38428", + "cvssV2": "", + "cvssV3": "9.1", + "applicability": { + "status": "Undetermined" + } + } + ], + "issueId": "XRAY-606103", + "references": [ + "https://git.savannah.gnu.org/cgit/wget.git/commit/?id=ed0c7c7e0e8f7298352646b2fd6e06a11e242ace", + "https://lists.gnu.org/archive/html/bug-wget/2024-06/msg00005.html", + "https://security-tracker.debian.org/tracker/CVE-2024-38428" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + { + "name": "debian:bookworm:wget", + "version": "1.21.3-1+b1", + "location": { + "file": "wget:1.21.3-1+b1" + } + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Critical", + "impactedPackageName": "cors.js", + "impactedPackageVersion": "0.0.1-security", + "impactedPackageType": "npm", + "components": [ + { + "name": "sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar", + "version": "", + "location": { + "file": "sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar" + } + } + ], + "summary": "Malicious package cors.js for Node.js", + "applicable": "Not Covered", + "fixedVersions": null, + "cves": null, + "issueId": "XRAY-264729", + "references": [ + "https://registry.npmjs.com" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar", + "version": "", + "location": { + "file": "sha256__ab1c0a95b2970fb44e2a4046c5c00f37a5b061e74d72b254a8975beb7d09f74f.tar" + } + }, + { + "name": "cors.js", + "version": "0.0.1-security", + "location": { + "file": "usr/src/app/node_modules/cors.js/package.json" + } + } + ] + ], + "jfrogResearchInformation": { + "severity": "Critical", + "summary": "Malicious package cors.js for Node.js", + "details": "The package cors.js for Node.js contains malicious code that installs a persistent connectback shell. The package is typosquatting the popular `cors` package. When installed, the package opens a connectback shell to the hardcoded host `107.175.32.229` on TCP port 56173. The malicious payload achieves persistency by installing a cron job that repeats every 10 seconds - `*/10 * * * * *`", + "remediation": "As with any malware, the malicious package must be completely removed, and steps must be taken care to remediate the damage that was done by the malicious package -\n\n##### Removing the malicious package\n\nRun `npm uninstall cors.js`\n\n##### Refreshing stolen credentials\n\nMany malicious packages steal stored user credentials, focusing on the following -\n\n* [Browser autocomplete](https://jfrog.com/blog/malicious-pypi-packages-stealing-credit-cards-injecting-code/) data, such as saved passwords and credit cards\n* [Environment variables](https://jfrog.com/blog/malicious-npm-packages-are-after-your-discord-tokens-17-new-packages-disclosed/) passed to the malicious code\n* [Stored Discord tokens](https://jfrog.com/blog/malicious-npm-packages-are-after-your-discord-tokens-17-new-packages-disclosed/)\n* AWS / GitHub credentials stored in cleartext files\n\nIt is highly recommended to change or revoke data that is stored in the infected machine at those locations\n\n##### Stopping malicious processes\n\nMany malicious packages start malicious processes such as [connectback shells](https://jfrog.com/blog/jfrog-discloses-3-remote-access-trojans-in-pypi/) or crypto-miners. Search for any unfamiliar processes that consume a large amount of CPU or a large amount of network traffic, and stop them. On Windows, this can be facilitated with [Sysinternals Process Explorer](https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer).\n\n##### Removing installed backdoors\n\nMany malicious packages install themselves as a [persistent backdoor](https://jfrog.com/blog/npm-supply-chain-attack-targets-german-based-companies/), in order to guarantee the malicious code survives a reboot. Search for any unfamiliar binaries set to be run on startup, and remove them. On Windows, this can be facilitated with [Sysinternals Autoruns](https://docs.microsoft.com/en-us/sysinternals/downloads/autoruns).\n\n##### Defining an Xray policy that blocks downloads of Artifacts with malicious packages\n\nIt is possible to [create an Xray policy](https://www.jfrog.com/confluence/display/JFROG/Creating+Xray+Policies+and+Rules) that will not allow artifacts with identified malicious packages to be downloaded from Artifactory. To create such a policy, add a new `Security` policy and set `Minimal Severity` to `Critical`. Under `Automatic Actions` check the `Block Download` action.\n\n##### Contacting the JFrog Security Research team for additional information\n\nOptionally, if you are unsure of the full impact of the malicious package and wish to get more details, the JFrog Security Research team can help you assess the potential damage from the installed malicious package.\n\nPlease contact us at research@jfrog.com with details of the affected artifact and the name of the identified malicious package." + } + }, + { + "severity": "Low", + "impactedPackageName": "debian:bookworm:openssh-client:1", + "impactedPackageVersion": "9.2p1-2+deb12u3", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "version": "", + "location": { + "file": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + } + ], + "summary": "OpenSSH through 9.6, when common types of DRAM are used, might allow row hammer attacks (for authentication bypass) because the integer value of authenticated in mm_answer_authpassword does not resist flips of a single bit. NOTE: this is applicable to a certain threat model of attacker-victim co-location in which the attacker has user privileges.", + "applicable": "Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2023-51767", + "cvssV2": "", + "cvssV3": "7.0", + "applicability": { + "status": "Applicable", + "scannerDescription": "The CVE is always applicable.\n\nNote - The vulnerability is hardware-dependent." + } + } + ], + "issueId": "XRAY-585612", + "references": [ + "https://arxiv.org/abs/2309.02545", + "https://github.com/openssh/openssh-portable/blob/8241b9c0529228b4b86d88b1a6076fb9f97e4a99/monitor.c#L878", + "https://github.com/openssh/openssh-portable/blob/8241b9c0529228b4b86d88b1a6076fb9f97e4a99/auth-passwd.c#L77", + "https://bugzilla.redhat.com/show_bug.cgi?id=2255850", + "https://security-tracker.debian.org/tracker/CVE-2023-51767", + "https://ubuntu.com/security/CVE-2023-51767", + "https://security.netapp.com/advisory/ntap-20240125-0006/", + "https://access.redhat.com/security/cve/CVE-2023-51767" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "version": "", + "location": { + "file": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + }, + { + "name": "debian:bookworm:openssh-client:1", + "version": "9.2p1-2+deb12u3", + "location": { + "file": "openssh-client:1:9.2p1-2+deb12u3" + } + } + ] + ], + "jfrogResearchInformation": { + "severity": "Low", + "summary": "The RowHammer fault injection attack can theoretically lead to local authentication bypass in OpenSSH.", + "details": "[OpenSSH](https://www.openssh.com/) is a popular open-source implementation of the SSH (Secure Shell) protocol, providing encrypted communication over a network.\nIt was discovered that the OpenSSH authentication logic can be susceptible in some cases to a side-channel fault injection attack. The attack can theoretically be carried out by a local attacker which eventually bypass OpenSSH authentication mechanism.\n\nThis vulnerability currently lacks widely known published exploits, and its exploitation is considered highly complex. The intricacies of the attack, combined with the absence of well-documented exploits, contribute to the difficulty in achieving successful exploitation. Furthermore, it's essential to note that the susceptibility to this vulnerability is hardware-dependent, and the success of an attack relies on probabilities associated with the specific hardware configuration. \n\nThe vulnerability is theoretically exploitable by several different ways, the only two published ways are:\n\nIn the OpenSSH function `mm_answer_authpassword()`, a stack variable `authenticated`, is assigned to the value of the function `auth_password()` which returns 1/0 and then returned. If the value of `authenticated` is 1, the SSH connection will be established. Since `authenticated` is stored on the stack, therefore in DRAM, a local attacker could flip this 32-bit integer least significant bit, thus, bypass authentication.\n\nAnother possible exploit is the `result` stack variable in `auth_password()` function. It is initialized to 0 and set to 1 if the password is correct. \nSimilarly to the previous method, this attack requires a single bit flip of the `result` variable in order for the function to return 1 and bypass the authentication.\n\nAttackers can trigger the vulnerability via a RowHammer fault injection. The Rowhammer bug is a hardware reliability issue in which an attacker repeatedly accesses (hammers) DRAM cells to cause unauthorized changes in physically adjacent memory locations.\nSimply put:\n\n* A specific register value(`authenticated`/`result` value) is pushed onto the stack during program execution. \n* The stack, where the register value is stored, is identified to be located in a memory row susceptible to bit flips (flippable row) due to the RowHammer vulnerability in DRAM.\n* The attacker performs a series of rapid and repeated memory accesses to the adjacent rows of the flippable row in the DRAM. This repeated access exploits the RowHammer vulnerability, inducing bit flips in the targeted flippable row.\n* Due to the RowHammer effect, bit flips occur in the flippable row, potentially corrupting the data stored there.\n* After inducing bit flips in the flippable row, the attacker manipulates the program's control flow to pop the corrupted value from the stack into a register.\n* The register now holds a value that has been corrupted through the RowHammer attack. Now the `authenticated`/`result` variables hold this corrupted value thus it can lead to authentication bypass, as it may impact the control flow in a way advantageous to the attacker.", + "severityReasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The vulnerability depends on the OS and hardware. It was only evaluated in one test environment, therefore results for other conditions might differ. The attacker must be extremely familiar with the details of the exploited system (ex. know the exact hardware which is running the OS).", + "isPositive": true + }, + { + "name": "The issue can only be exploited by an attacker that can execute code on the vulnerable machine (excluding exceedingly rare circumstances)", + "isPositive": true + }, + { + "name": "No high-impact exploit or technical writeup were published, and exploitation of the issue with high impact is either non-trivial or completely unproven", + "description": "Exploitation is extremely non-trivial (even theoretically), no public exploits have been published.", + "isPositive": true + }, + { + "name": "The reported CVSS was either wrongly calculated, downgraded by other vendors, or does not reflect the vulnerability's impact", + "description": "The vulnerability's attack complexity is significantly higher than what the CVSS represents.", + "isPositive": true + } + ] + } + }, + { + "severity": "Unknown", + "impactedPackageName": "debian:bookworm:libssl3", + "impactedPackageVersion": "3.0.13-1~deb12u1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + } + ], + "summary": "CVE-2024-4741", + "applicable": "Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2024-4741", + "cvssV2": "", + "cvssV3": "", + "applicability": { + "status": "Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `SSL_free_buffers` is called.", + "evidence": [ + { + "file": "usr/local/bin/node", + "reason": "References to the vulnerable functions were found" + } + ] + } + } + ], + "issueId": "XRAY-603657", + "references": [ + "https://security-tracker.debian.org/tracker/CVE-2024-4741" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + { + "name": "debian:bookworm:libssl3", + "version": "3.0.13-1~deb12u1", + "location": { + "file": "libssl3:3.0.13-1~deb12u1" + } + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Unknown", + "impactedPackageName": "debian:bookworm:openssl", + "impactedPackageVersion": "3.0.13-1~deb12u1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + } + ], + "summary": "CVE-2024-4741", + "applicable": "Applicable", + "fixedVersions": [ + "[3.0.14-1~deb12u1]" + ], + "cves": [ + { + "id": "CVE-2024-4741", + "cvssV2": "", + "cvssV3": "", + "applicability": { + "status": "Applicable", + "scannerDescription": "The scanner checks whether the vulnerable function `SSL_free_buffers` is called.", + "evidence": [ + { + "file": "usr/local/bin/node", + "reason": "References to the vulnerable functions were found" + } + ] + } + } + ], + "issueId": "XRAY-603657", + "references": [ + "https://security-tracker.debian.org/tracker/CVE-2024-4741" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + { + "name": "debian:bookworm:openssl", + "version": "3.0.13-1~deb12u1", + "location": { + "file": "openssl:3.0.13-1~deb12u1" + } + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Unknown", + "impactedPackageName": "debian:bookworm:libssl3", + "impactedPackageVersion": "3.0.13-1~deb12u1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + } + ], + "summary": "Issue summary: Applications performing certificate name checks (e.g., TLS\nclients checking server certificates) may attempt to read an invalid memory\naddress resulting in abnormal termination of the application process.\n\nImpact summary: Abnormal termination of an application can a cause a denial of\nservice.\n\nApplications performing certificate name checks (e.g., TLS clients checking\nserver certificates) may attempt to read an invalid memory address when\ncomparing the expected name with an `otherName` subject alternative name of an\nX.509 certificate. This may result in an exception that terminates the\napplication program.\n\nNote that basic certificate chain validation (signatures, dates, ...) is not\naffected, the denial of service can occur only when the application also\nspecifies an expected DNS name, Email address or IP address.\n\nTLS servers rarely solicit client certificates, and even when they do, they\ngenerally don't perform a name check against a reference identifier (expected\nidentity), but rather extract the presented identity after checking the\ncertificate chain. So TLS servers are generally not affected and the severity\nof the issue is Moderate.\n\nThe FIPS modules in 3.3, 3.2, 3.1 and 3.0 are not affected by this issue.", + "applicable": "Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2024-6119", + "cvssV2": "", + "cvssV3": "", + "applicability": { + "status": "Applicable", + "scannerDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `X509_VERIFY_PARAM_set1_email`\n\n- `X509_check_email`\n\n- `X509_VERIFY_PARAM_set1_host`\n\n- `X509_check_host`", + "evidence": [ + { + "file": "usr/local/bin/node", + "reason": "References to the vulnerable functions were found" + } + ] + } + } + ], + "issueId": "XRAY-632747", + "references": [ + "https://openssl-library.org/news/secadv/20240903.txt", + "https://github.com/openssl/openssl/commit/621f3729831b05ee828a3203eddb621d014ff2b2", + "https://github.com/openssl/openssl/commit/05f360d9e849a1b277db628f1f13083a7f8dd04f", + "https://security-tracker.debian.org/tracker/CVE-2024-6119", + "https://github.com/openssl/openssl/commit/7dfcee2cd2a63b2c64b9b4b0850be64cb695b0a0", + "https://github.com/openssl/openssl/commit/06d1dc3fa96a2ba5a3e22735a033012aadc9f0d6" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + { + "name": "debian:bookworm:libssl3", + "version": "3.0.13-1~deb12u1", + "location": { + "file": "libssl3:3.0.13-1~deb12u1" + } + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Out of bounds read in OpenSSL clients can lead to denial of service when using non-default TLS verification options and connecting to malicious TLS servers", + "severityReasons": [ + { + "name": "The issue has an exploit published", + "description": "The fix commit contains PoC certificates that trigger the denial of service issue" + }, + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The attacker must make the victim client connect to their malicious TLS server, in order to serve the malformed TLS certificate. The victim client must use OpenSSL and must enable non-default certificate verification options, either -\n\n* DNS verification - by using `X509_VERIFY_PARAM_set1_host` or `X509_check_host`\n* Email verification - by using ` X509_VERIFY_PARAM_set1_email` or `X509_check_email`", + "isPositive": true + }, + { + "name": "The issue cannot result in a severe impact (such as remote code execution)", + "description": "Denial of service of a TLS clients only. This out of bounds read cannot lead to data disclosure.", + "isPositive": true + } + ] + } + }, + { + "severity": "Unknown", + "impactedPackageName": "debian:bookworm:openssl", + "impactedPackageVersion": "3.0.13-1~deb12u1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + } + ], + "summary": "Issue summary: Applications performing certificate name checks (e.g., TLS\nclients checking server certificates) may attempt to read an invalid memory\naddress resulting in abnormal termination of the application process.\n\nImpact summary: Abnormal termination of an application can a cause a denial of\nservice.\n\nApplications performing certificate name checks (e.g., TLS clients checking\nserver certificates) may attempt to read an invalid memory address when\ncomparing the expected name with an `otherName` subject alternative name of an\nX.509 certificate. This may result in an exception that terminates the\napplication program.\n\nNote that basic certificate chain validation (signatures, dates, ...) is not\naffected, the denial of service can occur only when the application also\nspecifies an expected DNS name, Email address or IP address.\n\nTLS servers rarely solicit client certificates, and even when they do, they\ngenerally don't perform a name check against a reference identifier (expected\nidentity), but rather extract the presented identity after checking the\ncertificate chain. So TLS servers are generally not affected and the severity\nof the issue is Moderate.\n\nThe FIPS modules in 3.3, 3.2, 3.1 and 3.0 are not affected by this issue.", + "applicable": "Applicable", + "fixedVersions": [ + "[3.0.14-1~deb12u2]" + ], + "cves": [ + { + "id": "CVE-2024-6119", + "cvssV2": "", + "cvssV3": "", + "applicability": { + "status": "Applicable", + "scannerDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `X509_VERIFY_PARAM_set1_email`\n\n- `X509_check_email`\n\n- `X509_VERIFY_PARAM_set1_host`\n\n- `X509_check_host`", + "evidence": [ + { + "file": "usr/local/bin/node", + "reason": "References to the vulnerable functions were found" + } + ] + } + } + ], + "issueId": "XRAY-632747", + "references": [ + "https://openssl-library.org/news/secadv/20240903.txt", + "https://github.com/openssl/openssl/commit/621f3729831b05ee828a3203eddb621d014ff2b2", + "https://github.com/openssl/openssl/commit/05f360d9e849a1b277db628f1f13083a7f8dd04f", + "https://security-tracker.debian.org/tracker/CVE-2024-6119", + "https://github.com/openssl/openssl/commit/7dfcee2cd2a63b2c64b9b4b0850be64cb695b0a0", + "https://github.com/openssl/openssl/commit/06d1dc3fa96a2ba5a3e22735a033012aadc9f0d6" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar", + "version": "", + "location": { + "file": "sha256__f21c087a3964a446bce1aa4e3ec7cf82020dd77ad14f1cf4ea49cbb32eda1595.tar" + } + }, + { + "name": "debian:bookworm:openssl", + "version": "3.0.13-1~deb12u1", + "location": { + "file": "openssl:3.0.13-1~deb12u1" + } + } + ] + ], + "jfrogResearchInformation": { + "severity": "Medium", + "summary": "Out of bounds read in OpenSSL clients can lead to denial of service when using non-default TLS verification options and connecting to malicious TLS servers", + "severityReasons": [ + { + "name": "The issue has an exploit published", + "description": "The fix commit contains PoC certificates that trigger the denial of service issue" + }, + { + "name": "The prerequisites for exploiting the issue are extremely unlikely", + "description": "The attacker must make the victim client connect to their malicious TLS server, in order to serve the malformed TLS certificate. The victim client must use OpenSSL and must enable non-default certificate verification options, either -\n\n* DNS verification - by using `X509_VERIFY_PARAM_set1_host` or `X509_check_host`\n* Email verification - by using ` X509_VERIFY_PARAM_set1_email` or `X509_check_email`", + "isPositive": true + }, + { + "name": "The issue cannot result in a severe impact (such as remote code execution)", + "description": "Denial of service of a TLS clients only. This out of bounds read cannot lead to data disclosure.", + "isPositive": true + } + ] + } + }, + { + "severity": "Critical", + "impactedPackageName": "debian:bookworm:libexpat1", + "impactedPackageVersion": "2.5.0-1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "version": "", + "location": { + "file": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + } + ], + "summary": "An issue was discovered in libexpat before 2.6.3. xmlparse.c does not reject a negative length for XML_ParseBuffer.", + "applicable": "Not Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2024-45490", + "cvssV2": "", + "cvssV3": "9.8", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `XML_Parse()`\n- `XML_ParseBuffer()`\n\nAn additional condition, which the scanner currently does not check, is that the `len` parameter which is passed to those functions is user-controlled." + } + } + ], + "issueId": "XRAY-632613", + "references": [ + "https://github.com/libexpat/libexpat/issues/887", + "https://security-tracker.debian.org/tracker/CVE-2024-45490", + "https://github.com/libexpat/libexpat/pull/890" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "version": "", + "location": { + "file": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + }, + { + "name": "debian:bookworm:libexpat1", + "version": "2.5.0-1", + "location": { + "file": "libexpat1:2.5.0-1" + } + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Critical", + "impactedPackageName": "debian:bookworm:libexpat1", + "impactedPackageVersion": "2.5.0-1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "version": "", + "location": { + "file": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + } + ], + "summary": "An issue was discovered in libexpat before 2.6.3. nextScaffoldPart in xmlparse.c can have an integer overflow for m_groupSize on 32-bit platforms (where UINT_MAX equals SIZE_MAX).", + "applicable": "Not Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2024-45492", + "cvssV2": "", + "cvssV3": "9.8", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks whether the current binary was compiled with 32-bit architecture and if any of the vulnerable functions are called:\n\n- `XML_ParseBuffer()`\n- `XML_Parse()`\n\nNote - the vulnerability occurs when certain inputs are passed to those functions." + } + } + ], + "issueId": "XRAY-632612", + "references": [ + "https://github.com/libexpat/libexpat/issues/889", + "https://security-tracker.debian.org/tracker/CVE-2024-45492", + "https://github.com/libexpat/libexpat/pull/892" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar", + "version": "", + "location": { + "file": "sha256__20f026ae0a91ba4668a54b46f39853dd4c114a84cfedb4144ff24521d3e6dcb1.tar" + } + }, + { + "name": "debian:bookworm:libexpat1", + "version": "2.5.0-1", + "location": { + "file": "libexpat1:2.5.0-1" + } + } + ] + ], + "jfrogResearchInformation": null + }, + { + "severity": "Low", + "impactedPackageName": "debian:bookworm:apt", + "impactedPackageVersion": "2.6.1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + "version": "", + "location": { + "file": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + } + } + ], + "summary": "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.", + "applicable": "Not Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2011-3374", + "cvssV2": "4.3", + "cvssV3": "3.7", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks if the vulnerable variable `ARCHIVE_KEYRING_URI` in `/usr/bin/apt-key` is not empty and not commented out. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n\nThe below prerequisites are also crucial for exploitability but are not checked in the scanner:\n\n1. The command apt-key net-update should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the `python-apt` Python module should be called. This is for the malicious keys download.\n\n2. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine." + } + } + ], + "issueId": "XRAY-34417", + "references": [ + "https://people.canonical.com/~ubuntu-security/cve/2011/CVE-2011-3374.html", + "https://seclists.org/fulldisclosure/2011/Sep/221", + "https://ubuntu.com/security/CVE-2011-3374", + "https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=642480", + "https://access.redhat.com/security/cve/cve-2011-3374", + "https://snyk.io/vuln/SNYK-LINUX-APT-116518", + "https://security-tracker.debian.org/tracker/CVE-2011-3374" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + "version": "", + "location": { + "file": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + } + }, + { + "name": "debian:bookworm:apt", + "version": "2.6.1", + "location": { + "file": "apt:2.6.1" + } + } + ] + ], + "jfrogResearchInformation": { + "severity": "High", + "summary": "Improper signature validation in apt-key may enable Man-in-the-Middle attacks and result in code execution.", + "details": "`apt-key` is [`apt`](https://github.com/Debian/apt)'s key management utility, and is used to manage the keys that are used by `apt` to authenticate packages.\n\nA vulnerability in `apt-key`'s `net-update` function exists, in which [`GPG`](https://www.gnupg.org/) keys, that are used for signing packages and validating their authenticity, aren't validated correctly. The `net-update` function pulls the signing keys that should be added from an insecure location (`http://...`), exposing it to a Man-in-the-Middle attack in which malicious signing keys could be added to the system's keyring. This issue happens due to a vulnerability in the `add_keys_with_veirfy_against_master_keyring()` function, which allows adding signing keys without proper signature validation. \n\nThis vulnerability then potentially allows a malicious actor to perform a Man-in-the-Middle attack on a target, by making it validate malicious packages that were signed with the `GPG` signing key used by the attacker. Effectively, this means that `apt` can be duped to install malicious services and daemons with root privileges.\n\nThe conditions for this vulnerability to be applicable:\n \n1. A valid URI should be configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`. This is the URI that an attacker would need to target in a Man In The Middle attack.\n2. The command `apt-key net-update` should be executed on the affected system, or alternatively `apt.auth.net_update()` function from [python-apt](https://pypi.org/project/python-apt/) Python module should be called. This is for the malicious keys download.\n3. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine.\n\nDo note that `apt-key` is **deprecated** and shouldn't be used, and in most Debian versions `ARCHIVE_KEYRING_URI` is not defined, making this vulnerability unexploitable in most Debian systems.", + "severityReasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The conditions for this vulnerability to be applicable:\n \n1. A valid URI should be configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n2. The command `apt-key net-update` should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the python-apt Python module should be called. This is for the malicious keys download.\n3. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine.", + "isPositive": true + }, + { + "name": "The issue can be exploited by attackers over the network", + "description": "This vulnerability is remotely exploitable when the applicability conditions apply." + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Remote code execution is possible when the applicability conditions apply." + }, + { + "name": "The issue has an exploit published", + "description": "The reporter of this issue has provided a GPG key that can be used for an actual attack, as well as a simple PoC example." + } + ], + "remediation": "##### Deployment mitigations\n\n* Dot not execute `apt-key` command, as it is deprecated.\n* Remove the URI configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`." + } + }, + { + "severity": "Low", + "impactedPackageName": "debian:bookworm:libapt-pkg6.0", + "impactedPackageVersion": "2.6.1", + "impactedPackageType": "Debian", + "components": [ + { + "name": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + "version": "", + "location": { + "file": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + } + } + ], + "summary": "It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.", + "applicable": "Not Applicable", + "fixedVersions": null, + "cves": [ + { + "id": "CVE-2011-3374", + "cvssV2": "4.3", + "cvssV3": "3.7", + "applicability": { + "status": "Not Applicable", + "scannerDescription": "The scanner checks if the vulnerable variable `ARCHIVE_KEYRING_URI` in `/usr/bin/apt-key` is not empty and not commented out. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n\nThe below prerequisites are also crucial for exploitability but are not checked in the scanner:\n\n1. The command apt-key net-update should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the `python-apt` Python module should be called. This is for the malicious keys download.\n\n2. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine." + } + } + ], + "issueId": "XRAY-34417", + "references": [ + "https://people.canonical.com/~ubuntu-security/cve/2011/CVE-2011-3374.html", + "https://seclists.org/fulldisclosure/2011/Sep/221", + "https://ubuntu.com/security/CVE-2011-3374", + "https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=642480", + "https://access.redhat.com/security/cve/cve-2011-3374", + "https://snyk.io/vuln/SNYK-LINUX-APT-116518", + "https://security-tracker.debian.org/tracker/CVE-2011-3374" + ], + "impactPaths": [ + [ + { + "name": "platform.jfrog.io/swamp-docker/swamp", + "version": "latest" + }, + { + "name": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + "version": "", + "location": { + "file": "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar" + } + }, + { + "name": "debian:bookworm:libapt-pkg6.0", + "version": "2.6.1", + "location": { + "file": "libapt-pkg6.0:2.6.1" + } + } + ] + ], + "jfrogResearchInformation": { + "severity": "High", + "summary": "Improper signature validation in apt-key may enable Man-in-the-Middle attacks and result in code execution.", + "details": "`apt-key` is [`apt`](https://github.com/Debian/apt)'s key management utility, and is used to manage the keys that are used by `apt` to authenticate packages.\n\nA vulnerability in `apt-key`'s `net-update` function exists, in which [`GPG`](https://www.gnupg.org/) keys, that are used for signing packages and validating their authenticity, aren't validated correctly. The `net-update` function pulls the signing keys that should be added from an insecure location (`http://...`), exposing it to a Man-in-the-Middle attack in which malicious signing keys could be added to the system's keyring. This issue happens due to a vulnerability in the `add_keys_with_veirfy_against_master_keyring()` function, which allows adding signing keys without proper signature validation. \n\nThis vulnerability then potentially allows a malicious actor to perform a Man-in-the-Middle attack on a target, by making it validate malicious packages that were signed with the `GPG` signing key used by the attacker. Effectively, this means that `apt` can be duped to install malicious services and daemons with root privileges.\n\nThe conditions for this vulnerability to be applicable:\n \n1. A valid URI should be configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`. This is the URI that an attacker would need to target in a Man In The Middle attack.\n2. The command `apt-key net-update` should be executed on the affected system, or alternatively `apt.auth.net_update()` function from [python-apt](https://pypi.org/project/python-apt/) Python module should be called. This is for the malicious keys download.\n3. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine.\n\nDo note that `apt-key` is **deprecated** and shouldn't be used, and in most Debian versions `ARCHIVE_KEYRING_URI` is not defined, making this vulnerability unexploitable in most Debian systems.", + "severityReasons": [ + { + "name": "Exploitation of the issue is only possible when the vulnerable component is used in a specific manner. The attacker has to perform per-target research to determine the vulnerable attack vector", + "description": "The conditions for this vulnerability to be applicable:\n \n1. A valid URI should be configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`. This is the URI that an attacker would need to target in a Man-in-the-Middle attack.\n2. The command `apt-key net-update` should be executed on the affected system, or alternatively `apt.auth.net_update()` function from the python-apt Python module should be called. This is for the malicious keys download.\n3. After the execution of `apt-key net-update`, APT packages should be installed or updated on the machine.", + "isPositive": true + }, + { + "name": "The issue can be exploited by attackers over the network", + "description": "This vulnerability is remotely exploitable when the applicability conditions apply." + }, + { + "name": "The issue results in a severe impact (such as remote code execution)", + "description": "Remote code execution is possible when the applicability conditions apply." + }, + { + "name": "The issue has an exploit published", + "description": "The reporter of this issue has provided a GPG key that can be used for an actual attack, as well as a simple PoC example." + } + ], + "remediation": "##### Deployment mitigations\n\n* Dot not execute `apt-key` command, as it is deprecated.\n* Remove the URI configured in `ARCHIVE_KEYRING_URI` variable in the file `/usr/bin/apt-key`." + } + } + ], + "securityViolations": null, + "licensesViolations": null, + "licenses": null, + "operationalRiskViolations": null, + "secrets": [ + { + "severity": "Medium", + "file": "usr/src/app/server/scripts/__pycache__/fetch_github_repo.cpython-311.pyc", + "snippet": "htt************", + "finding": "Hardcoded secrets were found", + "scannerDescription": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + { + "severity": "Medium", + "file": "private/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/tmpsfyn_3d1/unpacked/filesystem/blobs/sha256/9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0/usr/src/app/server/index.js", + "startLine": 5, + "startColumn": 7, + "endLine": 5, + "endColumn": 57, + "snippet": "tok************", + "finding": "Hardcoded secrets were found", + "scannerDescription": "Storing hardcoded secrets in your source code or binary artifact could lead to several risks.\n\nIf the secret is associated with a wide scope of privileges, attackers could extract it from the source code or binary artifact and use it maliciously to attack many targets. For example, if the hardcoded password gives high-privilege access to an AWS account, the attackers may be able to query/modify company-wide sensitive data without per-user authentication.\n\n## Best practices\n\nUse safe storage when storing high-privilege secrets such as passwords and tokens, for example -\n\n* ### Environment Variables\n\nEnvironment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example -\n`SECRET_VAR=MySecret ./my_application`\nThis way, `MySecret` does not have to be hardcoded into `my_application`.\n\nNote that if your entire binary artifact is published (ex. a Docker container published to Docker Hub), the value for the environment variable must not be stored in the artifact itself (ex. inside the `Dockerfile` or one of the container's files) but rather must be passed dynamically, for example in the `docker run` call as an argument.\n\n* ### Secret management services\n\nExternal vendors offer cloud-based secret management services, that provide proper access control to each secret. The given access to each secret can be dynamically modified or even revoked. Some examples include -\n\n* [Hashicorp Vault](https://www.vaultproject.io)\n* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service)\n* [Google Cloud KMS](https://cloud.google.com/security-key-management)\n\n## Least-privilege principle\n\nStoring a secret in a hardcoded manner can be made safer, by making sure the secret grants the least amount of privilege as needed by the application.\nFor example - if the application needs to read a specific table from a specific database, and the secret grants access to perform this operation **only** (meaning - no access to other tables, no write access at all) then the damage from any secret leaks is mitigated.\nThat being said, it is still not recommended to store secrets in a hardcoded manner, since this type of storage does not offer any way to revoke or moderate the usage of the secret.\n" + }, + { + "severity": "Medium", + "file": "private/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/tmpsfyn_3d1/unpacked/filesystem/blobs/sha256/9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0/usr/src/app/server/index.js", + "startLine": 6, + "startColumn": 14, + "endLine": 6, + "endColumn": 24, + "snippet": "eyJ************", + "finding": "Secret keys were found", + "scannerDescription": "\nStoring an API key in the image could lead to several risks.\n\nIf the key is associated with a wide scope of privileges, attackers could extract it from a single image or firmware and use it maliciously to attack many targets. For example, if the embedded key allows querying/modifying data for all cloud user accounts, without per-user authentication, the attackers who extract it would gain access to system-wide data.\n\nIf the cloud/SaaS provider bills by key usage - for example, every million queries cost the key's owner a fixed sum of money - attackers could use the keys for their own purposes (or just as a form of vandalism), incurring a large cost to the legitimate user or operator.\n\n## Best practices\n\nUse narrow scopes for stored API keys. As much as possible, API keys should be unique per host and require additional authentication with the user's individual credentials for any sensitive actions.\n\nAvoid placing keys whose use incurs costs directly in the image. Store the key with any software or hardware protection available on the host for key storage (such as operating system key-stores, hardware cryptographic storage mechanisms or cloud-managed secure storage services such as [AWS KMS](https://aws.amazon.com/kms/)).\n\nTokens that were detected as exposed should be revoked and replaced -\n\n* [AWS Key Revocation](https://aws.amazon.com/premiumsupport/knowledge-center/delete-access-key/#:~:text=If%20you%20see%20a%20warning,the%20confirmation%20box%2C%20choose%20Deactivate.)\n* [GCP Key Revocation](https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudIAM/delete-api-keys.html)\n* [Azure Key Revocation](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows#revoke-a-pat)\n* [GitHub Key Revocation](https://docs.github.com/en/rest/apps/oauth-applications#delete-an-app-authorization)\n" + } + ], + "iacViolations": null, + "sastViolations": null, + "errors": null +} diff --git a/tests/testdata/output/dockerscan/docker_summary.json b/tests/testdata/output/dockerscan/docker_summary.json new file mode 100644 index 00000000..f325e059 --- /dev/null +++ b/tests/testdata/output/dockerscan/docker_summary.json @@ -0,0 +1,43 @@ +{ + "scans": [ + { + "target": "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1726210535-1985298017/image.tar", + "name": "platform.jfrog.io/swamp-docker/swamp:latest", + "vulnerabilities": { + "sca": { + "scan_ids": [ + "27da9106-88ea-416b-799b-bc7d15783473" + ], + "security": { + "Critical": { + "Not Applicable": 2, + "Not Covered": 1, + "Undetermined": 1 + }, + "Low": { + "Applicable": 1, + "Not Applicable": 1 + }, + "Unknown": { + "Applicable": 2 + } + } + }, + "iac": {}, + "secrets": { + "Medium": { + "": 3 + } + }, + "sast": {} + }, + "violations": { + "sca": { + "scan_ids": [ + "27da9106-88ea-416b-799b-bc7d15783473" + ] + } + } + } + ] + } \ No newline at end of file diff --git a/tests/testdata/other/jobSummary/binary_vulnerabilities.md b/tests/testdata/output/jobSummary/binary_vulnerabilities.md similarity index 100% rename from tests/testdata/other/jobSummary/binary_vulnerabilities.md rename to tests/testdata/output/jobSummary/binary_vulnerabilities.md diff --git a/tests/testdata/other/jobSummary/build_scan_vulnerabilities.md b/tests/testdata/output/jobSummary/build_scan_vulnerabilities.md similarity index 100% rename from tests/testdata/other/jobSummary/build_scan_vulnerabilities.md rename to tests/testdata/output/jobSummary/build_scan_vulnerabilities.md diff --git a/tests/testdata/other/jobSummary/docker_vulnerabilities.md b/tests/testdata/output/jobSummary/docker_vulnerabilities.md similarity index 100% rename from tests/testdata/other/jobSummary/docker_vulnerabilities.md rename to tests/testdata/output/jobSummary/docker_vulnerabilities.md diff --git a/tests/testdata/other/jobSummary/no_violations.md b/tests/testdata/output/jobSummary/no_violations.md similarity index 100% rename from tests/testdata/other/jobSummary/no_violations.md rename to tests/testdata/output/jobSummary/no_violations.md diff --git a/tests/testdata/other/jobSummary/no_vulnerabilities.md b/tests/testdata/output/jobSummary/no_vulnerabilities.md similarity index 100% rename from tests/testdata/other/jobSummary/no_vulnerabilities.md rename to tests/testdata/output/jobSummary/no_vulnerabilities.md diff --git a/tests/testdata/other/jobSummary/security_section.md b/tests/testdata/output/jobSummary/security_section.md similarity index 100% rename from tests/testdata/other/jobSummary/security_section.md rename to tests/testdata/output/jobSummary/security_section.md diff --git a/tests/testdata/other/jobSummary/violations.md b/tests/testdata/output/jobSummary/violations.md similarity index 100% rename from tests/testdata/other/jobSummary/violations.md rename to tests/testdata/output/jobSummary/violations.md diff --git a/tests/testdata/other/jobSummary/violations_not_defined.md b/tests/testdata/output/jobSummary/violations_not_defined.md similarity index 100% rename from tests/testdata/other/jobSummary/violations_not_defined.md rename to tests/testdata/output/jobSummary/violations_not_defined.md diff --git a/tests/testdata/other/jobSummary/violations_not_extended_view.md b/tests/testdata/output/jobSummary/violations_not_extended_view.md similarity index 100% rename from tests/testdata/other/jobSummary/violations_not_extended_view.md rename to tests/testdata/output/jobSummary/violations_not_extended_view.md diff --git a/tests/testdata/projects/jas/jas-config/sast/result.sarif b/tests/testdata/projects/jas/jas-config/sast/result.sarif index 839f3481..c499a4aa 100644 --- a/tests/testdata/projects/jas/jas-config/sast/result.sarif +++ b/tests/testdata/projects/jas/jas-config/sast/result.sarif @@ -63,12 +63,12 @@ { "executionSuccessful": true, "arguments": [ - "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "/users/user/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", "scan", "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693492973-1963413933/results.sarif" ], "workingDirectory": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast" + "uri": "file:///Users/user/testdata/xray/jas/sast" } } ], @@ -87,7 +87,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/__init__.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/__init__.py" }, "region": { "endColumn": 39, @@ -117,7 +117,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/__init__.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/__init__.py" }, "region": { "endColumn": 39, @@ -151,7 +151,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 31, @@ -174,7 +174,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 35, @@ -197,7 +197,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 44, @@ -220,7 +220,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 16, @@ -243,7 +243,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 10, @@ -266,7 +266,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 10, @@ -295,7 +295,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 10, @@ -329,7 +329,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 31, @@ -352,7 +352,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 35, @@ -375,7 +375,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 44, @@ -398,7 +398,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 16, @@ -421,7 +421,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 63, @@ -444,7 +444,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 70, @@ -467,7 +467,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 76, @@ -490,7 +490,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 16, @@ -513,7 +513,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 62, @@ -536,7 +536,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 62, @@ -565,7 +565,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 62, @@ -594,7 +594,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/run.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/run.py" }, "region": { "endColumn": 24, diff --git a/tests/testdata/projects/jas/jas/sast/result.sarif b/tests/testdata/projects/jas/jas/sast/result.sarif index 839f3481..c499a4aa 100644 --- a/tests/testdata/projects/jas/jas/sast/result.sarif +++ b/tests/testdata/projects/jas/jas/sast/result.sarif @@ -63,12 +63,12 @@ { "executionSuccessful": true, "arguments": [ - "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "/users/user/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", "scan", "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693492973-1963413933/results.sarif" ], "workingDirectory": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast" + "uri": "file:///Users/user/testdata/xray/jas/sast" } } ], @@ -87,7 +87,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/__init__.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/__init__.py" }, "region": { "endColumn": 39, @@ -117,7 +117,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/__init__.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/__init__.py" }, "region": { "endColumn": 39, @@ -151,7 +151,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 31, @@ -174,7 +174,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 35, @@ -197,7 +197,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 44, @@ -220,7 +220,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 16, @@ -243,7 +243,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 10, @@ -266,7 +266,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 10, @@ -295,7 +295,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 10, @@ -329,7 +329,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 31, @@ -352,7 +352,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 35, @@ -375,7 +375,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 44, @@ -398,7 +398,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 16, @@ -421,7 +421,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 63, @@ -444,7 +444,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 70, @@ -467,7 +467,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 76, @@ -490,7 +490,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 16, @@ -513,7 +513,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 62, @@ -536,7 +536,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 62, @@ -565,7 +565,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/flask_webgoat/ui.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/flask_webgoat/ui.py" }, "region": { "endColumn": 62, @@ -594,7 +594,7 @@ ], "physicalLocation": { "artifactLocation": { - "uri": "file:///Users/assafa/Documents/code/cli-projects/jfrog-cli/testdata/xray/jas/sast/run.py" + "uri": "file:///Users/user/testdata/xray/jas/sast/run.py" }, "region": { "endColumn": 24, diff --git a/tests/testdata/projects/package-managers/npm/npm-un-installable/package.json b/tests/testdata/projects/package-managers/npm/npm-un-installable/package.json new file mode 100644 index 00000000..7e4cf93e --- /dev/null +++ b/tests/testdata/projects/package-managers/npm/npm-un-installable/package.json @@ -0,0 +1,14 @@ +{ + "name": "jfrog-cli-tests", + "version": "v1.0.0", + "description": "test package", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "non-existing-dependency": "1.0.1" + } +} diff --git a/tests/utils/test_config.go b/tests/utils/test_config.go index fcf1e6c7..be583877 100644 --- a/tests/utils/test_config.go +++ b/tests/utils/test_config.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/assert" - "github.com/jfrog/jfrog-cli-security/cli" configTests "github.com/jfrog/jfrog-cli-security/tests" "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/repository" @@ -48,9 +47,7 @@ func CreateJfrogHomeConfig(t *testing.T, encryptPassword bool) { assert.NoError(t, err) } -func InitTestCliDetails() { - testApplication := cli.GetJfrogCliSecurityApp() - +func InitTestCliDetails(testApplication components.App) { configTests.TestApplication = &testApplication if configTests.PlatformCli == nil { configTests.PlatformCli = GetTestCli(testApplication) diff --git a/tests/utils/test_utils.go b/tests/utils/test_utils.go index 3aae46c0..9d7a5fa5 100644 --- a/tests/utils/test_utils.go +++ b/tests/utils/test_utils.go @@ -5,13 +5,18 @@ import ( "encoding/xml" "errors" "fmt" - "github.com/jfrog/jfrog-cli-security/formats" "os" "path/filepath" "strconv" + "strings" "testing" "time" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/owenrumney/go-sarif/v2/sarif" + clientUtils "github.com/jfrog/jfrog-client-go/utils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/require" @@ -50,6 +55,29 @@ func InitSecurityTest(t *testing.T, xrayMinVersion string) { ValidateXrayVersion(t, xrayMinVersion) } +func ValidateXrayVersion(t *testing.T, minVersion string) { + xrayVersion, err := getTestsXrayVersion() + if err != nil { + assert.NoError(t, err) + return + } + err = clientUtils.ValidateMinimumVersion(clientUtils.Xray, xrayVersion.GetVersion(), minVersion) + if err != nil { + t.Skip(err) + } +} + +func ValidateXscVersion(t *testing.T, minVersion string) { + xscVersion, err := getTestsXscVersion() + if err != nil { + t.Skip(err) + } + err = clientUtils.ValidateMinimumVersion(clientUtils.Xsc, xscVersion.GetVersion(), minVersion) + if err != nil { + t.Skip(err) + } +} + func InitTestWithMockCommandOrParams(t *testing.T, mockCommands ...func() components.Command) (mockCli *coreTests.JfrogCli, cleanUp func()) { oldHomeDir := os.Getenv(coreutils.HomeDir) // Create server config to use with the command. @@ -93,12 +121,12 @@ func removeDirs(dirs ...string) { } } -func getXrayVersion() (version.Version, error) { +func getTestsXrayVersion() (version.Version, error) { xrayVersion, err := configTests.XrAuth.GetVersion() return *version.NewVersion(xrayVersion), err } -func getXscVersion() (version.Version, error) { +func getTestsXscVersion() (version.Version, error) { xscVersion, err := configTests.XscAuth.GetVersion() return *version.NewVersion(xscVersion), err } @@ -110,6 +138,175 @@ func ChangeWD(t *testing.T, newPath string) string { return prevDir } +func ReadCmdScanResults(t *testing.T, path string) *results.SecurityCommandResults { + content, err := os.ReadFile(path) + require.NoError(t, err) + var cmdResults *results.SecurityCommandResults + if !assert.NoError(t, json.Unmarshal(content, &cmdResults)) { + return &results.SecurityCommandResults{} + } + // replace paths separators + for _, targetResults := range cmdResults.Targets { + targetResults.Target = filepath.FromSlash(targetResults.Target) + if targetResults.ScaResults != nil { + for i, descriptor := range targetResults.ScaResults.Descriptors { + targetResults.ScaResults.Descriptors[i] = filepath.FromSlash(descriptor) + } + } + if targetResults.JasResults != nil { + convertSarifRunPathsForOS(targetResults.JasResults.ApplicabilityScanResults...) + convertSarifRunPathsForOS(targetResults.JasResults.SecretsScanResults...) + convertSarifRunPathsForOS(targetResults.JasResults.IacScanResults...) + convertSarifRunPathsForOS(targetResults.JasResults.SastScanResults...) + } + } + return cmdResults +} + +func convertSarifRunPathsForOS(runs ...*sarif.Run) { + for r := range runs { + for i := range runs[r].Invocations { + if runs[r].Invocations[i].WorkingDirectory != nil && runs[r].Invocations[i].WorkingDirectory.URI != nil { + *runs[r].Invocations[i].WorkingDirectory.URI = filepath.FromSlash(sarifutils.GetInvocationWorkingDirectory(runs[r].Invocations[i])) + } + } + for i := range runs[r].Results { + for j := range runs[r].Results[i].Locations { + if runs[r].Results[i].Locations[j] != nil && runs[r].Results[i].Locations[j].PhysicalLocation != nil && runs[r].Results[i].Locations[j].PhysicalLocation.ArtifactLocation != nil && runs[r].Results[i].Locations[j].PhysicalLocation.ArtifactLocation.URI != nil { + *runs[r].Results[i].Locations[j].PhysicalLocation.ArtifactLocation.URI = getJasConvertedPath(sarifutils.GetLocationFileName(runs[r].Results[i].Locations[j])) + } + } + for j := range runs[r].Results[i].CodeFlows { + for k := range runs[r].Results[i].CodeFlows[j].ThreadFlows { + for l := range runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations { + if runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations[l] != nil && runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations[l].Location != nil && runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations[l].Location.PhysicalLocation != nil && runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations[l].Location.PhysicalLocation.ArtifactLocation != nil && runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations[l].Location.PhysicalLocation.ArtifactLocation.URI != nil { + *runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations[l].Location.PhysicalLocation.ArtifactLocation.URI = getJasConvertedPath(sarifutils.GetLocationFileName(runs[r].Results[i].CodeFlows[j].ThreadFlows[k].Locations[l].Location)) + } + } + } + } + } + } +} + +func ReadSimpleJsonResults(t *testing.T, path string) formats.SimpleJsonResults { + content, err := os.ReadFile(path) + require.NoError(t, err) + var results formats.SimpleJsonResults + if !assert.NoError(t, json.Unmarshal(content, &results)) { + return formats.SimpleJsonResults{} + } + // replace paths separators + for _, vulnerability := range results.Vulnerabilities { + convertScaSimpleJsonPathsForOS(&vulnerability.Components, &vulnerability.ImpactPaths, &vulnerability.ImpactedDependencyDetails, &vulnerability.Cves) + } + for _, violation := range results.SecurityViolations { + convertScaSimpleJsonPathsForOS(&violation.Components, &violation.ImpactPaths, &violation.ImpactedDependencyDetails, &violation.Cves) + } + for _, licenseViolation := range results.LicensesViolations { + convertScaSimpleJsonPathsForOS(&licenseViolation.Components, &licenseViolation.ImpactPaths, &licenseViolation.ImpactedDependencyDetails, nil) + } + for _, orViolation := range results.OperationalRiskViolations { + convertScaSimpleJsonPathsForOS(&orViolation.Components, nil, &orViolation.ImpactedDependencyDetails, nil) + } + for _, secret := range results.Secrets { + convertJasSimpleJsonPathsForOS(&secret) + } + for _, sast := range results.Sast { + convertJasSimpleJsonPathsForOS(&sast) + } + for _, iac := range results.Iacs { + convertJasSimpleJsonPathsForOS(&iac) + } + return results +} + +func convertJasSimpleJsonPathsForOS(jas *formats.SourceCodeRow) { + if jas == nil { + return + } + jas.Location.File = getJasConvertedPath(jas.Location.File) + if jas.Applicability != nil { + for i := range jas.Applicability.Evidence { + jas.Applicability.Evidence[i].Location.File = getJasConvertedPath(jas.Applicability.Evidence[i].Location.File) + } + } + for i := range jas.CodeFlow { + for j := range jas.CodeFlow[i] { + jas.CodeFlow[i][j].File = getJasConvertedPath(jas.CodeFlow[i][j].File) + } + } +} + +func convertScaSimpleJsonPathsForOS(potentialComponents *[]formats.ComponentRow, potentialImpactPaths *[][]formats.ComponentRow, potentialImpactedDependencyDetails *formats.ImpactedDependencyDetails, potentialCves *[]formats.CveRow) { + if potentialComponents != nil { + components := *potentialComponents + for i := range components { + if components[i].Location != nil { + components[i].Location.File = filepath.FromSlash(components[i].Location.File) + } + } + } + if potentialImpactPaths != nil { + impactPaths := *potentialImpactPaths + for i := range impactPaths { + for j := range impactPaths[i] { + if impactPaths[i][j].Location != nil { + impactPaths[i][j].Location.File = filepath.FromSlash(impactPaths[i][j].Location.File) + } + } + } + } + if potentialImpactedDependencyDetails != nil { + impactedDependencyDetails := *potentialImpactedDependencyDetails + for i := range impactedDependencyDetails.Components { + if impactedDependencyDetails.Components[i].Location != nil { + impactedDependencyDetails.Components[i].Location.File = filepath.FromSlash(impactedDependencyDetails.Components[i].Location.File) + } + } + } + if potentialCves != nil { + cves := *potentialCves + for i := range cves { + if cves[i].Applicability != nil { + for i := range cves[i].Applicability.Evidence { + cves[i].Applicability.Evidence[i].Location.File = filepath.FromSlash(cves[i].Applicability.Evidence[i].Location.File) + } + } + } + } +} + +func ReadSarifResults(t *testing.T, path string) *sarif.Report { + content, err := os.ReadFile(path) + require.NoError(t, err) + var results *sarif.Report + if !assert.NoError(t, json.Unmarshal(content, &results)) { + return &sarif.Report{} + } + // replace paths separators + convertSarifRunPathsForOS(results.Runs...) + return results +} + +func ReadSummaryResults(t *testing.T, path string) formats.ResultsSummary { + content, err := os.ReadFile(path) + require.NoError(t, err) + var results formats.ResultsSummary + if !assert.NoError(t, json.Unmarshal(content, &results)) { + return formats.ResultsSummary{} + } + // replace paths separators + for _, targetResults := range results.Scans { + targetResults.Target = filepath.FromSlash(targetResults.Target) + } + return results +} + +func getJasConvertedPath(pathToConvert string) string { + return filepath.FromSlash(strings.TrimPrefix(pathToConvert, "file://")) +} + func CreateTestWatch(t *testing.T, policyName string, watchName, severity xrayUtils.Severity) (string, func()) { xrayManager, err := xray.CreateXrayServiceManager(configTests.XrDetails) require.NoError(t, err) diff --git a/tests/utils/test_validation.go b/tests/utils/test_validation.go deleted file mode 100644 index dcb46463..00000000 --- a/tests/utils/test_validation.go +++ /dev/null @@ -1,106 +0,0 @@ -package utils - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/utils/jasutils" - - clientUtils "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/xray/services" -) - -func ValidateXrayVersion(t *testing.T, minVersion string) { - xrayVersion, err := getXrayVersion() - if err != nil { - assert.NoError(t, err) - return - } - err = clientUtils.ValidateMinimumVersion(clientUtils.Xray, xrayVersion.GetVersion(), minVersion) - if err != nil { - t.Skip(err) - } -} - -func ValidateXscVersion(t *testing.T, minVersion string) { - xscVersion, err := getXscVersion() - if err != nil { - t.Skip(err) - } - err = clientUtils.ValidateMinimumVersion(clientUtils.Xsc, xscVersion.GetVersion(), minVersion) - if err != nil { - t.Skip(err) - } -} - -func VerifyJsonScanResults(t *testing.T, content string, minViolations, minVulnerabilities, minLicenses int) { - var results []services.ScanResponse - err := json.Unmarshal([]byte(content), &results) - if assert.NoError(t, err) { - var violations []services.Violation - var vulnerabilities []services.Vulnerability - var licenses []services.License - for _, result := range results { - violations = append(violations, result.Violations...) - vulnerabilities = append(vulnerabilities, result.Vulnerabilities...) - licenses = append(licenses, result.Licenses...) - } - assert.True(t, len(violations) >= minViolations, fmt.Sprintf("Expected at least %d violations in scan results, but got %d violations.", minViolations, len(violations))) - assert.True(t, len(vulnerabilities) >= minVulnerabilities, fmt.Sprintf("Expected at least %d vulnerabilities in scan results, but got %d vulnerabilities.", minVulnerabilities, len(vulnerabilities))) - assert.True(t, len(licenses) >= minLicenses, fmt.Sprintf("Expected at least %d Licenses in scan results, but got %d Licenses.", minLicenses, len(licenses))) - } -} - -func VerifySimpleJsonScanResults(t *testing.T, content string, minViolations, minVulnerabilities, minLicenses int) { - var results formats.SimpleJsonResults - err := json.Unmarshal([]byte(content), &results) - if assert.NoError(t, err) { - assert.GreaterOrEqual(t, len(results.SecurityViolations), minViolations) - assert.GreaterOrEqual(t, len(results.Vulnerabilities), minVulnerabilities) - assert.GreaterOrEqual(t, len(results.Licenses), minLicenses) - } -} - -func VerifySimpleJsonJasResults(t *testing.T, content string, minSastViolations, minIacViolations, minSecrets, - minApplicable, minUndetermined, minNotCovered, minNotApplicable, minMissingContext, minInactives int) { - var results formats.SimpleJsonResults - err := json.Unmarshal([]byte(content), &results) - if assert.NoError(t, err) { - assert.GreaterOrEqual(t, len(results.Sast), minSastViolations, "Found less sast then expected") - assert.GreaterOrEqual(t, len(results.Secrets), minSecrets, "Found less secrets then expected") - assert.GreaterOrEqual(t, len(results.Iacs), minIacViolations, "Found less IaC then expected") - var applicableResults, undeterminedResults, notCoveredResults, notApplicableResults, missingContextResults int - for _, vuln := range results.Vulnerabilities { - switch vuln.Applicable { - case string(jasutils.NotApplicable): - notApplicableResults++ - case string(jasutils.Applicable): - applicableResults++ - case string(jasutils.NotCovered): - notCoveredResults++ - case string(jasutils.ApplicabilityUndetermined): - undeterminedResults++ - case string(jasutils.MissingContext): - missingContextResults++ - } - } - countInactives := 0 - for _, result := range results.Secrets { - if result.Applicability != nil { - if result.Applicability.Status == "Inactive" { - countInactives += 1 - } - } - } - assert.GreaterOrEqual(t, countInactives, minInactives) - assert.GreaterOrEqual(t, applicableResults, minApplicable, "Found less applicableResults then expected") - assert.GreaterOrEqual(t, undeterminedResults, minUndetermined, "Found less undeterminedResults then expected") - assert.GreaterOrEqual(t, notCoveredResults, minNotCovered, "Found less notCoveredResults then expected") - assert.GreaterOrEqual(t, notApplicableResults, minNotApplicable, "Found less notApplicableResults then expected") - assert.GreaterOrEqual(t, missingContextResults, minMissingContext, "Found less missingContextResults then expected") - } -} diff --git a/utils/auditbasicparams.go b/utils/auditbasicparams.go index 2e315119..b7543643 100644 --- a/utils/auditbasicparams.go +++ b/utils/auditbasicparams.go @@ -68,6 +68,7 @@ type AuditBasicParams struct { exclusions []string isRecursiveScan bool skipAutoInstall bool + allowPartialResults bool } func (abp *AuditBasicParams) DirectDependencies() *[]string { @@ -108,6 +109,11 @@ func (abp *AuditBasicParams) SetSkipAutoInstall(skipAutoInstall bool) *AuditBasi return abp } +func (abp *AuditBasicParams) SetAllowPartialResults(allowPartialResults bool) *AuditBasicParams { + abp.allowPartialResults = allowPartialResults + return abp +} + func (abp *AuditBasicParams) UseJas() bool { return abp.useJas } @@ -276,3 +282,7 @@ func (abp *AuditBasicParams) IsRecursiveScan() bool { func (abp *AuditBasicParams) SkipAutoInstall() bool { return abp.skipAutoInstall } + +func (abp *AuditBasicParams) AllowPartialResults() bool { + return abp.allowPartialResults +} diff --git a/formats/conversion.go b/utils/formats/conversion.go similarity index 59% rename from formats/conversion.go rename to utils/formats/conversion.go index 49c97e8e..f53a4518 100644 --- a/formats/conversion.go +++ b/utils/formats/conversion.go @@ -6,6 +6,84 @@ import ( "strings" ) +func ConvertSecurityTableRowToScanTableRow(tableRows []vulnerabilityTableRow) (scanTableRows []vulnerabilityScanTableRow) { + for i := range tableRows { + scanTableRows = append(scanTableRows, vulnerabilityScanTableRow{ + severity: tableRows[i].severity, + severityNumValue: tableRows[i].severityNumValue, + applicable: tableRows[i].applicable, + impactedPackageName: tableRows[i].impactedDependencyName, + impactedPackageVersion: tableRows[i].impactedDependencyVersion, + ImpactedPackageType: tableRows[i].impactedDependencyType, + fixedVersions: tableRows[i].fixedVersions, + directPackages: convertToComponentScanTableRow(tableRows[i].directDependencies), + cves: tableRows[i].cves, + issueId: tableRows[i].issueId, + }) + } + return +} + +func ConvertLicenseViolationTableRowToScanTableRow(tableRows []licenseViolationTableRow) (scanTableRows []licenseViolationScanTableRow) { + for i := range tableRows { + scanTableRows = append(scanTableRows, licenseViolationScanTableRow{ + licenseKey: tableRows[i].licenseKey, + severity: tableRows[i].severity, + severityNumValue: tableRows[i].severityNumValue, + impactedPackageName: tableRows[i].impactedDependencyName, + impactedPackageVersion: tableRows[i].impactedDependencyVersion, + impactedDependencyType: tableRows[i].impactedDependencyType, + directDependencies: convertToComponentScanTableRow(tableRows[i].directDependencies), + }) + } + return +} + +func ConvertOperationalRiskTableRowToScanTableRow(tableRows []operationalRiskViolationTableRow) (scanTableRows []operationalRiskViolationScanTableRow) { + for i := range tableRows { + scanTableRows = append(scanTableRows, operationalRiskViolationScanTableRow{ + Severity: tableRows[i].Severity, + severityNumValue: tableRows[i].severityNumValue, + impactedPackageName: tableRows[i].impactedDependencyName, + impactedPackageVersion: tableRows[i].impactedDependencyVersion, + impactedDependencyType: tableRows[i].impactedDependencyType, + directDependencies: convertToComponentScanTableRow(tableRows[i].directDependencies), + isEol: tableRows[i].isEol, + cadence: tableRows[i].cadence, + commits: tableRows[i].Commits, + committers: tableRows[i].committers, + newerVersions: tableRows[i].newerVersions, + latestVersion: tableRows[i].latestVersion, + riskReason: tableRows[i].riskReason, + eolMessage: tableRows[i].eolMessage, + }) + } + return +} + +func ConvertLicenseTableRowToScanTableRow(tableRows []licenseTableRow) (scanTableRows []licenseScanTableRow) { + for i := range tableRows { + scanTableRows = append(scanTableRows, licenseScanTableRow{ + licenseKey: tableRows[i].licenseKey, + directDependencies: convertToComponentScanTableRow(tableRows[i].directDependencies), + impactedPackageName: tableRows[i].impactedDependencyName, + impactedPackageVersion: tableRows[i].impactedDependencyVersion, + impactedDependencyType: tableRows[i].impactedDependencyType, + }) + } + return +} + +func convertToComponentScanTableRow(rows []directDependenciesTableRow) (tableRows []directPackagesTableRow) { + for i := range rows { + tableRows = append(tableRows, directPackagesTableRow{ + name: rows[i].name, + version: rows[i].version, + }) + } + return +} + func ConvertToVulnerabilityTableRow(rows []VulnerabilityOrViolationRow) (tableRows []vulnerabilityTableRow) { for i := range rows { tableRows = append(tableRows, vulnerabilityTableRow{ @@ -24,24 +102,6 @@ func ConvertToVulnerabilityTableRow(rows []VulnerabilityOrViolationRow) (tableRo return } -func ConvertToVulnerabilityScanTableRow(rows []VulnerabilityOrViolationRow) (tableRows []vulnerabilityScanTableRow) { - for i := range rows { - tableRows = append(tableRows, vulnerabilityScanTableRow{ - severity: rows[i].Severity, - severityNumValue: rows[i].SeverityNumValue, - applicable: rows[i].Applicable, - impactedPackageName: rows[i].ImpactedDependencyName, - impactedPackageVersion: rows[i].ImpactedDependencyVersion, - ImpactedPackageType: rows[i].ImpactedDependencyType, - fixedVersions: strings.Join(rows[i].FixedVersions, "\n"), - directPackages: convertToComponentScanTableRow(rows[i].Components), - cves: convertToCveTableRow(rows[i].Cves), - issueId: rows[i].IssueId, - }) - } - return -} - func ConvertToLicenseViolationTableRow(rows []LicenseRow) (tableRows []licenseViolationTableRow) { for i := range rows { tableRows = append(tableRows, licenseViolationTableRow{ @@ -57,21 +117,6 @@ func ConvertToLicenseViolationTableRow(rows []LicenseRow) (tableRows []licenseVi return } -func ConvertToLicenseViolationScanTableRow(rows []LicenseRow) (tableRows []licenseViolationScanTableRow) { - for i := range rows { - tableRows = append(tableRows, licenseViolationScanTableRow{ - licenseKey: rows[i].LicenseKey, - severity: rows[i].Severity, - severityNumValue: rows[i].SeverityNumValue, - impactedPackageName: rows[i].ImpactedDependencyName, - impactedPackageVersion: rows[i].ImpactedDependencyVersion, - impactedDependencyType: rows[i].ImpactedDependencyType, - directDependencies: convertToComponentScanTableRow(rows[i].Components), - }) - } - return -} - func ConvertToLicenseTableRow(rows []LicenseRow) (tableRows []licenseTableRow) { for i := range rows { tableRows = append(tableRows, licenseTableRow{ @@ -85,19 +130,6 @@ func ConvertToLicenseTableRow(rows []LicenseRow) (tableRows []licenseTableRow) { return } -func ConvertToLicenseScanTableRow(rows []LicenseRow) (tableRows []licenseScanTableRow) { - for i := range rows { - tableRows = append(tableRows, licenseScanTableRow{ - licenseKey: rows[i].LicenseKey, - impactedPackageName: rows[i].ImpactedDependencyName, - impactedPackageVersion: rows[i].ImpactedDependencyVersion, - impactedDependencyType: rows[i].ImpactedDependencyType, - directDependencies: convertToComponentScanTableRow(rows[i].Components), - }) - } - return -} - func ConvertToOperationalRiskViolationTableRow(rows []OperationalRiskViolationRow) (tableRows []operationalRiskViolationTableRow) { for i := range rows { tableRows = append(tableRows, operationalRiskViolationTableRow{ @@ -120,28 +152,6 @@ func ConvertToOperationalRiskViolationTableRow(rows []OperationalRiskViolationRo return } -func ConvertToOperationalRiskViolationScanTableRow(rows []OperationalRiskViolationRow) (tableRows []operationalRiskViolationScanTableRow) { - for i := range rows { - tableRows = append(tableRows, operationalRiskViolationScanTableRow{ - Severity: rows[i].Severity, - severityNumValue: rows[i].SeverityNumValue, - impactedPackageName: rows[i].ImpactedDependencyName, - impactedPackageVersion: rows[i].ImpactedDependencyVersion, - impactedDependencyType: rows[i].ImpactedDependencyType, - directDependencies: convertToComponentScanTableRow(rows[i].Components), - isEol: rows[i].IsEol, - cadence: rows[i].Cadence, - commits: rows[i].Commits, - committers: rows[i].Committers, - newerVersions: rows[i].NewerVersions, - latestVersion: rows[i].LatestVersion, - riskReason: rows[i].RiskReason, - eolMessage: rows[i].EolMessage, - }) - } - return -} - func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow) { for i := range rows { var status string @@ -185,16 +195,6 @@ func convertToComponentTableRow(rows []ComponentRow) (tableRows []directDependen return } -func convertToComponentScanTableRow(rows []ComponentRow) (tableRows []directPackagesTableRow) { - for i := range rows { - tableRows = append(tableRows, directPackagesTableRow{ - name: rows[i].Name, - version: rows[i].Version, - }) - } - return -} - func convertToCveTableRow(rows []CveRow) (tableRows []cveTableRow) { for i := range rows { tableRows = append(tableRows, cveTableRow{ diff --git a/formats/enrich_formats.go b/utils/formats/enrich_formats.go similarity index 100% rename from formats/enrich_formats.go rename to utils/formats/enrich_formats.go diff --git a/formats/sarifutils/sarifutils.go b/utils/formats/sarifutils/sarifutils.go similarity index 66% rename from formats/sarifutils/sarifutils.go rename to utils/formats/sarifutils/sarifutils.go index 3a5abe96..05604f84 100644 --- a/formats/sarifutils/sarifutils.go +++ b/utils/formats/sarifutils/sarifutils.go @@ -30,6 +30,68 @@ func CombineReports(reports ...*sarif.Report) (combined *sarif.Report, err error return } +func GetToolVersion(run *sarif.Run) string { + if run.Tool.Driver != nil && run.Tool.Driver.Version != nil { + return *run.Tool.Driver.Version + } + return "" +} + +func CopyRunMetadata(run *sarif.Run) (copied *sarif.Run) { + if run == nil { + return + } + copied = sarif.NewRun(*sarif.NewTool(sarif.NewDriver(GetRunToolName(run)))).WithInvocations(run.Invocations) + + if toolFullName := GetRunToolFullName(run); toolFullName != "" { + copied.Tool.Driver.FullName = &toolFullName + } + if toolVersion := GetToolVersion(run); toolVersion != "" { + copied.Tool.Driver.Version = &toolVersion + } + if fullDescription := GetRunToolFullDescription(run); fullDescription != "" { + SetRunToolFullDescriptionText(fullDescription, copied) + } + if fullDescriptionMarkdown := GetRunToolFullDescriptionMarkdown(run); fullDescriptionMarkdown != "" { + SetRunToolFullDescriptionMarkdown(fullDescriptionMarkdown, copied) + } + if language := getRunLanguage(run); language != "" { + copied.Language = &language + } + if informationURI := GetRunToolInformationURI(run); informationURI != "" { + copied.Tool.Driver.InformationURI = &informationURI + } + return +} + +func GetRunToolFullName(run *sarif.Run) string { + if run.Tool.Driver != nil && run.Tool.Driver.FullName != nil { + return *run.Tool.Driver.FullName + } + return "" +} + +func GetRunToolFullDescription(run *sarif.Run) string { + if run.Tool.Driver != nil && run.Tool.Driver.FullDescription != nil && run.Tool.Driver.FullDescription.Text != nil { + return *run.Tool.Driver.FullDescription.Text + } + return "" +} + +func getRunLanguage(run *sarif.Run) string { + if run.Language != nil { + return *run.Language + } + return "" +} + +func GetRunToolInformationURI(run *sarif.Run) string { + if run.Tool.Driver != nil && run.Tool.Driver.InformationURI != nil { + return *run.Tool.Driver.InformationURI + } + return "" +} + func NewPhysicalLocation(physicalPath string) *sarif.PhysicalLocation { return &sarif.PhysicalLocation{ ArtifactLocation: &sarif.ArtifactLocation{ @@ -66,6 +128,78 @@ func ReadScanRunsFromFile(fileName string) (sarifRuns []*sarif.Run, err error) { return } +func CopyResult(result *sarif.Result) *sarif.Result { + copied := &sarif.Result{ + RuleID: result.RuleID, + RuleIndex: result.RuleIndex, + Kind: result.Kind, + Fingerprints: result.Fingerprints, + CodeFlows: result.CodeFlows, + Level: result.Level, + Message: result.Message, + PropertyBag: result.PropertyBag, + } + for _, location := range result.Locations { + copied.Locations = append(copied.Locations, CopyLocation(location)) + } + return copied +} + +func copyStrAttribute(attr *string) *string { + if attr == nil { + return nil + } + copy := *attr + return © +} + +func copyIntAttribute(attr *int) *int { + if attr == nil { + return nil + } + copy := *attr + return © +} + +func CopyLocation(location *sarif.Location) *sarif.Location { + if location == nil { + return nil + } + copied := sarif.NewLocation() + if location.PhysicalLocation != nil { + copied.PhysicalLocation = &sarif.PhysicalLocation{} + if location.PhysicalLocation.ArtifactLocation != nil { + copied.PhysicalLocation.ArtifactLocation = &sarif.ArtifactLocation{ + URI: copyStrAttribute(location.PhysicalLocation.ArtifactLocation.URI), + } + } + if location.PhysicalLocation.Region != nil { + copied.PhysicalLocation.Region = &sarif.Region{ + StartLine: copyIntAttribute(location.PhysicalLocation.Region.StartLine), + StartColumn: copyIntAttribute(location.PhysicalLocation.Region.StartColumn), + EndLine: copyIntAttribute(location.PhysicalLocation.Region.EndLine), + EndColumn: copyIntAttribute(location.PhysicalLocation.Region.EndColumn), + } + if location.PhysicalLocation.Region.Snippet != nil { + copied.PhysicalLocation.Region.Snippet = &sarif.ArtifactContent{ + Text: copyStrAttribute(location.PhysicalLocation.Region.Snippet.Text), + } + } + } + } + copied.Properties = location.Properties + for _, logicalLocation := range location.LogicalLocations { + copied.LogicalLocations = append(copied.LogicalLocations, &sarif.LogicalLocation{ + Name: logicalLocation.Name, + FullyQualifiedName: logicalLocation.FullyQualifiedName, + DecoratedName: logicalLocation.DecoratedName, + Kind: logicalLocation.Kind, + PropertyBag: logicalLocation.PropertyBag, + }) + } + return copied +} + func AggregateMultipleRunsIntoSingle(runs []*sarif.Run, destination *sarif.Run) { if len(runs) == 0 { return @@ -88,17 +222,6 @@ func AggregateMultipleRunsIntoSingle(runs []*sarif.Run, destination *sarif.Run) } } -func GetResultProperty(key string, result *sarif.Result) string { - if result != nil && result.Properties != nil && result.Properties[key] != nil { - status, ok := result.Properties[key].(string) - if !ok { - return "" - } - return status - } - return "" -} - func GetLocationRelatedCodeFlowsFromResult(location *sarif.Location, result *sarif.Result) (codeFlows []*sarif.CodeFlow) { for _, codeFlow := range result.CodeFlows { for _, stackTrace := range codeFlow.ThreadFlows { @@ -135,7 +258,7 @@ func GetLogicalLocation(kind string, location *sarif.Location) *sarif.LogicalLoc func GetLocationId(location *sarif.Location) string { return fmt.Sprintf("%s:%s:%d:%d:%d:%d", GetLocationFileName(location), - GetLocationSnippet(location), + GetLocationSnippetText(location), GetLocationStartLine(location), GetLocationStartColumn(location), GetLocationEndLine(location), @@ -150,6 +273,13 @@ func SetRunToolName(toolName string, run *sarif.Run) { run.Tool.Driver.Name = toolName } +func GetRunToolName(run *sarif.Run) string { + if run.Tool.Driver != nil { + return run.Tool.Driver.Name + } + return "" +} + func SetRunToolFullDescriptionText(txt string, run *sarif.Run) { if run.Tool.Driver == nil { run.Tool.Driver = &sarif.ToolComponent{} @@ -185,13 +315,6 @@ func GetRunToolFullDescriptionMarkdown(run *sarif.Run) string { return "" } -func GetRunToolName(run *sarif.Run) string { - if run.Tool.Driver != nil { - return run.Tool.Driver.Name - } - return "" -} - func GetResultsLocationCount(runs ...*sarif.Run) (count int) { for _, run := range runs { for _, result := range run.Results { @@ -214,6 +337,15 @@ func GetRunsByWorkingDirectory(workingDirectory string, runs ...*sarif.Run) (fil return } +func GetRunsByToolName(report *sarif.Report, toolName string) (filteredRuns []*sarif.Run) { + for _, run := range report.Runs { + if run.Tool.Driver != nil && run.Tool.Driver.Name == toolName { + filteredRuns = append(filteredRuns, run) + } + } + return +} + func SetResultMsgMarkdown(markdown string, result *sarif.Result) { result.Message.Markdown = &markdown } @@ -239,6 +371,19 @@ func GetResultRuleId(result *sarif.Result) string { return "" } +func GetResultProperty(key string, result *sarif.Result) (value string) { + if result == nil || result.Properties == nil { + return + } + if _, exists := result.Properties[key]; !exists { + return + } + if value, ok := result.Properties[key].(string); ok { + return value + } + return +} + func IsFingerprintsExists(result *sarif.Result) bool { return len(result.Fingerprints) > 0 } @@ -253,19 +398,27 @@ func SetResultFingerprint(algorithm, value string, result *sarif.Result) { func GetResultLocationSnippets(result *sarif.Result) []string { var snippets []string for _, location := range result.Locations { - if snippet := GetLocationSnippet(location); snippet != "" { + if snippet := GetLocationSnippetText(location); snippet != "" { snippets = append(snippets, snippet) } } return snippets } -func GetLocationSnippet(location *sarif.Location) string { +func GetLocationSnippetText(location *sarif.Location) string { + snippetContent := GetLocationSnippet(location) + if snippetContent != nil && snippetContent.Text != nil { + return *snippetContent.Text + } + return "" +} + +func GetLocationSnippet(location *sarif.Location) *sarif.ArtifactContent { region := getLocationRegion(location) if region != nil && region.Snippet != nil { - return *region.Snippet.Text + return region.Snippet } - return "" + return nil } func SetLocationSnippet(location *sarif.Location, snippet string) { @@ -385,6 +538,51 @@ func IsResultKindNotPass(result *sarif.Result) bool { return !(result.Kind != nil && *result.Kind == "pass") } +func GetRuleById(run *sarif.Run, ruleId string) *sarif.ReportingDescriptor { + for _, rule := range GetRunRules(run) { + if rule.ID == ruleId { + return rule + } + } + return nil +} + +func GetRuleFullDescription(rule *sarif.ReportingDescriptor) string { + if rule.FullDescription != nil && rule.FullDescription.Text != nil { + return *rule.FullDescription.Text + } + return "" +} + +func GetRuleFullDescriptionMarkdown(rule *sarif.ReportingDescriptor) string { + if rule.FullDescription != nil && rule.FullDescription.Markdown != nil { + return *rule.FullDescription.Markdown + } + return "" + +} + +func GetRuleHelp(rule *sarif.ReportingDescriptor) string { + if rule.Help != nil && rule.Help.Text != nil { + return *rule.Help.Text + } + return "" +} + +func GetRuleHelpMarkdown(rule *sarif.ReportingDescriptor) string { + if rule.Help != nil && rule.Help.Markdown != nil { + return *rule.Help.Markdown + } + return "" +} + +func GetRuleShortDescription(rule *sarif.ReportingDescriptor) string { + if rule.ShortDescription != nil && rule.ShortDescription.Text != nil { + return *rule.ShortDescription.Text + } + return "" +} + func GetRuleFullDescriptionText(rule *sarif.ReportingDescriptor) string { if rule.FullDescription != nil && rule.FullDescription.Text != nil { return *rule.FullDescription.Text diff --git a/formats/sarifutils/sarifutils_test.go b/utils/formats/sarifutils/sarifutils_test.go similarity index 98% rename from formats/sarifutils/sarifutils_test.go rename to utils/formats/sarifutils/sarifutils_test.go index 69c54723..11652d1e 100644 --- a/formats/sarifutils/sarifutils_test.go +++ b/utils/formats/sarifutils/sarifutils_test.go @@ -208,7 +208,7 @@ func TestGetResultMsgText(t *testing.T) { } } -func TestGetLocationSnippet(t *testing.T) { +func TestGetLocationSnippetText(t *testing.T) { tests := []struct { location *sarif.Location expectedOutput string @@ -224,7 +224,7 @@ func TestGetLocationSnippet(t *testing.T) { } for _, test := range tests { - assert.Equal(t, test.expectedOutput, GetLocationSnippet(test.location)) + assert.Equal(t, test.expectedOutput, GetLocationSnippetText(test.location)) } } @@ -245,7 +245,7 @@ func TestSetLocationSnippet(t *testing.T) { for _, test := range tests { SetLocationSnippet(test.location, test.expectedOutput) - assert.Equal(t, test.expectedOutput, GetLocationSnippet(test.location)) + assert.Equal(t, test.expectedOutput, GetLocationSnippetText(test.location)) } } diff --git a/formats/sarifutils/test_sarifutils.go b/utils/formats/sarifutils/test_sarifutils.go similarity index 90% rename from formats/sarifutils/test_sarifutils.go rename to utils/formats/sarifutils/test_sarifutils.go index d044de78..78cac692 100644 --- a/formats/sarifutils/test_sarifutils.go +++ b/utils/formats/sarifutils/test_sarifutils.go @@ -12,11 +12,10 @@ func CreateRunWithDummyResults(results ...*sarif.Result) *sarif.Run { return createRunWithDummyResults("", results...) } -func CreateDummyDriver(toolName, infoURI string, rules ...*sarif.ReportingDescriptor) *sarif.ToolComponent { +func CreateDummyDriver(toolName string, rules ...*sarif.ReportingDescriptor) *sarif.ToolComponent { return &sarif.ToolComponent{ - Name: toolName, - InformationURI: &infoURI, - Rules: rules, + Name: toolName, + Rules: rules, } } @@ -25,7 +24,7 @@ func CreateRunNameWithResults(toolName string, results ...*sarif.Result) *sarif. } func createRunWithDummyResults(toolName string, results ...*sarif.Result) *sarif.Run { - run := sarif.NewRunWithInformationURI(toolName, "") + run := sarif.NewRun(*sarif.NewSimpleTool(toolName)) for _, result := range results { if result.RuleID != nil { run.AddRule(*result.RuleID) @@ -39,14 +38,14 @@ func CreateRunWithDummyResultAndRuleProperties(result *sarif.Result, properties, if len(properties) != len(values) { return nil } - run := sarif.NewRunWithInformationURI("", "") - if result.RuleID != nil { - run.AddRule(*result.RuleID) + run := CreateRunWithDummyResults(result) + rule := GetRuleById(run, GetResultRuleId(result)) + if rule == nil { + return nil } - run.AddResult(result) - run.Tool.Driver.Rules[0].Properties = make(sarif.Properties, len(properties)) + rule.Properties = map[string]interface{}{} for index := range properties { - run.Tool.Driver.Rules[0].Properties[properties[index]] = values[index] + rule.Properties[properties[index]] = values[index] } return run } diff --git a/formats/simplejsonapi.go b/utils/formats/simplejsonapi.go similarity index 97% rename from formats/simplejsonapi.go rename to utils/formats/simplejsonapi.go index 1da5b1f8..a322b758 100644 --- a/formats/simplejsonapi.go +++ b/utils/formats/simplejsonapi.go @@ -84,8 +84,9 @@ type Location struct { } type ComponentRow struct { - Name string `json:"name"` - Version string `json:"version"` + Name string `json:"name"` + Version string `json:"version"` + Location *Location `json:"location,omitempty"` } type CveRow struct { diff --git a/formats/summary.go b/utils/formats/summary.go similarity index 99% rename from formats/summary.go rename to utils/formats/summary.go index 4e16fe0c..60d1c2b0 100644 --- a/formats/summary.go +++ b/utils/formats/summary.go @@ -29,6 +29,7 @@ type ResultsSummary struct { type ScanSummary struct { Target string `json:"target"` + Name string `json:"name,omitempty"` Vulnerabilities *ScanResultSummary `json:"vulnerabilities,omitempty"` Violations *ScanViolationsSummary `json:"violations,omitempty"` CuratedPackages *CuratedPackages `json:"curated,omitempty"` diff --git a/formats/summary_test.go b/utils/formats/summary_test.go similarity index 100% rename from formats/summary_test.go rename to utils/formats/summary_test.go diff --git a/formats/table.go b/utils/formats/table.go similarity index 93% rename from formats/table.go rename to utils/formats/table.go index 1734463e..a2c258f8 100644 --- a/formats/table.go +++ b/utils/formats/table.go @@ -4,6 +4,18 @@ package formats // Annotations are as described in the tableutils.PrintTable description. // Use the conversion methods in this package to convert from the API structs to the table structs. +type ResultsTables struct { + SecurityVulnerabilitiesTable []vulnerabilityTableRow + SecurityViolationsTable []vulnerabilityTableRow + LicensesTable []licenseTableRow + LicenseViolationsTable []licenseViolationTableRow + OperationalRiskViolationsTable []operationalRiskViolationTableRow + IacTable []iacOrSastTableRow + SastTable []iacOrSastTableRow + SecretsTable []secretsTableRow + Errors []error +} + // Used for vulnerabilities and security violations type vulnerabilityTableRow struct { severity string `col-name:"Severity"` diff --git a/utils/jasutils/jasutils.go b/utils/jasutils/jasutils.go index a889864d..3c2d11c8 100644 --- a/utils/jasutils/jasutils.go +++ b/utils/jasutils/jasutils.go @@ -4,18 +4,15 @@ import ( "strings" "github.com/gookit/color" + "github.com/jfrog/jfrog-cli-security/utils" ) const ( - ApplicabilityRuleIdPrefix = "applic_" -) + ApplicabilityRuleIdPrefix = "applic_" + ApplicabilitySarifPropertyKey = "applicability" -const ( DynamicTokenValidationMinXrayVersion = "3.101.0" -) - -const ( - TokenValidationStatusForNonTokens = "Not a token" + TokenValidationStatusForNonTokens = "Not a token" ) const ( @@ -41,6 +38,10 @@ func (jst JasScanType) String() string { return string(jst) } +func GetJasScanTypes() []JasScanType { + return []JasScanType{Applicability, Secrets, IaC, Sast} +} + func (tvs TokenValidationStatus) String() string { return string(tvs) } func (tvs TokenValidationStatus) ToString() string { @@ -85,6 +86,20 @@ func (as ApplicabilityStatus) ToString(pretty bool) string { } } +func SubScanTypeToJasScanType(subScanType utils.SubScanType) JasScanType { + switch subScanType { + case utils.SastScan: + return Sast + case utils.IacScan: + return IaC + case utils.SecretsScan: + return Secrets + case utils.ContextualAnalysisScan: + return Applicability + } + return "" +} + func ConvertToApplicabilityStatus(status string) ApplicabilityStatus { switch status { case Applicable.String(): diff --git a/utils/results.go b/utils/results.go deleted file mode 100644 index e78e2cf7..00000000 --- a/utils/results.go +++ /dev/null @@ -1,135 +0,0 @@ -package utils - -import ( - "github.com/jfrog/gofrog/datastructures" - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" - "github.com/jfrog/jfrog-cli-security/utils/techutils" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/owenrumney/go-sarif/v2/sarif" -) - -type Results struct { - ResultType CommandType - ScaResults []*ScaScanResult - XrayVersion string - ScansErr error - - ExtendedScanResults *ExtendedScanResults - - MultiScanId string -} - -func NewAuditResults(resultType CommandType) *Results { - return &Results{ResultType: resultType, ExtendedScanResults: &ExtendedScanResults{}} -} - -func (r *Results) GetScaScansXrayResults() (results []services.ScanResponse) { - for _, scaResult := range r.ScaResults { - results = append(results, scaResult.XrayResults...) - } - return -} - -func (r *Results) GetScaScannedTechnologies() []techutils.Technology { - technologies := datastructures.MakeSet[techutils.Technology]() - for _, scaResult := range r.ScaResults { - technologies.Add(scaResult.Technology) - } - return technologies.ToSlice() -} - -func (r *Results) IsMultipleProject() bool { - if len(r.ScaResults) == 0 { - return false - } - if len(r.ScaResults) == 1 { - if r.ScaResults[0].IsMultipleRootProject == nil { - return false - } - return *r.ScaResults[0].IsMultipleRootProject - } - return true -} - -func (r *Results) IsScaIssuesFound() bool { - for _, scan := range r.ScaResults { - if scan.HasInformation() { - return true - } - } - return false -} - -func (r *Results) getScaScanResultByTarget(target string) *ScaScanResult { - for _, scan := range r.ScaResults { - if scan.Target == target { - return scan - } - } - return nil -} - -func (r *Results) IsIssuesFound() bool { - if r.IsScaIssuesFound() { - return true - } - if r.ExtendedScanResults.IsIssuesFound() { - return true - } - return false -} - -// Counts the total number of unique findings in the provided results. -// A unique SCA finding is identified by a unique pair of vulnerability's/violation's issueId and component id or by a result returned from one of JAS scans. -func (r *Results) CountScanResultsFindings(includeVulnerabilities, includeViolations bool) (total int) { - summary := formats.ResultsSummary{Scans: GetScanSummaryByTargets(r, includeVulnerabilities, includeViolations)} - if summary.HasViolations() { - return summary.GetTotalViolations() - } - return summary.GetTotalVulnerabilities() -} - -type ScaScanResult struct { - // Could be working directory (audit), file path (binary scan) or build name+number (build scan) - Target string `json:"Target"` - Name string `json:"Name,omitempty"` - Technology techutils.Technology `json:"Technology,omitempty"` - XrayResults []services.ScanResponse `json:"XrayResults,omitempty"` - Descriptors []string `json:"Descriptors,omitempty"` - IsMultipleRootProject *bool `json:"IsMultipleRootProject,omitempty"` -} - -func (s ScaScanResult) HasInformation() bool { - for _, scan := range s.XrayResults { - if len(scan.Vulnerabilities) > 0 || len(scan.Violations) > 0 || len(scan.Licenses) > 0 { - return true - } - } - return false -} - -type ExtendedScanResults struct { - ApplicabilityScanResults []*sarif.Run - SecretsScanResults []*sarif.Run - IacScanResults []*sarif.Run - SastScanResults []*sarif.Run - EntitledForJas bool - SecretValidation bool -} - -func (e *ExtendedScanResults) IsIssuesFound() bool { - return sarifutils.GetResultsLocationCount(e.ApplicabilityScanResults...) > 0 || - sarifutils.GetResultsLocationCount(e.SecretsScanResults...) > 0 || - sarifutils.GetResultsLocationCount(e.IacScanResults...) > 0 || - sarifutils.GetResultsLocationCount(e.SastScanResults...) > 0 -} - -func (e *ExtendedScanResults) GetResultsForTarget(target string) (result *ExtendedScanResults) { - return &ExtendedScanResults{ - ApplicabilityScanResults: sarifutils.GetRunsByWorkingDirectory(target, e.ApplicabilityScanResults...), - SecretsScanResults: sarifutils.GetRunsByWorkingDirectory(target, e.SecretsScanResults...), - IacScanResults: sarifutils.GetRunsByWorkingDirectory(target, e.IacScanResults...), - SastScanResults: sarifutils.GetRunsByWorkingDirectory(target, e.SastScanResults...), - } -} diff --git a/utils/results/common.go b/utils/results/common.go new file mode 100644 index 00000000..7b73421a --- /dev/null +++ b/utils/results/common.go @@ -0,0 +1,625 @@ +package results + +import ( + "errors" + "fmt" + "path/filepath" + "strconv" + "strings" + + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" + "golang.org/x/exp/slices" +) + +const ( + customLicenseViolationId = "custom_license_violation" + RootIndex = 0 + DirectDependencyIndex = 1 + DirectDependencyPathLength = 2 + nodeModules = "node_modules" +) + +var ( + ErrResetConvertor = fmt.Errorf("reset must be called before parsing new scan results metadata") + ErrNoTargetConvertor = fmt.Errorf("ParseNewTargetResults must be called before starting to parse issues") +) + +func NewFailBuildError() error { + return coreutils.CliError{ExitCode: coreutils.ExitCodeVulnerableBuild, ErrorMsg: "One or more of the detected violations are configured to fail the build that including them"} +} + +// In case one (or more) of the violations contains the field FailBuild set to true, CliError with exit code 3 will be returned. +func CheckIfFailBuild(results []services.ScanResponse) bool { + for _, result := range results { + for _, violation := range result.Violations { + if violation.FailBuild { + return true + } + } + } + return false +} + +type ParseScaVulnerabilityFunc func(vulnerability services.Vulnerability, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error +type ParseScaViolationFunc func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error +type ParseLicensesFunc func(license services.License, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error +type ParseJasFunc func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) error + +// PrepareJasIssues allows to iterate over the provided SARIF runs and call the provided handler for each issue to process it. +func PrepareJasIssues(runs []*sarif.Run, entitledForJas bool, handler ParseJasFunc) error { + if !entitledForJas || handler == nil { + return nil + } + for _, run := range runs { + for _, result := range run.Results { + severity, err := severityutils.ParseSeverity(sarifutils.GetResultLevel(result), true) + if err != nil { + return err + } + rule, err := run.GetRuleById(sarifutils.GetResultRuleId(result)) + if errorutils.CheckError(err) != nil { + return err + } + if len(result.Locations) == 0 { + // If there are no locations, the issue is not specific to a location, and we should handle it as a general issue. + if err := handler(run, rule, severity, result, nil); err != nil { + return err + } + } else { + for _, location := range result.Locations { + if err := handler(run, rule, severity, result, location); err != nil { + return err + } + } + } + } + } + return nil +} + +// PrepareScaVulnerabilities allows to iterate over the provided SCA security vulnerabilities and call the provided handler for each impacted component/package with a vulnerability to process it. +func PrepareScaVulnerabilities(target ScanTarget, vulnerabilities []services.Vulnerability, entitledForJas bool, applicabilityRuns []*sarif.Run, handler ParseScaVulnerabilityFunc) error { + if handler == nil { + return nil + } + for _, vulnerability := range vulnerabilities { + impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes, fixedVersions, directComponents, impactPaths, err := SplitComponents(target.Target, vulnerability.Components) + if err != nil { + return err + } + cves, applicabilityStatus := ConvertCvesWithApplicability(vulnerability.Cves, entitledForJas, applicabilityRuns, vulnerability.Components) + severity, err := severityutils.ParseSeverity(vulnerability.Severity, false) + if err != nil { + return err + } + for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { + if err := handler( + vulnerability, cves, applicabilityStatus, severity, + impactedPackagesNames[compIndex], impactedPackagesVersions[compIndex], impactedPackagesTypes[compIndex], + fixedVersions[compIndex], directComponents[compIndex], impactPaths[compIndex], + ); err != nil { + return err + } + } + } + return nil +} + +// PrepareScaViolations allows to iterate over the provided SCA violations and call the provided handler for each impacted component/package with a violation to process it. +func PrepareScaViolations(target ScanTarget, violations []services.Violation, entitledForJas bool, applicabilityRuns []*sarif.Run, securityHandler ParseScaViolationFunc, licenseHandler ParseScaViolationFunc, operationalRiskHandler ParseScaViolationFunc) (watches []string, failBuild bool, err error) { + if securityHandler == nil && licenseHandler == nil && operationalRiskHandler == nil { + return + } + watchesSet := datastructures.MakeSet[string]() + for _, violation := range violations { + // Handle duplicates and general attributes + watchesSet.Add(violation.WatchName) + failBuild = failBuild || violation.FailBuild + // Prepare violation information + impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes, fixedVersions, directComponents, impactPaths, e := SplitComponents(target.Target, violation.Components) + if e != nil { + err = errors.Join(err, e) + continue + } + cves, applicabilityStatus := ConvertCvesWithApplicability(violation.Cves, entitledForJas, applicabilityRuns, violation.Components) + severity, e := severityutils.ParseSeverity(violation.Severity, false) + if e != nil { + err = errors.Join(err, e) + continue + } + // Parse the violation according to its type + switch violation.ViolationType { + case utils.ViolationTypeSecurity.String(): + if securityHandler == nil { + // No handler was provided for security violations + continue + } + for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { + if e := securityHandler( + violation, cves, applicabilityStatus, severity, + impactedPackagesNames[compIndex], impactedPackagesVersions[compIndex], impactedPackagesTypes[compIndex], + fixedVersions[compIndex], directComponents[compIndex], impactPaths[compIndex], + ); e != nil { + err = errors.Join(err, e) + continue + } + } + case utils.ViolationTypeLicense.String(): + if licenseHandler == nil { + // No handler was provided for license violations + continue + } + for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { + if e := licenseHandler( + violation, cves, applicabilityStatus, severity, + impactedPackagesNames[compIndex], impactedPackagesVersions[compIndex], impactedPackagesTypes[compIndex], + fixedVersions[compIndex], directComponents[compIndex], impactPaths[compIndex], + ); e != nil { + err = errors.Join(err, e) + continue + } + } + case utils.ViolationTypeOperationalRisk.String(): + if operationalRiskHandler == nil { + // No handler was provided for operational risk violations + continue + } + for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { + if e := operationalRiskHandler( + violation, cves, applicabilityStatus, severity, + impactedPackagesNames[compIndex], impactedPackagesVersions[compIndex], impactedPackagesTypes[compIndex], + fixedVersions[compIndex], directComponents[compIndex], impactPaths[compIndex], + ); e != nil { + err = errors.Join(err, e) + continue + } + } + } + } + watches = watchesSet.ToSlice() + return +} + +// PrepareLicenses allows to iterate over the provided licenses and call the provided handler for each component/package with a license to process it. +func PrepareLicenses(target ScanTarget, licenses []services.License, handler ParseLicensesFunc) error { + if handler == nil { + return nil + } + for _, license := range licenses { + impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes, _, directComponents, impactPaths, err := SplitComponents(target.Target, license.Components) + if err != nil { + return err + } + for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { + if err := handler( + license, impactedPackagesNames[compIndex], impactedPackagesVersions[compIndex], impactedPackagesTypes[compIndex], directComponents[compIndex], impactPaths[compIndex], + ); err != nil { + return err + } + } + } + return nil +} + +func SplitComponents(target string, impactedPackages map[string]services.Component) (impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes []string, fixedVersions [][]string, directComponents [][]formats.ComponentRow, impactPaths [][][]formats.ComponentRow, err error) { + if len(impactedPackages) == 0 { + err = errorutils.CheckErrorf("failed while parsing the response from Xray: violation doesn't have any components") + return + } + for currCompId, currComp := range impactedPackages { + currCompName, currCompVersion, currCompType := techutils.SplitComponentId(currCompId) + impactedPackagesNames = append(impactedPackagesNames, currCompName) + impactedPackagesVersions = append(impactedPackagesVersions, currCompVersion) + impactedPackagesTypes = append(impactedPackagesTypes, currCompType) + fixedVersions = append(fixedVersions, currComp.FixedVersions) + currDirectComponents, currImpactPaths := getDirectComponentsAndImpactPaths(target, currComp.ImpactPaths) + directComponents = append(directComponents, currDirectComponents) + impactPaths = append(impactPaths, currImpactPaths) + } + return +} + +// Gets a slice of the direct dependencies or packages of the scanned component, that depends on the vulnerable package, and converts the impact paths. +func getDirectComponentsAndImpactPaths(target string, impactPaths [][]services.ImpactPathNode) (components []formats.ComponentRow, impactPathsRows [][]formats.ComponentRow) { + componentsMap := make(map[string]formats.ComponentRow) + + // The first node in the impact path is the scanned component itself. The second one is the direct dependency. + impactPathLevel := 1 + for _, impactPath := range impactPaths { + impactPathIndex := impactPathLevel + if len(impactPath) <= impactPathLevel { + impactPathIndex = len(impactPath) - 1 + } + componentId := impactPath[impactPathIndex].ComponentId + if _, exist := componentsMap[componentId]; !exist { + compName, compVersion, _ := techutils.SplitComponentId(componentId) + componentsMap[componentId] = formats.ComponentRow{Name: compName, Version: compVersion, Location: getComponentLocation(impactPath[impactPathIndex].FullPath, target)} + } + + // Convert the impact path + var compImpactPathRows []formats.ComponentRow + for _, pathNode := range impactPath { + nodeCompName, nodeCompVersion, _ := techutils.SplitComponentId(pathNode.ComponentId) + compImpactPathRows = append(compImpactPathRows, formats.ComponentRow{ + Name: nodeCompName, + Version: nodeCompVersion, + Location: getComponentLocation(pathNode.FullPath), + }) + } + impactPathsRows = append(impactPathsRows, compImpactPathRows) + } + + for _, row := range componentsMap { + components = append(components, row) + } + return +} + +func getComponentLocation(pathsByPriority ...string) *formats.Location { + for _, path := range pathsByPriority { + if path != "" { + return &formats.Location{File: path} + } + } + return nil +} + +func GetIssueIdentifier(cvesRow []formats.CveRow, issueId string, delimiter string) string { + var cvesBuilder strings.Builder + for i, cve := range cvesRow { + if i > 0 { + cvesBuilder.WriteString(delimiter) + } + cvesBuilder.WriteString(cve.Id) + } + identifier := cvesBuilder.String() + if identifier == "" { + identifier = issueId + } + return identifier +} + +func ConvertCvesWithApplicability(cves []services.Cve, entitledForJas bool, applicabilityRuns []*sarif.Run, components map[string]services.Component) (convertedCves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus) { + convertedCves = convertCves(cves) + for i := range convertedCves { + convertedCves[i].Applicability = GetCveApplicabilityField(convertedCves[i].Id, applicabilityRuns, components) + } + applicabilityStatus = GetApplicableCveStatus(entitledForJas, applicabilityRuns, convertedCves) + return +} + +func convertCves(cves []services.Cve) []formats.CveRow { + var cveRows []formats.CveRow + for _, cveObj := range cves { + cveRows = append(cveRows, formats.CveRow{Id: cveObj.Id, CvssV2: cveObj.CvssV2Score, CvssV3: cveObj.CvssV3Score}) + } + return cveRows +} + +// FindMaxCVEScore returns the maximum CVSS score of the given CVEs or score based on severity and applicability status if not exists. +func FindMaxCVEScore(severity severityutils.Severity, applicabilityStatus jasutils.ApplicabilityStatus, cves []formats.CveRow) (string, error) { + if len(cves) == 0 { + return fmt.Sprintf("%.1f", severityutils.GetSeverityScore(severity, applicabilityStatus)), nil + } + maxCve := severityutils.MinCveScore + for _, cve := range cves { + cveScore, err := GetCveScore(severity, applicabilityStatus, cve) + if err != nil { + return "", err + } + if cveScore > maxCve { + maxCve = cveScore + } + // if found maximum possible cve score, no need to keep iterating + if maxCve == severityutils.MaxCveScore { + break + } + } + return fmt.Sprintf("%.1f", maxCve), nil +} + +// GetCveScore returns the CVSS score of the given CVE or score based on severity and applicability status if not exists. +func GetCveScore(severity severityutils.Severity, applicabilityStatus jasutils.ApplicabilityStatus, cve formats.CveRow) (float32, error) { + if cve.CvssV3 == "" { + return severityutils.GetSeverityScore(severity, applicabilityStatus), nil + } + score, err := strconv.ParseFloat(cve.CvssV3, 32) + return float32(score), err +} + +func GetViolatedLicenses(allowedLicenses []string, licenses []services.License) (violatedLicenses []services.Violation) { + if len(allowedLicenses) == 0 { + return + } + for _, license := range licenses { + if !slices.Contains(allowedLicenses, license.Key) { + violatedLicenses = append(violatedLicenses, services.Violation{ + LicenseKey: license.Key, + LicenseName: license.Name, + Severity: severityutils.Medium.String(), + Components: license.Components, + IssueId: customLicenseViolationId, + WatchName: fmt.Sprintf("jfrog_%s", customLicenseViolationId), + ViolationType: utils.ViolationTypeLicense.String(), + }) + } + } + return +} + +// AppendUniqueImpactPathsForMultipleRoots appends the source impact path to the target impact path while avoiding duplicates. +// Specifically, it is designed for handling multiple root projects, such as Maven or Gradle, by comparing each pair of paths and identifying the path that is closest to the direct dependency. +func AppendUniqueImpactPathsForMultipleRoots(target [][]services.ImpactPathNode, source [][]services.ImpactPathNode) [][]services.ImpactPathNode { + for targetPathIndex, targetPath := range target { + for sourcePathIndex, sourcePath := range source { + var subset []services.ImpactPathNode + if len(sourcePath) <= len(targetPath) { + subset = isImpactPathIsSubset(targetPath, sourcePath) + if len(subset) != 0 { + target[targetPathIndex] = subset + } + } else { + subset = isImpactPathIsSubset(sourcePath, targetPath) + if len(subset) != 0 { + source[sourcePathIndex] = subset + } + } + } + } + + return AppendUniqueImpactPaths(target, source, false) +} + +// isImpactPathIsSubset checks if targetPath is a subset of sourcePath, and returns the subset if exists +func isImpactPathIsSubset(target []services.ImpactPathNode, source []services.ImpactPathNode) []services.ImpactPathNode { + var subsetImpactPath []services.ImpactPathNode + impactPathNodesMap := make(map[string]bool) + for _, node := range target { + impactPathNodesMap[node.ComponentId] = true + } + + for _, node := range source { + if impactPathNodesMap[node.ComponentId] { + subsetImpactPath = append(subsetImpactPath, node) + } + } + + if len(subsetImpactPath) == len(target) || len(subsetImpactPath) == len(source) { + return subsetImpactPath + } + return []services.ImpactPathNode{} +} + +// appendImpactPathsWithoutDuplicates appends the elements of a source [][]ImpactPathNode struct to a target [][]ImpactPathNode, without adding any duplicate elements. +// This implementation uses the ComponentId field of the ImpactPathNode struct to check for duplicates, as it is guaranteed to be unique. +func AppendUniqueImpactPaths(target [][]services.ImpactPathNode, source [][]services.ImpactPathNode, multipleRoots bool) [][]services.ImpactPathNode { + if multipleRoots { + return AppendUniqueImpactPathsForMultipleRoots(target, source) + } + impactPathMap := make(map[string][]services.ImpactPathNode) + for _, path := range target { + // The first node component id is the key and the value is the whole path + key := getImpactPathKey(path) + impactPathMap[key] = path + } + + for _, path := range source { + key := getImpactPathKey(path) + if _, exists := impactPathMap[key]; !exists { + impactPathMap[key] = path + target = append(target, path) + } + } + return target +} + +// getImpactPathKey return a key that is used as a key to identify and deduplicate impact paths. +// If an impact path length is equal to directDependencyPathLength, then the direct dependency is the key, and it's in the directDependencyIndex place. +func getImpactPathKey(path []services.ImpactPathNode) string { + key := path[RootIndex].ComponentId + if len(path) == DirectDependencyPathLength { + key = path[DirectDependencyIndex].ComponentId + } + return key +} + +func GetCveApplicabilityField(cveId string, applicabilityScanResults []*sarif.Run, components map[string]services.Component) *formats.Applicability { + if len(applicabilityScanResults) == 0 { + return nil + } + applicability := formats.Applicability{} + resultFound := false + var applicabilityStatuses []jasutils.ApplicabilityStatus + for _, applicabilityRun := range applicabilityScanResults { + if rule, _ := applicabilityRun.GetRuleById(jasutils.CveToApplicabilityRuleId(cveId)); rule != nil { + applicability.ScannerDescription = sarifutils.GetRuleFullDescription(rule) + applicability.UndeterminedReason = GetRuleUndeterminedReason(rule) + status := getApplicabilityStatusFromRule(rule) + if status != "" { + applicabilityStatuses = append(applicabilityStatuses, status) + } + + } + result, _ := applicabilityRun.GetResultByRuleId(jasutils.CveToApplicabilityRuleId(cveId)) + if result == nil { + continue + } + resultFound = true + // Add new evidences from locations + for _, location := range result.Locations { + if evidence := getEvidence(components, result, location, applicabilityRun.Invocations...); evidence != nil { + applicability.Evidence = append(applicability.Evidence, *evidence) + } + } + } + switch { + case len(applicabilityStatuses) > 0: + applicability.Status = string(getFinalApplicabilityStatus(applicabilityStatuses)) + case !resultFound: + applicability.Status = string(jasutils.ApplicabilityUndetermined) + case len(applicability.Evidence) == 0: + applicability.Status = string(jasutils.NotApplicable) + default: + applicability.Status = string(jasutils.Applicable) + } + return &applicability +} + +func getEvidence(components map[string]services.Component, result *sarif.Result, location *sarif.Location, invocations ...*sarif.Invocation) *formats.Evidence { + fileName := sarifutils.GetRelativeLocationFileName(location, invocations) + if shouldDisqualifyEvidence(components, fileName) { + return nil + } + return &formats.Evidence{ + Location: formats.Location{ + File: fileName, + StartLine: sarifutils.GetLocationStartLine(location), + StartColumn: sarifutils.GetLocationStartColumn(location), + EndLine: sarifutils.GetLocationEndLine(location), + EndColumn: sarifutils.GetLocationEndColumn(location), + Snippet: sarifutils.GetLocationSnippetText(location), + }, + Reason: sarifutils.GetResultMsgText(result), + } +} + +func GetApplicableCveStatus(entitledForJas bool, applicabilityScanResults []*sarif.Run, cves []formats.CveRow) jasutils.ApplicabilityStatus { + if !entitledForJas || len(applicabilityScanResults) == 0 { + return jasutils.NotScanned + } + if len(cves) == 0 { + return jasutils.NotCovered + } + var applicableStatuses []jasutils.ApplicabilityStatus + for _, cve := range cves { + if cve.Applicability != nil { + applicableStatuses = append(applicableStatuses, jasutils.ApplicabilityStatus(cve.Applicability.Status)) + } + } + return getFinalApplicabilityStatus(applicableStatuses) +} + +func GetRuleUndeterminedReason(rule *sarif.ReportingDescriptor) string { + return sarifutils.GetRuleProperty("undetermined_reason", rule) +} + +func GetResultPropertyTokenValidation(result *sarif.Result) string { + return sarifutils.GetResultProperty("tokenValidation", result) +} + +func GetResultPropertyMetadata(result *sarif.Result) string { + return sarifutils.GetResultProperty("metadata", result) +} + +func getApplicabilityStatusFromRule(rule *sarif.ReportingDescriptor) jasutils.ApplicabilityStatus { + if rule.Properties[jasutils.ApplicabilitySarifPropertyKey] != nil { + status, ok := rule.Properties[jasutils.ApplicabilitySarifPropertyKey].(string) + if !ok { + log.Debug(fmt.Sprintf("Failed to get applicability status from rule properties for rule_id %s", rule.ID)) + } + switch status { + case "not_covered": + return jasutils.NotCovered + case "undetermined": + return jasutils.ApplicabilityUndetermined + case "not_applicable": + return jasutils.NotApplicable + case "applicable": + return jasutils.Applicable + case "missing_context": + return jasutils.MissingContext + } + } + return "" +} + +func GetDependencyId(depName, version string) string { + return fmt.Sprintf("%s:%s", depName, version) +} + +func GetScaIssueId(depName, version, issueId string) string { + return fmt.Sprintf("%s_%s_%s", issueId, depName, version) +} + +// GetUniqueKey returns a unique string key of format "vulnerableDependency:vulnerableVersion:xrayID:fixVersionExist" +func GetUniqueKey(vulnerableDependency, vulnerableVersion, xrayID string, fixVersionExist bool) string { + return strings.Join([]string{vulnerableDependency, vulnerableVersion, xrayID, strconv.FormatBool(fixVersionExist)}, ":") +} + +// Relevant only when "third-party-contextual-analysis" flag is on, +// which mean we scan the environment folders as well (node_modules for example...) +// When a certain package is reported applicable, and the evidence found +// is inside the source code of the same package, we should disqualify it. +// +// For example, +// Cve applicability was found inside the 'mquery' package. +// filePath = myProject/node_modules/mquery/badCode.js , disqualify = True. +// Disqualify the above evidence, as the reported applicability is used inside its own package. +// +// filePath = myProject/node_modules/mpath/badCode.js , disqualify = False. +// Found use of a badCode inside the node_modules from a different package, report applicable. +func shouldDisqualifyEvidence(components map[string]services.Component, evidenceFilePath string) (disqualify bool) { + for key := range components { + if !strings.HasPrefix(key, techutils.Npm.GetPackageTypeId()) { + return + } + dependencyName, _, _ := techutils.SplitComponentId(key) + // Check both Unix & Windows paths. + if strings.Contains(evidenceFilePath, nodeModules+"/"+dependencyName) || strings.Contains(evidenceFilePath, filepath.Join(nodeModules, dependencyName)) { + return true + } + } + return +} + +// If we don't get any statues it means the applicability scanner didn't run -> final value is not scanned +// If at least one cve is applicable -> final value is applicable +// Else if at least one cve is undetermined -> final value is undetermined +// Else if at least one cve is missing context -> final value is missing context +// Else if all cves are not covered -> final value is not covered +// Else (case when all cves aren't applicable) -> final value is not applicable +func getFinalApplicabilityStatus(applicabilityStatuses []jasutils.ApplicabilityStatus) jasutils.ApplicabilityStatus { + if len(applicabilityStatuses) == 0 { + return jasutils.NotScanned + } + foundUndetermined := false + foundMissingContext := false + foundNotCovered := false + for _, status := range applicabilityStatuses { + if status == jasutils.Applicable { + return jasutils.Applicable + } + if status == jasutils.ApplicabilityUndetermined { + foundUndetermined = true + } + if status == jasutils.MissingContext { + foundMissingContext = true + } + if status == jasutils.NotCovered { + foundNotCovered = true + } + + } + if foundUndetermined { + return jasutils.ApplicabilityUndetermined + } + if foundMissingContext { + return jasutils.MissingContext + } + if foundNotCovered { + return jasutils.NotCovered + } + + return jasutils.NotApplicable +} diff --git a/utils/results/common_test.go b/utils/results/common_test.go new file mode 100644 index 00000000..62607fac --- /dev/null +++ b/utils/results/common_test.go @@ -0,0 +1,702 @@ +package results + +import ( + "path/filepath" + "testing" + + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/stretchr/testify/assert" + + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-client-go/xray/services" +) + +func TestViolationFailBuild(t *testing.T) { + components := map[string]services.Component{"gav://antparent:ant:1.6.5": {}} + tests := []struct { + violations []services.Violation + expectedError bool + }{ + {[]services.Violation{{Components: components, FailBuild: false}, {Components: components, FailBuild: false}, {Components: components, FailBuild: false}}, false}, + {[]services.Violation{{Components: components, FailBuild: false}, {Components: components, FailBuild: true}, {Components: components, FailBuild: false}}, true}, + {[]services.Violation{{Components: components, FailBuild: true}, {Components: components, FailBuild: true}, {Components: components, FailBuild: true}}, true}, + } + + for _, test := range tests { + var err error + if CheckIfFailBuild([]services.ScanResponse{{Violations: test.violations}}) { + err = NewFailBuildError() + } + assert.Equal(t, test.expectedError, err != nil) + } +} + +func TestFindMaxCVEScore(t *testing.T) { + testCases := []struct { + name string + severity severityutils.Severity + status jasutils.ApplicabilityStatus + cves []formats.CveRow + expectedOutput string + expectedError bool + }{ + { + name: "CVEScore with valid float values", + severity: severityutils.High, + status: jasutils.Applicable, + cves: []formats.CveRow{{Id: "CVE-2021-1234", CvssV3: "7.5"}, {Id: "CVE-2021-5678", CvssV3: "9.2"}}, + expectedOutput: "9.2", + }, + { + name: "CVEScore with invalid float value", + severity: severityutils.High, + status: jasutils.Applicable, + cves: []formats.CveRow{{Id: "CVE-2022-4321", CvssV3: "invalid"}}, + expectedOutput: "", + expectedError: true, + }, + { + name: "CVEScore without values", + severity: severityutils.High, + status: jasutils.Applicable, + cves: []formats.CveRow{}, + expectedOutput: "8.9", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := FindMaxCVEScore(tc.severity, tc.status, tc.cves) + assert.False(t, tc.expectedError && err == nil) + assert.Equal(t, tc.expectedOutput, output) + }) + } +} + +func TestGetIssueIdentifier(t *testing.T) { + testCases := []struct { + name string + cves []formats.CveRow + delimiter string + issueId string + expectedOutput string + }{ + { + name: "Single CVE", + cves: []formats.CveRow{{Id: "CVE-2022-1234"}}, + delimiter: ",", + issueId: "XRAY-123456", + expectedOutput: "CVE-2022-1234", + }, + { + name: "Multiple CVEs", + cves: []formats.CveRow{{Id: "CVE-2022-1234"}, {Id: "CVE-2019-1234"}}, + delimiter: ", ", + issueId: "XRAY-123456", + expectedOutput: "CVE-2022-1234, CVE-2019-1234", + }, + { + name: "No CVEs", + cves: nil, + delimiter: ", ", + issueId: "XRAY-123456", + expectedOutput: "XRAY-123456", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output := GetIssueIdentifier(tc.cves, tc.issueId, tc.delimiter) + assert.Equal(t, tc.expectedOutput, output) + }) + } +} + +func TestIsImpactPathIsSubset(t *testing.T) { + testCases := []struct { + name string + target, source, expectedResult []services.ImpactPathNode + }{ + {"subset found in both target and source", + []services.ImpactPathNode{{ComponentId: "B"}, {ComponentId: "C"}}, + []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, + []services.ImpactPathNode{{ComponentId: "B"}, {ComponentId: "C"}}, + }, + {"subset not found in both target and source", + []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "D"}}, + []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, + []services.ImpactPathNode{}, + }, + {"target and source are identical", + []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}}, + []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}}, + []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := isImpactPathIsSubset(tc.target, tc.source) + assert.Equal(t, tc.expectedResult, result) + }) + } +} + +func TestAppendUniqueImpactPathsForMultipleRoots(t *testing.T) { + testCases := []struct { + name string + target [][]services.ImpactPathNode + source [][]services.ImpactPathNode + expectedResult [][]services.ImpactPathNode + }{ + { + name: "subset is found in both target and source", + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, + {{ComponentId: "D"}, {ComponentId: "E"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "B"}, {ComponentId: "C"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + expectedResult: [][]services.ImpactPathNode{ + {{ComponentId: "B"}, {ComponentId: "C"}}, + {{ComponentId: "D"}, {ComponentId: "E"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + }, + { + name: "subset is not found in both target and source", + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, + {{ComponentId: "D"}, {ComponentId: "E"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "B"}, {ComponentId: "C"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + expectedResult: [][]services.ImpactPathNode{ + {{ComponentId: "B"}, {ComponentId: "C"}}, + {{ComponentId: "D"}, {ComponentId: "E"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + }, + { + name: "target slice is empty", + target: [][]services.ImpactPathNode{}, + source: [][]services.ImpactPathNode{ + {{ComponentId: "E"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + expectedResult: [][]services.ImpactPathNode{ + {{ComponentId: "E"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + }, + { + name: "source slice is empty", + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + source: [][]services.ImpactPathNode{}, + expectedResult: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + }, + { + name: "target and source slices are identical", + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + expectedResult: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + }, + { + name: "target and source slices contain multiple subsets", + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "E"}}, + {{ComponentId: "C"}, {ComponentId: "D"}, {ComponentId: "F"}}, + {{ComponentId: "G"}, {ComponentId: "H"}}, + }, + expectedResult: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + {{ComponentId: "G"}, {ComponentId: "H"}}, + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expectedResult, AppendUniqueImpactPathsForMultipleRoots(test.target, test.source)) + }) + } +} + +func TestGetImpactPathKey(t *testing.T) { + testCases := []struct { + path []services.ImpactPathNode + expectedKey string + }{ + { + path: []services.ImpactPathNode{ + {ComponentId: "A"}, + {ComponentId: "B"}, + }, + expectedKey: "B", + }, + { + path: []services.ImpactPathNode{ + {ComponentId: "A"}, + }, + expectedKey: "A", + }, + } + + for _, test := range testCases { + key := getImpactPathKey(test.path) + assert.Equal(t, test.expectedKey, key) + } +} + +func TestAppendUniqueImpactPaths(t *testing.T) { + testCases := []struct { + name string + multipleRoots bool + target [][]services.ImpactPathNode + source [][]services.ImpactPathNode + expected [][]services.ImpactPathNode + }{ + { + name: "Test case 1: Unique impact paths found", + multipleRoots: false, + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}}, + {{ComponentId: "B"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "C"}}, + {{ComponentId: "D"}}, + }, + expected: [][]services.ImpactPathNode{ + {{ComponentId: "A"}}, + {{ComponentId: "B"}}, + {{ComponentId: "C"}}, + {{ComponentId: "D"}}, + }, + }, + { + name: "Test case 2: No unique impact paths found", + multipleRoots: false, + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}}, + {{ComponentId: "B"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "A"}}, + {{ComponentId: "B"}}, + }, + expected: [][]services.ImpactPathNode{ + {{ComponentId: "A"}}, + {{ComponentId: "B"}}, + }, + }, + { + name: "Test case 3: paths in source are not in target", + multipleRoots: false, + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "E"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + expected: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + {{ComponentId: "E"}}, + {{ComponentId: "F"}, {ComponentId: "G"}}, + }, + }, + { + name: "Test case 4: paths in source are already in target", + multipleRoots: false, + target: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + source: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + expected: [][]services.ImpactPathNode{ + {{ComponentId: "A"}, {ComponentId: "B"}}, + {{ComponentId: "C"}, {ComponentId: "D"}}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := AppendUniqueImpactPaths(tc.target, tc.source, tc.multipleRoots) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestGetApplicableCveValue(t *testing.T) { + testCases := []struct { + name string + entitledForJas bool + applicabilityScanResults []*sarif.Run + cves []services.Cve + components map[string]services.Component + expectedResult jasutils.ApplicabilityStatus + expectedCves []formats.CveRow + }{ + { + name: "not entitled for jas", + entitledForJas: false, + expectedResult: jasutils.NotScanned, + }, + { + name: "no cves", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateResultWithOneLocation("fileName1", 0, 1, 0, 0, "snippet1", "applic_testCve1", "info"), + sarifutils.CreateDummyPassingResult("applic_testCve2"), + ), + }, + cves: nil, + expectedResult: jasutils.NotCovered, + expectedCves: nil, + }, + { + name: "applicable cve", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateDummyPassingResult("applic_testCve1"), + sarifutils.CreateResultWithOneLocation("fileName2", 1, 0, 0, 0, "snippet2", "applic_testCve2", "warning"), + ), + }, + cves: []services.Cve{{Id: "testCve2"}}, + expectedResult: jasutils.Applicable, + expectedCves: []formats.CveRow{{Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.Applicable), Evidence: []formats.Evidence{{ + Location: formats.Location{ + File: "fileName2", + StartLine: 1, + Snippet: "snippet2", + }, + }}}}}, + }, + { + name: "missing context cve", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"missing_context"}), + }, + cves: []services.Cve{{Id: "testCve1"}}, + expectedResult: jasutils.MissingContext, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.MissingContext.String()}}}, + }, + { + name: "undetermined cve", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateDummyPassingResult("applic_testCve1"), + sarifutils.CreateResultWithOneLocation("fileName3", 0, 1, 0, 0, "snippet3", "applic_testCve2", "info"), + ), + }, + cves: []services.Cve{{Id: "testCve3"}}, + expectedResult: jasutils.ApplicabilityUndetermined, + expectedCves: []formats.CveRow{{Id: "testCve3"}}, + }, + { + name: "not applicable cve", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateDummyPassingResult("applic_testCve1"), + sarifutils.CreateDummyPassingResult("applic_testCve2"), + ), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: jasutils.NotApplicable, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}}, + }, + { + name: "applicable and not applicable cves", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateDummyPassingResult("applic_testCve1"), + sarifutils.CreateResultWithOneLocation("fileName4", 1, 0, 0, 0, "snippet", "applic_testCve2", "warning"), + ), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: jasutils.Applicable, + expectedCves: []formats.CveRow{ + {Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.Applicable), + Evidence: []formats.Evidence{{Location: formats.Location{File: "fileName4", StartLine: 1, Snippet: "snippet"}}}, + }}, + }, + }, + { + name: "undetermined and not applicable cves", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyPassingResult("applic_testCve1")), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: jasutils.ApplicabilityUndetermined, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}, {Id: "testCve2"}}, + }, + { + name: "new scan statuses - applicable wins all statuses", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"applicable"}), + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"not_applicable"}), + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve3"), []string{"applicability"}, []string{"not_covered"}), + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve4"), []string{"applicability"}, []string{"missing_context"}), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}, {Id: "testCve4"}}, + expectedResult: jasutils.Applicable, + expectedCves: []formats.CveRow{ + {Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.Applicable.String()}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotCovered.String()}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.MissingContext.String()}}, + }, + }, + { + name: "new scan statuses - not covered wins not applicable", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"not_covered"}), + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"not_applicable"}), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: jasutils.NotCovered, + expectedCves: []formats.CveRow{ + {Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotCovered)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}, + }, + }, + { + name: "new scan statuses - undetermined wins not covered", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"not_covered"}), + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"undetermined"}), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: jasutils.ApplicabilityUndetermined, + expectedCves: []formats.CveRow{ + {Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotCovered)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.ApplicabilityUndetermined)}}, + }, + }, + { + name: "new scan statuses - missing context wins not covered", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"missing_context"}), + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"not_covered"}), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: jasutils.MissingContext, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.MissingContext.String()}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotCovered.String()}}, + }, + }, + { + name: "undetermined with undetermined reason", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability", "undetermined_reason"}, []string{"undetermined", "however"}), + }, + cves: []services.Cve{{Id: "testCve2"}}, + expectedResult: jasutils.ApplicabilityUndetermined, + expectedCves: []formats.CveRow{ + {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.ApplicabilityUndetermined.String(), UndeterminedReason: "however"}}, + }, + }, + { + name: "disqualified evidence", + entitledForJas: true, + applicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateDummyPassingResult("applic_testCve1"), + sarifutils.CreateResultWithOneLocation("fileName4", 1, 0, 0, 0, "snippet", "applic_testCve2", "warning"), + ), + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + components: map[string]services.Component{ + "npm://protobufjs:6.11.2": {ImpactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{FullPath: "fileName4", ComponentId: "npm://mquery:3.2.2"}}}}, + "npm://mquery:3.2.2": {}, + }, + expectedResult: jasutils.Applicable, + expectedCves: []formats.CveRow{ + {Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.Applicable), Evidence: []formats.Evidence{{Location: formats.Location{File: "fileName4", StartLine: 1, Snippet: "snippet"}}}}}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + cves := convertCves(testCase.cves) + for i := range cves { + cves[i].Applicability = GetCveApplicabilityField(cves[i].Id, testCase.applicabilityScanResults, testCase.components) + } + applicableValue := GetApplicableCveStatus(testCase.entitledForJas, testCase.applicabilityScanResults, cves) + assert.Equal(t, testCase.expectedResult, applicableValue) + if assert.True(t, len(testCase.expectedCves) == len(cves)) { + for i := range cves { + if testCase.expectedCves[i].Applicability != nil && assert.NotNil(t, cves[i].Applicability) { + assert.Equal(t, testCase.expectedCves[i].Applicability.Status, cves[i].Applicability.Status) + assert.ElementsMatch(t, testCase.expectedCves[i].Applicability.Evidence, cves[i].Applicability.Evidence) + } + } + } + }) + } +} + +func TestShouldDisqualifyEvidence(t *testing.T) { + testCases := []struct { + name string + component map[string]services.Component + filePath string + disqualify bool + }{ + { + name: "package folders", + component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/protobufjs/src/badCode.js", + disqualify: true, + }, { + name: "nested folders", + component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/someDep/node_modules/protobufjs/src/badCode.js", + disqualify: true, + }, { + name: "applicability in node modules", + component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/mquery/src/badCode.js", + disqualify: false, + }, { + // Only npm supported + name: "not npm", + component: map[string]services.Component{"yarn://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/protobufjs/src/badCode.js", + disqualify: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.disqualify, shouldDisqualifyEvidence(tc.component, tc.filePath)) + }) + } +} + +func TestGetDirectComponents(t *testing.T) { + tests := []struct { + name string + target string + impactPaths [][]services.ImpactPathNode + expectedDirectComponentRows []formats.ComponentRow + expectedConvImpactPaths [][]formats.ComponentRow + }{ + { + name: "one direct component", + impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack:1.2.3"}}}, + expectedDirectComponentRows: []formats.ComponentRow{{Name: "jfrog:pack", Version: "1.2.3"}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack", Version: "1.2.3"}}}, + }, + { + name: "one direct component with target", + target: filepath.Join("root", "dir", "file"), + impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack2:1.2.3"}}}, + expectedDirectComponentRows: []formats.ComponentRow{{Name: "jfrog:pack2", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}}, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack2", Version: "1.2.3"}}}, + }, + { + name: "multiple direct components", + target: filepath.Join("root", "dir", "file"), + impactPaths: [][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack21:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}, {services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack22:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}}, + expectedDirectComponentRows: []formats.ComponentRow{ + {Name: "jfrog:pack21", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, + {Name: "jfrog:pack22", Version: "1.2.3", Location: &formats.Location{File: filepath.Join("root", "dir", "file")}}, + }, + expectedConvImpactPaths: [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack21", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}, {{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack22", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actualComponentRows, actualConvImpactPaths := getDirectComponentsAndImpactPaths(test.target, test.impactPaths) + assert.ElementsMatch(t, test.expectedDirectComponentRows, actualComponentRows) + assert.ElementsMatch(t, test.expectedConvImpactPaths, actualConvImpactPaths) + }) + } +} + +func TestGetFinalApplicabilityStatus(t *testing.T) { + testCases := []struct { + name string + input []jasutils.ApplicabilityStatus + expectedOutput jasutils.ApplicabilityStatus + }{ + { + name: "applicable wins all statuses", + input: []jasutils.ApplicabilityStatus{jasutils.ApplicabilityUndetermined, jasutils.Applicable, jasutils.NotCovered, jasutils.NotApplicable}, + expectedOutput: jasutils.Applicable, + }, + { + name: "undetermined wins not covered", + input: []jasutils.ApplicabilityStatus{jasutils.NotCovered, jasutils.ApplicabilityUndetermined, jasutils.NotCovered, jasutils.NotApplicable}, + expectedOutput: jasutils.ApplicabilityUndetermined, + }, + { + name: "not covered wins not applicable", + input: []jasutils.ApplicabilityStatus{jasutils.NotApplicable, jasutils.NotCovered, jasutils.NotApplicable}, + expectedOutput: jasutils.NotCovered, + }, + { + name: "all statuses are not applicable", + input: []jasutils.ApplicabilityStatus{jasutils.NotApplicable, jasutils.NotApplicable, jasutils.NotApplicable}, + expectedOutput: jasutils.NotApplicable, + }, + { + name: "no statuses", + input: []jasutils.ApplicabilityStatus{}, + expectedOutput: jasutils.NotScanned, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedOutput, getFinalApplicabilityStatus(tc.input)) + }) + } +} diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go new file mode 100644 index 00000000..e95ceed7 --- /dev/null +++ b/utils/results/conversion/convertor.go @@ -0,0 +1,178 @@ +package conversion + +import ( + "strings" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion/sarifparser" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion/simplejsonparser" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion/summaryparser" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion/tableparser" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +type CommandResultsConvertor struct { + Params ResultConvertParams +} + +type ResultConvertParams struct { + // If true, a violation context was provided and we expect violation results + HasViolationContext bool + // Control if the output should include vulnerabilities information + IncludeVulnerabilities bool + // If true and commandType.IsTargetBinary(), binary inner paths in results will be converted to the CI job file (relevant only for SARIF) + PatchBinaryPaths bool + // Control if the output should include licenses information + IncludeLicenses bool + // Control and override converting command results as multi target results, if nil will be determined by the results.HasMultipleTargets() + IsMultipleRoots *bool + // The requested scans to be included in the results, if empty all scans will be included + RequestedScans []utils.SubScanType + // Create local license violations if repo context was not provided and a license is not in this list + AllowedLicenses []string + // Output will contain only the unique violations determined by the GetUniqueKey function (SimpleJson only) + SimplifiedOutput bool + // Convert the results to a pretty format if supported (Table and SimpleJson only) + Pretty bool +} + +func NewCommandResultsConvertor(params ResultConvertParams) *CommandResultsConvertor { + return &CommandResultsConvertor{Params: params} +} + +// Parse a stream of results and convert them to the desired format T +type ResultsStreamFormatParser[T interface{}] interface { + // Reset the convertor to start converting a new command results + Reset(cmdType utils.CommandType, multiScanId, xrayVersion string, entitledForJas, multipleTargets bool) error + // Will be called for each scan target (indicating the current is done parsing and starting to parse a new scan) + ParseNewTargetResults(target results.ScanTarget, errors ...error) error + // Parse SCA content to the current scan target + ParseViolations(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) error + ParseVulnerabilities(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) error + ParseLicenses(target results.ScanTarget, licenses []services.License) error + // Parse JAS content to the current scan target + ParseSecrets(target results.ScanTarget, secrets ...*sarif.Run) error + ParseIacs(target results.ScanTarget, iacs ...*sarif.Run) error + ParseSast(target results.ScanTarget, sast ...*sarif.Run) error + // When done parsing the stream results, get the converted content + Get() (T, error) +} + +func (c *CommandResultsConvertor) ConvertToSimpleJson(cmdResults *results.SecurityCommandResults) (simpleJsonResults formats.SimpleJsonResults, err error) { + parser := simplejsonparser.NewCmdResultsSimpleJsonConverter(false, c.Params.SimplifiedOutput) + return parseCommandResults(c.Params, parser, cmdResults) +} + +func (c *CommandResultsConvertor) ConvertToSarif(cmdResults *results.SecurityCommandResults) (sarifReport *sarif.Report, err error) { + parser := sarifparser.NewCmdResultsSarifConverter(c.Params.IncludeVulnerabilities, c.Params.HasViolationContext, c.Params.PatchBinaryPaths) + return parseCommandResults(c.Params, parser, cmdResults) +} + +func (c *CommandResultsConvertor) ConvertToTable(cmdResults *results.SecurityCommandResults) (tableResults formats.ResultsTables, err error) { + parser := tableparser.NewCmdResultsTableConverter(c.Params.Pretty) + return parseCommandResults(c.Params, parser, cmdResults) +} + +func (c *CommandResultsConvertor) ConvertToSummary(cmdResults *results.SecurityCommandResults) (summaryResults formats.ResultsSummary, err error) { + parser := summaryparser.NewCmdResultsSummaryConverter(c.Params.IncludeVulnerabilities, c.Params.HasViolationContext) + return parseCommandResults(c.Params, parser, cmdResults) +} + +func parseCommandResults[T interface{}](params ResultConvertParams, parser ResultsStreamFormatParser[T], cmdResults *results.SecurityCommandResults) (converted T, err error) { + jasEntitled := cmdResults.EntitledForJas + multipleTargets := cmdResults.HasMultipleTargets() + if params.IsMultipleRoots != nil { + multipleTargets = *params.IsMultipleRoots + } + if err = parser.Reset(cmdResults.CmdType, cmdResults.MultiScanId, cmdResults.XrayVersion, jasEntitled, multipleTargets); err != nil { + return + } + for _, targetScansResults := range cmdResults.Targets { + if err = parser.ParseNewTargetResults(targetScansResults.ScanTarget, targetScansResults.Errors...); err != nil { + return + } + if utils.IsScanRequested(cmdResults.CmdType, utils.ScaScan, params.RequestedScans...) && targetScansResults.ScaResults != nil { + if err = parseScaResults(params, parser, targetScansResults, jasEntitled); err != nil { + return + } + } + if !jasEntitled || targetScansResults.JasResults == nil { + continue + } + if utils.IsScanRequested(cmdResults.CmdType, utils.SecretsScan, params.RequestedScans...) { + if err = parser.ParseSecrets(targetScansResults.ScanTarget, targetScansResults.JasResults.SecretsScanResults...); err != nil { + return + } + } + if utils.IsScanRequested(cmdResults.CmdType, utils.IacScan, params.RequestedScans...) { + if err = parser.ParseIacs(targetScansResults.ScanTarget, targetScansResults.JasResults.IacScanResults...); err != nil { + return + } + } + if utils.IsScanRequested(cmdResults.CmdType, utils.SastScan, params.RequestedScans...) { + if err = parser.ParseSast(targetScansResults.ScanTarget, targetScansResults.JasResults.SastScanResults...); err != nil { + return + } + } + } + return parser.Get() +} + +func parseScaResults[T interface{}](params ResultConvertParams, parser ResultsStreamFormatParser[T], targetScansResults *results.TargetResults, jasEntitled bool) (err error) { + if targetScansResults.ScaResults == nil { + return + } + for _, scaResults := range targetScansResults.ScaResults.XrayResults { + actualTarget := getScaScanTarget(targetScansResults.ScaResults, targetScansResults.ScanTarget) + var applicableRuns []*sarif.Run + if jasEntitled && targetScansResults.JasResults != nil { + applicableRuns = targetScansResults.JasResults.ApplicabilityScanResults + } + if params.IncludeVulnerabilities { + if err = parser.ParseVulnerabilities(actualTarget, scaResults, applicableRuns...); err != nil { + return + } + } + if params.HasViolationContext { + if err = parser.ParseViolations(actualTarget, scaResults, applicableRuns...); err != nil { + return + } + } else if len(scaResults.Violations) == 0 && len(params.AllowedLicenses) > 0 { + // If no violations were found, check if there are licenses that are not allowed + if scaResults.Violations = results.GetViolatedLicenses(params.AllowedLicenses, scaResults.Licenses); len(scaResults.Violations) > 0 { + if err = parser.ParseViolations(actualTarget, scaResults); err != nil { + return + } + } + } + if params.IncludeLicenses { + if err = parser.ParseLicenses(actualTarget, scaResults.Licenses); err != nil { + return + } + } + } + return +} + +// Get the best match for the scan target in the sca results +func getScaScanTarget(scaResults *results.ScaScanResults, target results.ScanTarget) results.ScanTarget { + if scaResults == nil || len(scaResults.Descriptors) == 0 { + // If No Sca scan or no descriptors discovered, use the scan target (build-scan, binary-scan...) + return target + } + // Get the one that it's directory is the prefix of the target and the shortest + // This is for multi module projects where there are multiple sca results for the same target + var bestMatch string + for _, descriptor := range scaResults.Descriptors { + if strings.HasPrefix(descriptor, target.Target) && (bestMatch == "" || len(descriptor) < len(bestMatch)) { + bestMatch = descriptor + } + } + if bestMatch != "" { + return target.Copy(bestMatch) + } + return target +} diff --git a/utils/results/conversion/convertor_test.go b/utils/results/conversion/convertor_test.go new file mode 100644 index 00000000..ae247d27 --- /dev/null +++ b/utils/results/conversion/convertor_test.go @@ -0,0 +1,171 @@ +package conversion + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + + testUtils "github.com/jfrog/jfrog-cli-security/tests/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/validations" + + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/stretchr/testify/assert" +) + +var ( + testDataDir = filepath.Join("..", "..", "..", "tests", "testdata", "output") +) + +const ( + SimpleJson conversionFormat = "simple-json" + Sarif conversionFormat = "sarif" + Summary conversionFormat = "summary" +) + +type conversionFormat string + +func getAuditValidationParams() validations.ValidationParams { + return validations.ValidationParams{ + ExactResultsMatch: true, + SecurityViolations: 11, + Vulnerabilities: 19, + Applicable: 1, + NotApplicable: 7, + NotCovered: 4, + Sast: 4, + Secrets: 3, + } +} + +// For Summary we count unique CVE finding (issueId), for SARIF and SimpleJson we count all findings (pair of issueId+impactedComponent) +// We have in the result 2 CVE with 2 impacted components each +func getDockerScanValidationParams(unique bool) validations.ValidationParams { + params := validations.ValidationParams{ + ExactResultsMatch: true, + Secrets: 3, + } + if unique { + params.Vulnerabilities = 11 + params.Applicable = 3 + params.NotApplicable = 3 + params.NotCovered = 1 + params.Undetermined = 1 + } else { + params.Vulnerabilities = 14 + params.Applicable = 5 + params.NotApplicable = 4 + params.NotCovered = 1 + params.Undetermined = 1 + } + return params +} + +func TestConvertResults(t *testing.T) { + auditInputResults := testUtils.ReadCmdScanResults(t, filepath.Join(testDataDir, "audit", "audit_results.json")) + dockerScanInputResults := testUtils.ReadCmdScanResults(t, filepath.Join(testDataDir, "dockerscan", "docker_results.json")) + + testCases := []struct { + contentFormat conversionFormat + inputResults *results.SecurityCommandResults + expectedContentPath string + }{ + { + contentFormat: SimpleJson, + inputResults: auditInputResults, + expectedContentPath: filepath.Join(testDataDir, "audit", "audit_simple_json.json"), + }, + { + contentFormat: Sarif, + inputResults: auditInputResults, + expectedContentPath: filepath.Join(testDataDir, "audit", "audit_sarif.json"), + }, + { + contentFormat: Summary, + inputResults: auditInputResults, + expectedContentPath: filepath.Join(testDataDir, "audit", "audit_summary.json"), + }, + { + contentFormat: SimpleJson, + inputResults: dockerScanInputResults, + expectedContentPath: filepath.Join(testDataDir, "dockerscan", "docker_simple_json.json"), + }, + { + contentFormat: Sarif, + inputResults: dockerScanInputResults, + expectedContentPath: filepath.Join(testDataDir, "dockerscan", "docker_sarif.json"), + }, + { + contentFormat: Summary, + inputResults: dockerScanInputResults, + expectedContentPath: filepath.Join(testDataDir, "dockerscan", "docker_summary.json"), + }, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("%s convert to %s", testCase.inputResults.CmdType, testCase.contentFormat), func(t *testing.T) { + var validationParams validations.ValidationParams + switch testCase.inputResults.CmdType { + case utils.SourceCode: + validationParams = getAuditValidationParams() + case utils.DockerImage: + validationParams = getDockerScanValidationParams(testCase.contentFormat == Summary) + default: + t.Fatalf("Unsupported command type: %s", testCase.inputResults.CmdType) + } + pretty := false + if testCase.contentFormat == Sarif { + pretty = true + } + convertor := NewCommandResultsConvertor(ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: true, Pretty: pretty}) + + switch testCase.contentFormat { + case SimpleJson: + validateSimpleJsonConversion(t, testUtils.ReadSimpleJsonResults(t, testCase.expectedContentPath), testCase.inputResults, convertor, validationParams) + case Sarif: + validateSarifConversion(t, testUtils.ReadSarifResults(t, testCase.expectedContentPath), testCase.inputResults, convertor, validationParams) + case Summary: + validateSummaryConversion(t, testUtils.ReadSummaryResults(t, testCase.expectedContentPath), testCase.inputResults, convertor, validationParams) + } + }) + } +} + +func validateSimpleJsonConversion(t *testing.T, expectedResults formats.SimpleJsonResults, inputResults *results.SecurityCommandResults, convertor *CommandResultsConvertor, validationParams validations.ValidationParams) { + validationParams.Expected = expectedResults + + actualResults, err := convertor.ConvertToSimpleJson(inputResults) + if !assert.NoError(t, err) { + return + } + validationParams.Actual = actualResults + + validations.ValidateCommandSimpleJsonOutput(t, validationParams) +} + +func validateSarifConversion(t *testing.T, expectedResults *sarif.Report, inputResults *results.SecurityCommandResults, convertor *CommandResultsConvertor, validationParams validations.ValidationParams) { + validationParams.Expected = expectedResults + + actualResults, err := convertor.ConvertToSarif(inputResults) + if !assert.NoError(t, err) { + return + } + validationParams.Actual = actualResults + + validations.ValidateCommandSarifOutput(t, validationParams) +} + +func validateSummaryConversion(t *testing.T, expectedResults formats.ResultsSummary, inputResults *results.SecurityCommandResults, convertor *CommandResultsConvertor, validationParams validations.ValidationParams) { + validationParams.Expected = expectedResults + + actualResults, err := convertor.ConvertToSummary(inputResults) + if !assert.NoError(t, err) { + return + } + validationParams.Actual = actualResults + + validations.ValidateCommandSummaryOutput(t, validationParams) +} diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go new file mode 100644 index 00000000..dd51fbb6 --- /dev/null +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -0,0 +1,749 @@ +package sarifparser + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/owenrumney/go-sarif/v2/sarif" + + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "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-cli-security/utils/severityutils" + + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/jfrog/jfrog-client-go/xray/services" +) + +const ( + CurrentWorkflowNameEnvVar = "GITHUB_WORKFLOW" + CurrentWorkflowRunNumberEnvVar = "GITHUB_RUN_NUMBER" + CurrentWorkflowWorkspaceEnvVar = "GITHUB_WORKSPACE" + + FixedVersionSarifPropertyKey = "fixedVersion" + WatchSarifPropertyKey = "watch" + jfrogFingerprintAlgorithmName = "jfrogFingerprintHash" + MissingCveScore = "0" + maxPossibleCve = 10.0 + + // #nosec G101 -- Not credentials. + BinarySecretScannerToolName = "JFrog Binary Secrets Scanner" + ScaScannerToolName = "JFrog Xray Scanner" +) + +var ( + GithubBaseWorkflowDir = filepath.Join(".github", "workflows") + dockerJasLocationPathPattern = regexp.MustCompile(`.*[\\/](?P[^\\/]+)[\\/](?P[0-9a-fA-F]+)[\\/](?P.*)`) + dockerScaComponentNamePattern = regexp.MustCompile(`(?P[^__]+)__(?P[0-9a-fA-F]+)\.tar`) +) + +type CmdResultsSarifConverter struct { + // Include vulnerabilities/violations in the output + includeVulnerabilities bool + hasViolationContext bool + // If we are running on Github actions, we need to add/change information to the output + patchBinaryPaths bool + // Current stream parse cache information + current *sarif.Report + scaCurrentRun *sarif.Run + currentTarget results.ScanTarget + parsedScaKeys *datastructures.Set[string] + // General information on the current command results + entitledForJas bool + xrayVersion string + currentCmdType utils.CommandType +} + +func NewCmdResultsSarifConverter(includeVulnerabilities, hasViolationContext, patchBinaryPaths bool) *CmdResultsSarifConverter { + return &CmdResultsSarifConverter{includeVulnerabilities: includeVulnerabilities, hasViolationContext: hasViolationContext, patchBinaryPaths: patchBinaryPaths} +} + +func (sc *CmdResultsSarifConverter) Get() (*sarif.Report, error) { + if sc.current == nil { + return sarifutils.NewReport() + } + // Flush the current run + if err := sc.ParseNewTargetResults(results.ScanTarget{}, nil); err != nil { + return sarifutils.NewReport() + } + return sc.current, nil +} + +func (sc *CmdResultsSarifConverter) Reset(cmdType utils.CommandType, _, xrayVersion string, entitledForJas, _ bool) (err error) { + sc.current, err = sarifutils.NewReport() + if err != nil { + return + } + // Reset the current stream general information + sc.currentCmdType = cmdType + sc.xrayVersion = xrayVersion + sc.entitledForJas = entitledForJas + // Reset the current stream cache information + sc.scaCurrentRun = nil + return +} + +func (sc *CmdResultsSarifConverter) ParseNewTargetResults(target results.ScanTarget, errors ...error) (err error) { + if sc.current == nil { + return results.ErrResetConvertor + } + if sc.scaCurrentRun != nil { + // Flush the current run + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.ScaScan, sc.patchBinaryPaths, sc.currentTarget, sc.scaCurrentRun)...) + } + sc.currentTarget = target + if sc.hasViolationContext || sc.includeVulnerabilities { + // Create Sca Run if requested to parse all vulnerabilities/violations to it + sc.scaCurrentRun = sc.createScaRun(sc.currentTarget, len(errors)) + sc.parsedScaKeys = datastructures.MakeSet[string]() + } + return +} + +func (sc *CmdResultsSarifConverter) createScaRun(target results.ScanTarget, errorCount int) *sarif.Run { + run := sarif.NewRunWithInformationURI(ScaScannerToolName, utils.BaseDocumentationURL+"sca") + run.Tool.Driver.Version = &sc.xrayVersion + run.Invocations = append(run.Invocations, sarif.NewInvocation(). + WithWorkingDirectory(sarif.NewSimpleArtifactLocation(target.Target)). + WithExecutionSuccess(errorCount == 0), + ) + return run +} + +// validateBeforeParse checks if the parser is initialized to parse results (checks if Reset and at least one ParseNewTargetResults was called before) +func (sc *CmdResultsSarifConverter) validateBeforeParse() (err error) { + if sc.current == nil { + return results.ErrResetConvertor + } + if (sc.hasViolationContext || sc.includeVulnerabilities) && sc.scaCurrentRun == nil { + return results.ErrNoTargetConvertor + } + return +} + +func (sc *CmdResultsSarifConverter) ParseViolations(target results.ScanTarget, scanResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + if err = sc.validateBeforeParse(); err != nil || sc.scaCurrentRun == nil { + return + } + // Parse violations + sarifResults, sarifRules, err := PrepareSarifScaViolations(sc.currentCmdType, target, scanResponse.Violations, sc.entitledForJas, applicabilityRuns...) + if err != nil || len(sarifRules) == 0 || len(sarifResults) == 0 { + return + } + sc.addScaResultsToCurrentRun(sarifRules, sarifResults...) + return +} + +func (sc *CmdResultsSarifConverter) ParseVulnerabilities(target results.ScanTarget, scanResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + if err = sc.validateBeforeParse(); err != nil || sc.scaCurrentRun == nil { + return + } + sarifResults, sarifRules, err := PrepareSarifScaVulnerabilities(sc.currentCmdType, target, scanResponse.Vulnerabilities, sc.entitledForJas, applicabilityRuns...) + if err != nil || len(sarifRules) == 0 || len(sarifResults) == 0 { + return + } + sc.addScaResultsToCurrentRun(sarifRules, sarifResults...) + return +} + +func (sc *CmdResultsSarifConverter) ParseLicenses(target results.ScanTarget, licenses []services.License) (err error) { + // Not supported in Sarif format + return +} + +func (sc *CmdResultsSarifConverter) ParseSecrets(target results.ScanTarget, secrets ...*sarif.Run) (err error) { + if !sc.entitledForJas { + return + } + if sc.current == nil { + return results.ErrResetConvertor + } + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.SecretsScan, sc.patchBinaryPaths, target, secrets...)...) + return +} + +func (sc *CmdResultsSarifConverter) ParseIacs(target results.ScanTarget, iacs ...*sarif.Run) (err error) { + if !sc.entitledForJas { + return + } + if sc.current == nil { + return results.ErrResetConvertor + } + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.IacScan, sc.patchBinaryPaths, target, iacs...)...) + return +} + +func (sc *CmdResultsSarifConverter) ParseSast(target results.ScanTarget, sast ...*sarif.Run) (err error) { + if !sc.entitledForJas { + return + } + if sc.current == nil { + return results.ErrResetConvertor + } + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.SastScan, sc.patchBinaryPaths, target, sast...)...) + return +} + +func (sc *CmdResultsSarifConverter) addScaResultsToCurrentRun(rules map[string]*sarif.ReportingDescriptor, results ...*sarif.Result) { + for _, rule := range rules { + // This method will add the rule only if it doesn't exist + sc.scaCurrentRun.Tool.Driver.AddRule(rule) + } + for _, result := range results { + sc.scaCurrentRun.AddResult(result) + } +} + +func PrepareSarifScaViolations(cmdType utils.CommandType, target results.ScanTarget, violations []services.Violation, entitledForJas bool, applicabilityRuns ...*sarif.Run) ([]*sarif.Result, map[string]*sarif.ReportingDescriptor, error) { + sarifResults := []*sarif.Result{} + rules := map[string]*sarif.ReportingDescriptor{} + _, _, err := results.PrepareScaViolations( + target, + violations, + entitledForJas, + applicabilityRuns, + addSarifScaSecurityViolation(cmdType, &sarifResults, &rules), + addSarifScaLicenseViolation(cmdType, &sarifResults, &rules), + // Operational risks violations are not supported in Sarif format + nil, + ) + return sarifResults, rules, err +} + +func PrepareSarifScaVulnerabilities(cmdType utils.CommandType, target results.ScanTarget, vulnerabilities []services.Vulnerability, entitledForJas bool, applicabilityRuns ...*sarif.Run) ([]*sarif.Result, map[string]*sarif.ReportingDescriptor, error) { + sarifResults := []*sarif.Result{} + rules := map[string]*sarif.ReportingDescriptor{} + err := results.PrepareScaVulnerabilities( + target, + vulnerabilities, + entitledForJas, + applicabilityRuns, + addSarifScaVulnerability(cmdType, &sarifResults, &rules), + ) + return sarifResults, rules, err +} + +func addSarifScaVulnerability(cmdType utils.CommandType, sarifResults *[]*sarif.Result, rules *map[string]*sarif.ReportingDescriptor) results.ParseScaVulnerabilityFunc { + return func(vulnerability services.Vulnerability, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersions []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + maxCveScore, err := results.FindMaxCVEScore(severity, applicabilityStatus, cves) + if err != nil { + return err + } + markdownDescription, err := getScaIssueMarkdownDescription(directComponents, maxCveScore, applicabilityStatus, fixedVersions) + if err != nil { + return err + } + currentResults, currentRule := parseScaToSarifFormat(cmdType, vulnerability.IssueId, vulnerability.Summary, markdownDescription, maxCveScore, getScaIssueSarifHeadline, cves, severity, applicabilityStatus, impactedPackagesName, impactedPackagesVersion, fixedVersions, directComponents) + cveImpactedComponentRuleId := results.GetScaIssueId(impactedPackagesName, impactedPackagesVersion, results.GetIssueIdentifier(cves, vulnerability.IssueId, "_")) + if _, ok := (*rules)[cveImpactedComponentRuleId]; !ok { + // New Rule + (*rules)[cveImpactedComponentRuleId] = currentRule + } + *sarifResults = append(*sarifResults, currentResults...) + return nil + } +} + +func addSarifScaSecurityViolation(cmdType utils.CommandType, sarifResults *[]*sarif.Result, rules *map[string]*sarif.ReportingDescriptor) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersions []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + maxCveScore, err := results.FindMaxCVEScore(severity, applicabilityStatus, cves) + if err != nil { + return err + } + markdownDescription, err := getScaIssueMarkdownDescription(directComponents, maxCveScore, applicabilityStatus, fixedVersions) + if err != nil { + return err + } + currentResults, currentRule := parseScaToSarifFormat(cmdType, violation.IssueId, violation.Summary, markdownDescription, maxCveScore, getScaIssueSarifHeadline, cves, severity, applicabilityStatus, impactedPackagesName, impactedPackagesVersion, fixedVersions, directComponents, violation.WatchName) + cveImpactedComponentRuleId := results.GetScaIssueId(impactedPackagesName, impactedPackagesVersion, results.GetIssueIdentifier(cves, violation.IssueId, "_")) + if _, ok := (*rules)[cveImpactedComponentRuleId]; !ok { + // New Rule + (*rules)[cveImpactedComponentRuleId] = currentRule + } + *sarifResults = append(*sarifResults, currentResults...) + return nil + } +} + +func addSarifScaLicenseViolation(cmdType utils.CommandType, sarifResults *[]*sarif.Result, rules *map[string]*sarif.ReportingDescriptor) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersions []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + maxCveScore, err := results.FindMaxCVEScore(severity, applicabilityStatus, cves) + if err != nil { + return err + } + markdownDescription, err := getScaLicenseViolationMarkdown(impactedPackagesName, impactedPackagesVersion, violation.LicenseKey, directComponents) + if err != nil { + return err + } + currentResults, currentRule := parseScaToSarifFormat(cmdType, violation.LicenseKey, getLicenseViolationSummary(impactedPackagesName, impactedPackagesVersion, violation.LicenseKey), markdownDescription, maxCveScore, getXrayLicenseSarifHeadline, cves, severity, applicabilityStatus, impactedPackagesName, impactedPackagesVersion, fixedVersions, directComponents) + cveImpactedComponentRuleId := results.GetScaIssueId(impactedPackagesName, impactedPackagesVersion, results.GetIssueIdentifier(cves, violation.LicenseKey, "_")) + if _, ok := (*rules)[cveImpactedComponentRuleId]; !ok { + // New Rule + (*rules)[cveImpactedComponentRuleId] = currentRule + } + *sarifResults = append(*sarifResults, currentResults...) + return nil + } +} + +func parseScaToSarifFormat(cmdType utils.CommandType, xrayId, summary, markdownDescription, cveScore string, generateTitleFunc func(depName string, version string, issueId string) string, cves []formats.CveRow, severity severityutils.Severity, applicabilityStatus jasutils.ApplicabilityStatus, impactedPackagesName, impactedPackagesVersion string, fixedVersions []string, directComponents []formats.ComponentRow, watches ...string) (sarifResults []*sarif.Result, rule *sarif.ReportingDescriptor) { + // General information + issueId := results.GetIssueIdentifier(cves, xrayId, "_") + cveImpactedComponentRuleId := results.GetScaIssueId(impactedPackagesName, impactedPackagesVersion, issueId) + level := severityutils.SeverityToSarifSeverityLevel(severity) + // Add rule fpr the cve if not exists + rule = getScaIssueSarifRule( + cveImpactedComponentRuleId, + generateTitleFunc(impactedPackagesName, impactedPackagesVersion, issueId), + cveScore, + summary, + markdownDescription, + ) + for _, directDependency := range directComponents { + // Create result for each direct dependency + issueResult := sarif.NewRuleResult(cveImpactedComponentRuleId). + WithMessage(sarif.NewTextMessage(generateTitleFunc(directDependency.Name, directDependency.Version, issueId))). + WithLevel(level.String()) + // Add properties + resultsProperties := sarif.NewPropertyBag() + if applicabilityStatus != jasutils.NotScanned { + resultsProperties.Add(jasutils.ApplicabilitySarifPropertyKey, applicabilityStatus.String()) + } + if len(watches) > 0 { + resultsProperties.Add(WatchSarifPropertyKey, strings.Join(watches, ", ")) + } + resultsProperties.Add(FixedVersionSarifPropertyKey, getFixedVersionString(fixedVersions)) + issueResult.AttachPropertyBag(resultsProperties) + // Add location + issueLocation := getComponentSarifLocation(cmdType, directDependency) + if issueLocation != nil { + issueResult.AddLocation(issueLocation) + } + sarifResults = append(sarifResults, issueResult) + } + return +} + +func getScaIssueSarifRule(ruleId, ruleDescription, maxCveScore, summary, markdownDescription string) *sarif.ReportingDescriptor { + cveRuleProperties := sarif.NewPropertyBag() + cveRuleProperties.Add(severityutils.SarifSeverityRuleProperty, maxCveScore) + return sarif.NewRule(ruleId). + WithDescription(ruleDescription). + WithHelp(sarif.NewMultiformatMessageString(summary).WithMarkdown(markdownDescription)). + WithProperties(cveRuleProperties.Properties) +} + +func getComponentSarifLocation(cmtType utils.CommandType, component formats.ComponentRow) *sarif.Location { + filePath := "" + if component.Location != nil { + filePath = component.Location.File + } + if strings.TrimSpace(filePath) == "" { + // For tech that we don't support fetching the package descriptor related to the component + filePath = "Package-Descriptor" + } + var logicalLocations []*sarif.LogicalLocation + if cmtType == utils.DockerImage { + // Docker image - extract layer hash from component name + algorithm, layer := getLayerContentFromComponentId(component.Name) + if layer != "" { + logicalLocation := sarifutils.NewLogicalLocation(layer, "layer") + if algorithm != "" { + logicalLocation.Properties = map[string]interface{}{"algorithm": algorithm} + } + logicalLocations = append(logicalLocations, logicalLocation) + } + } + return sarif.NewLocation(). + WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + filePath))).WithLogicalLocations(logicalLocations) +} + +func getScaIssueMarkdownDescription(directDependencies []formats.ComponentRow, cveScore string, applicableStatus jasutils.ApplicabilityStatus, fixedVersions []string) (string, error) { + formattedDirectDependencies, err := getDirectDependenciesFormatted(directDependencies) + if err != nil { + return "", err + } + descriptionFixVersions := getFixedVersionString(fixedVersions) + if applicableStatus == jasutils.NotScanned { + return fmt.Sprintf("| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| %s | %s | %s |", + cveScore, formattedDirectDependencies, descriptionFixVersions), nil + } + return fmt.Sprintf("| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| %s | %s | %s | %s |", + cveScore, applicableStatus.String(), formattedDirectDependencies, descriptionFixVersions), nil +} + +func getFixedVersionString(fixedVersions []string) string { + if len(fixedVersions) == 0 { + return "No fix available" + } + return strings.Join(fixedVersions, ", ") +} + +func getDirectDependenciesFormatted(directDependencies []formats.ComponentRow) (string, error) { + var formattedDirectDependencies strings.Builder + for _, dependency := range directDependencies { + if _, err := formattedDirectDependencies.WriteString(fmt.Sprintf("`%s %s`
", dependency.Name, dependency.Version)); err != nil { + return "", err + } + } + return strings.TrimSuffix(formattedDirectDependencies.String(), "
"), nil +} + +func getScaIssueSarifHeadline(depName, version, issueId string) string { + return fmt.Sprintf("[%s] %s %s", issueId, depName, version) +} + +func getXrayLicenseSarifHeadline(depName, version, key string) string { + return fmt.Sprintf("License violation [%s] in %s %s", key, depName, version) +} + +func getLicenseViolationSummary(depName, version, key string) string { + return fmt.Sprintf("Dependency %s version %s is using a license (%s) that is not allowed.", depName, version, key) +} + +func getScaLicenseViolationMarkdown(depName, version, key string, directDependencies []formats.ComponentRow) (string, error) { + formattedDirectDependencies, err := getDirectDependenciesFormatted(directDependencies) + if err != nil { + return "", err + } + return fmt.Sprintf("%s
Direct dependencies:
%s", getLicenseViolationSummary(depName, version, key), formattedDirectDependencies), nil +} + +func patchRunsToPassIngestionRules(cmdType utils.CommandType, subScanType utils.SubScanType, patchBinaryPaths bool, target results.ScanTarget, runs ...*sarif.Run) []*sarif.Run { + // Since we run in temp directories files should be relative + // Patch by converting the file paths to relative paths according to the invocations + convertPaths(cmdType, subScanType, runs...) + patchedRuns := []*sarif.Run{} + // Patch changes may alter the original run, so we will create a new run for each + for _, run := range runs { + patched := sarifutils.CopyRunMetadata(run) + if cmdType.IsTargetBinary() && subScanType == utils.SecretsScan { + // Patch the tool name in case of binary scan + sarifutils.SetRunToolName(BinarySecretScannerToolName, patched) + } + if patched.Tool.Driver != nil { + patched.Tool.Driver.Rules = patchRules(cmdType, subScanType, run.Tool.Driver.Rules...) + } + patched.Results = patchResults(cmdType, subScanType, patchBinaryPaths, target, run, run.Results...) + patchedRuns = append(patchedRuns, patched) + } + return patchedRuns +} + +func convertPaths(commandType utils.CommandType, subScanType utils.SubScanType, runs ...*sarif.Run) { + // Convert base on invocation for source code + sarifutils.ConvertRunsPathsToRelative(runs...) + if !(commandType == utils.DockerImage && subScanType == utils.SecretsScan) { + return + } + for _, run := range runs { + for _, result := range run.Results { + // For Docker secret scan, patch the logical location if not exists + patchDockerSecretLocations(result) + } + } +} + +// Patch the URI to be the file path from sha// +// Extract the layer from the location URI, adds it as a logical location kind "layer" +func patchDockerSecretLocations(result *sarif.Result) { + for _, location := range result.Locations { + algorithm, layerHash, relativePath := getLayerContentFromPath(sarifutils.GetLocationFileName(location)) + if algorithm == "" || layerHash == "" || relativePath == "" { + continue + } + // Set Logical location kind "layer" with the layer hash + logicalLocation := sarifutils.NewLogicalLocation(layerHash, "layer") + logicalLocation.Properties = sarif.Properties(map[string]interface{}{"algorithm": algorithm}) + location.LogicalLocations = append(location.LogicalLocations, logicalLocation) + sarifutils.SetLocationFileName(location, relativePath) + } +} + +func patchRules(commandType utils.CommandType, subScanType utils.SubScanType, rules ...*sarif.ReportingDescriptor) (patched []*sarif.ReportingDescriptor) { + patched = []*sarif.ReportingDescriptor{} + for _, rule := range rules { + cloned := sarif.NewRule(rule.ID) + if rule.Name != nil && rule.ID == *rule.Name { + // SARIF1001 - if both 'id' and 'name' are present, they must be different. If they are identical, the tool must omit the 'name' property. + cloned.Name = rule.Name + } + cloned.ShortDescription = rule.ShortDescription + if commandType.IsTargetBinary() && subScanType == utils.SecretsScan { + // Patch the rule name in case of binary scan + sarifutils.SetRuleShortDescriptionText(fmt.Sprintf("[Secret in Binary found] %s", sarifutils.GetRuleShortDescriptionText(rule)), cloned) + } + cloned.FullDescription = rule.FullDescription + cloned.Help = rule.Help + if cloned.Help == nil { + // Github code scanning ingestion rules rejects rules without help content. + // Patch by transferring the full description to the help field. + cloned.Help = rule.FullDescription + } + cloned.HelpURI = rule.HelpURI + cloned.Properties = rule.Properties + cloned.MessageStrings = rule.MessageStrings + + patched = append(patched, cloned) + } + return +} + +func patchResults(commandType utils.CommandType, subScanType utils.SubScanType, patchBinaryPaths bool, target results.ScanTarget, run *sarif.Run, results ...*sarif.Result) (patched []*sarif.Result) { + patched = []*sarif.Result{} + for _, result := range results { + if len(result.Locations) == 0 { + // Github code scanning ingestion rules rejects results without locations. + // Patch by removing results without locations. + log.Debug(fmt.Sprintf("[%s] Removing result [ruleId=%s] without locations: %s", subScanType.String(), sarifutils.GetResultRuleId(result), sarifutils.GetResultMsgText(result))) + continue + } + if commandType.IsTargetBinary() { + var markdown string + if subScanType == utils.SecretsScan { + markdown = getSecretInBinaryMarkdownMsg(commandType, target, result) + } else { + markdown = getScaInBinaryMarkdownMsg(commandType, target, result) + } + sarifutils.SetResultMsgMarkdown(markdown, result) + if patchBinaryPaths { + // For Binary scans, override the physical location if applicable (after data already used for markdown) + result = convertBinaryPhysicalLocations(commandType, run, result) + } + // Calculate the fingerprints if not exists + if !sarifutils.IsFingerprintsExists(result) { + if err := calculateResultFingerprints(commandType, run, result); err != nil { + log.Warn(fmt.Sprintf("Failed to calculate the fingerprint for result [ruleId=%s]: %s", sarifutils.GetResultRuleId(result), err.Error())) + } + } + } + patched = append(patched, result) + } + return patched +} + +// This method may need to replace the physical location if applicable, to avoid override on the existing object we will return a new object if changed +func convertBinaryPhysicalLocations(commandType utils.CommandType, run *sarif.Run, result *sarif.Result) *sarif.Result { + if patchedLocation := getPatchedBinaryLocation(commandType, run); patchedLocation != "" { + patched := sarifutils.CopyResult(result) + for _, location := range patched.Locations { + // Patch the location - Reset the uri and region + location.PhysicalLocation = sarifutils.NewPhysicalLocation(patchedLocation) + } + return patched + } else { + return result + } +} + +func getPatchedBinaryLocation(commandType utils.CommandType, run *sarif.Run) (patchedLocation string) { + if commandType == utils.DockerImage { + if patchedLocation = getDockerfileLocationIfExists(run); patchedLocation != "" { + return + } + } + return getWorkflowFileLocationIfExists() +} + +func getDockerfileLocationIfExists(run *sarif.Run) string { + potentialLocations := []string{filepath.Clean("Dockerfile"), sarifutils.GetFullLocationFileName("Dockerfile", run.Invocations)} + for _, location := range potentialLocations { + if exists, err := fileutils.IsFileExists(location, false); err == nil && exists { + return location + } + } + if workspace := os.Getenv(CurrentWorkflowWorkspaceEnvVar); workspace != "" { + if exists, err := fileutils.IsFileExists(filepath.Join(workspace, "Dockerfile"), false); err == nil && exists { + return filepath.Join(workspace, "Dockerfile") + } + } + return "" +} + +func getGithubWorkflowsDirIfExists() string { + if exists, err := fileutils.IsDirExists(GithubBaseWorkflowDir, false); err == nil && exists { + return GithubBaseWorkflowDir + } + if workspace := os.Getenv(CurrentWorkflowWorkspaceEnvVar); workspace != "" { + if exists, err := fileutils.IsDirExists(filepath.Join(workspace, GithubBaseWorkflowDir), false); err == nil && exists { + return filepath.Join(workspace, GithubBaseWorkflowDir) + } + } + return "" +} + +func getWorkflowFileLocationIfExists() (location string) { + workflowName := os.Getenv(CurrentWorkflowNameEnvVar) + if workflowName == "" { + return + } + workflowsDir := getGithubWorkflowsDirIfExists() + if workflowsDir == "" { + return + } + currentWd, err := os.Getwd() + if err != nil { + log.Warn(fmt.Sprintf("Failed to get the current working directory to get workflow file location: %s", err.Error())) + return + } + // Check if exists in the .github/workflows directory as file name or in the content, return the file path or empty string + if files, err := fileutils.ListFiles(workflowsDir, false); err == nil && len(files) > 0 { + for _, file := range files { + if strings.Contains(file, workflowName) { + return strings.TrimPrefix(file, currentWd) + } + } + for _, file := range files { + if content, err := fileutils.ReadFile(file); err == nil && strings.Contains(string(content), workflowName) { + return strings.TrimPrefix(file, currentWd) + } + } + } + return +} + +func getSecretInBinaryMarkdownMsg(commandType utils.CommandType, target results.ScanTarget, result *sarif.Result) string { + if !commandType.IsTargetBinary() { + return "" + } + content := "🔒 Found Secrets in Binary" + if commandType == utils.DockerImage { + content += " docker" + } + content += " scanning:" + return content + getBaseBinaryDescriptionMarkdown(commandType, target, utils.SecretsScan, result) +} + +func getScaInBinaryMarkdownMsg(commandType utils.CommandType, target results.ScanTarget, result *sarif.Result) string { + return sarifutils.GetResultMsgText(result) + getBaseBinaryDescriptionMarkdown(commandType, target, utils.ScaScan, result) +} + +func getBaseBinaryDescriptionMarkdown(commandType utils.CommandType, target results.ScanTarget, subScanType utils.SubScanType, result *sarif.Result) (content string) { + // If in github action, add the workflow name and run number + if workflowLocation := getWorkflowFileLocationIfExists(); workflowLocation != "" { + content += fmt.Sprintf("\nGithub Actions Workflow: %s", workflowLocation) + } + if os.Getenv(CurrentWorkflowRunNumberEnvVar) != "" { + content += fmt.Sprintf("\nRun: %s", os.Getenv(CurrentWorkflowRunNumberEnvVar)) + } + // If is docker image, add the image tag + if commandType == utils.DockerImage { + if imageTag := getDockerImageTag(commandType, target); imageTag != "" { + content += fmt.Sprintf("\nImage: %s", imageTag) + } + } + var location *sarif.Location + if len(result.Locations) > 0 { + location = result.Locations[0] + } + return content + getBinaryLocationMarkdownString(commandType, subScanType, result, location) +} + +func getDockerImageTag(commandType utils.CommandType, target results.ScanTarget) string { + if commandType != utils.DockerImage { + return "" + } + if target.Name != "" { + return target.Name + } + return filepath.Base(target.Target) +} + +// If command is docker prepare the markdown string for the location: +// * Layer: +// * Filepath: +// * Evidence: +func getBinaryLocationMarkdownString(commandType utils.CommandType, subScanType utils.SubScanType, result *sarif.Result, location *sarif.Location) (content string) { + if location == nil { + return "" + } + if commandType == utils.DockerImage { + if layer, algorithm := getDockerLayer(location); layer != "" { + if algorithm != "" { + content += fmt.Sprintf("\nLayer (%s): %s", algorithm, layer) + } else { + content += fmt.Sprintf("\nLayer: %s", layer) + } + } + } + if subScanType != utils.SecretsScan { + return + } + if locationFilePath := sarifutils.GetLocationFileName(location); locationFilePath != "" { + content += fmt.Sprintf("\nFilepath: %s", locationFilePath) + } + if snippet := sarifutils.GetLocationSnippetText(location); snippet != "" { + content += fmt.Sprintf("\nEvidence: %s", snippet) + } + if tokenValidation := results.GetResultPropertyTokenValidation(result); tokenValidation != "" { + content += fmt.Sprintf("\nToken Validation %s", tokenValidation) + } + return +} + +func getDockerLayer(location *sarif.Location) (layer, algorithm string) { + // If location has logical location with kind "layer" return it + if logicalLocation := sarifutils.GetLogicalLocation("layer", location); logicalLocation != nil && logicalLocation.Name != nil { + layer = *logicalLocation.Name + if algorithmValue, ok := logicalLocation.Properties["algorithm"].(string); ok { + algorithm = algorithmValue + } + return + } + return +} + +// Match: +// Extract algorithm, hash and relative path +func getLayerContentFromPath(content string) (algorithm string, layerHash string, relativePath string) { + matches := dockerJasLocationPathPattern.FindStringSubmatch(content) + if len(matches) == 0 { + return + } + algorithm = matches[dockerJasLocationPathPattern.SubexpIndex("algorithm")] + layerHash = matches[dockerJasLocationPathPattern.SubexpIndex("hash")] + relativePath = matches[dockerJasLocationPathPattern.SubexpIndex("relativePath")] + return +} + +// Match: __.tar +// Extract algorithm and hash +func getLayerContentFromComponentId(componentId string) (algorithm string, layerHash string) { + matches := dockerScaComponentNamePattern.FindStringSubmatch(componentId) + if len(matches) == 0 { + return + } + algorithm = matches[dockerScaComponentNamePattern.SubexpIndex("algorithm")] + layerHash = matches[dockerScaComponentNamePattern.SubexpIndex("hash")] + return +} + +// According to the SARIF specification: +// To determine whether a result from a subsequent run is logically the same as a result from the baseline, +// there must be a way to use information contained in the result to construct a stable identifier for the result. We refer to this identifier as a fingerprint. +// A result management system SHOULD construct a fingerprint by using information contained in the SARIF file such as: +// The name of the tool that produced the result, the rule id, the file system path to the analysis target... +func calculateResultFingerprints(resultType utils.CommandType, run *sarif.Run, result *sarif.Result) error { + if !resultType.IsTargetBinary() { + return nil + } + ids := []string{sarifutils.GetRunToolName(run), sarifutils.GetResultRuleId(result)} + for _, location := range sarifutils.GetResultFileLocations(result) { + ids = append(ids, strings.ReplaceAll(location, string(filepath.Separator), "/")) + } + ids = append(ids, sarifutils.GetResultLocationSnippets(result)...) + // Calculate the hash value and set the fingerprint to the result + hashValue, err := utils.Md5Hash(ids...) + if err != nil { + return err + } + sarifutils.SetResultFingerprint(jfrogFingerprintAlgorithmName, hashValue, result) + return nil +} diff --git a/utils/results/conversion/sarifparser/sarifparser_test.go b/utils/results/conversion/sarifparser/sarifparser_test.go new file mode 100644 index 00000000..dc0060f6 --- /dev/null +++ b/utils/results/conversion/sarifparser/sarifparser_test.go @@ -0,0 +1,496 @@ +package sarifparser + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/jfrog/build-info-go/tests" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + clientTests "github.com/jfrog/jfrog-client-go/utils/tests" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "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/owenrumney/go-sarif/v2/sarif" + "github.com/stretchr/testify/assert" +) + +func TestGetComponentSarifLocation(t *testing.T) { + testCases := []struct { + name string + cmdType utils.CommandType + component formats.ComponentRow + expectedOutput *sarif.Location + }{ + { + name: "Component with name and version", + component: formats.ComponentRow{ + Name: "example-package", + Version: "1.0.0", + }, + expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation(). + WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://Package-Descriptor")), + ), + }, + { + name: "Component with location", + component: formats.ComponentRow{ + Name: "example-package", + Version: "1.0.0", + Location: &formats.Location{File: filepath.Join("dir", "file.txt")}, + }, + expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation(). + WithArtifactLocation(sarif.NewArtifactLocation().WithUri(fmt.Sprintf("file://%s", filepath.Join("dir", "file.txt")))), + ), + }, + { + name: "Component with location and logical location", + cmdType: utils.DockerImage, + component: formats.ComponentRow{Name: "sha256__3a8bca98bcad879bca98b9acd.tar"}, + expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation(). + WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://Package-Descriptor")), + ).WithLogicalLocations([]*sarif.LogicalLocation{sarifutils.CreateLogicalLocationWithProperty("3a8bca98bcad879bca98b9acd", "layer", "algorithm", "sha256")}), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedOutput, getComponentSarifLocation(tc.cmdType, tc.component)) + }) + } +} + +func TestGetVulnerabilityOrViolationSarifHeadline(t *testing.T) { + assert.Equal(t, "[CVE-2022-1234] loadsh 1.4.1", getScaIssueSarifHeadline("loadsh", "1.4.1", "CVE-2022-1234")) + assert.NotEqual(t, "[CVE-2022-1234] comp 1.4.1", getScaIssueSarifHeadline("comp", "1.2.1", "CVE-2022-1234")) +} + +func TestGetXrayLicenseSarifHeadline(t *testing.T) { + assert.Equal(t, "License violation [MIT] in loadsh 1.4.1", getXrayLicenseSarifHeadline("loadsh", "1.4.1", "MIT")) + assert.NotEqual(t, "License violation [] in comp 1.2.1", getXrayLicenseSarifHeadline("comp", "1.2.1", "MIT")) +} + +func TestGetLicenseViolationSummary(t *testing.T) { + assert.Equal(t, "Dependency loadsh version 1.4.1 is using a license (MIT) that is not allowed.", getLicenseViolationSummary("loadsh", "1.4.1", "MIT")) + assert.NotEqual(t, "Dependency comp version 1.2.1 is using a license () that is not allowed.", getLicenseViolationSummary("comp", "1.2.1", "MIT")) +} + +func TestGetSarifTableDescription(t *testing.T) { + testCases := []struct { + name string + directDependencies []formats.ComponentRow + cveScore string + applicableStatus jasutils.ApplicabilityStatus + fixedVersions []string + expectedDescription string + }{ + { + name: "Applicable vulnerability", + directDependencies: []formats.ComponentRow{ + {Name: "example-package", Version: "1.0.0"}, + }, + cveScore: "7.5", + applicableStatus: jasutils.Applicable, + fixedVersions: []string{"1.0.1", "1.0.2"}, + expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.5 | Applicable | `example-package 1.0.0` | 1.0.1, 1.0.2 |", + }, + { + name: "Not-scanned vulnerability", + directDependencies: []formats.ComponentRow{ + {Name: "example-package", Version: "2.0.0"}, + }, + cveScore: "6.2", + applicableStatus: jasutils.NotScanned, + fixedVersions: []string{"2.0.1"}, + expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 6.2 | `example-package 2.0.0` | 2.0.1 |", + }, + { + name: "No fixed versions", + directDependencies: []formats.ComponentRow{ + {Name: "example-package", Version: "3.0.0"}, + }, + cveScore: "3.0", + applicableStatus: jasutils.NotScanned, + fixedVersions: []string{}, + expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 3.0 | `example-package 3.0.0` | No fix available |", + }, + { + name: "Not-covered vulnerability", + directDependencies: []formats.ComponentRow{ + {Name: "example-package", Version: "3.0.0"}, + }, + cveScore: "3.0", + applicableStatus: jasutils.NotCovered, + fixedVersions: []string{"3.0.1"}, + expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Not Covered | `example-package 3.0.0` | 3.0.1 |", + }, + { + name: "Undetermined vulnerability", + directDependencies: []formats.ComponentRow{ + {Name: "example-package", Version: "3.0.0"}, + }, + cveScore: "3.0", + applicableStatus: jasutils.ApplicabilityUndetermined, + fixedVersions: []string{"3.0.1"}, + expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Undetermined | `example-package 3.0.0` | 3.0.1 |", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := getScaIssueMarkdownDescription(tc.directDependencies, tc.cveScore, tc.applicableStatus, tc.fixedVersions) + assert.NoError(t, err) + assert.Equal(t, tc.expectedDescription, output) + }) + } +} + +func TestGetDirectDependenciesFormatted(t *testing.T) { + testCases := []struct { + name string + directDeps []formats.ComponentRow + expectedOutput string + }{ + { + name: "Single direct dependency", + directDeps: []formats.ComponentRow{ + {Name: "example-package", Version: "1.0.0"}, + }, + expectedOutput: "`example-package 1.0.0`", + }, + { + name: "Multiple direct dependencies", + directDeps: []formats.ComponentRow{ + {Name: "dependency1", Version: "1.0.0"}, + {Name: "dependency2", Version: "2.0.0"}, + }, + expectedOutput: "`dependency1 1.0.0`
`dependency2 2.0.0`", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := getDirectDependenciesFormatted(tc.directDeps) + assert.NoError(t, err) + assert.Equal(t, tc.expectedOutput, output) + }) + } +} + +func TestGetScaLicenseViolationMarkdown(t *testing.T) { + testCases := []struct { + name string + license string + impactedDepName string + impactedDepVersion string + directDeps []formats.ComponentRow + expectedOutput string + }{ + { + name: "Single direct dependency", + license: "MIT", + impactedDepName: "example-package", + impactedDepVersion: "1.0.0", + directDeps: []formats.ComponentRow{ + {Name: "dependency1", Version: "1.0.0"}, + }, + expectedOutput: "Dependency example-package version 1.0.0 is using a license (MIT) that is not allowed.
Direct dependencies:
`dependency1 1.0.0`", + }, + { + name: "Multiple direct dependencies", + license: "MIT", + impactedDepName: "example-package", + impactedDepVersion: "1.0.0", + directDeps: []formats.ComponentRow{ + {Name: "dependency1", Version: "1.0.0"}, + {Name: "dependency2", Version: "2.0.0"}, + }, + expectedOutput: "Dependency example-package version 1.0.0 is using a license (MIT) that is not allowed.
Direct dependencies:
`dependency1 1.0.0`
`dependency2 2.0.0`", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output, err := getScaLicenseViolationMarkdown(tc.impactedDepName, tc.impactedDepVersion, tc.license, tc.directDeps) + assert.NoError(t, err) + assert.Equal(t, tc.expectedOutput, output) + }) + } +} + +func TestGetLayerContentFromComponentId(t *testing.T) { + testCases := []struct { + name string + path string + expectedAlgorithm string + expectedLayerHash string + }{ + { + name: "Valid path", + path: "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + expectedAlgorithm: "sha256", + expectedLayerHash: "cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", + }, + { + name: "Invalid path - not hex", + path: "sha256__NOT_HEX.tar", + }, + { + name: "Invalid path - no algorithm", + path: "_cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", + }, + { + name: "Invalid path - no suffix", + path: "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + algorithm, layerHash := getLayerContentFromComponentId(tc.path) + assert.Equal(t, tc.expectedAlgorithm, algorithm) + assert.Equal(t, tc.expectedLayerHash, layerHash) + }) + } +} + +func preparePatchTestEnv(t *testing.T) (string, string, func()) { + currentWd, err := os.Getwd() + assert.NoError(t, err) + wd, cleanUpTempDir := tests.CreateTempDirWithCallbackAndAssert(t) + cleanUpWd := clientTests.ChangeDirWithCallback(t, currentWd, wd) + dockerfileDir := filepath.Join(wd, "DockerfileDir") + err = fileutils.CreateDirIfNotExist(dockerfileDir) + // Prepare env content + assert.NoError(t, err) + createDummyDockerfile(t, dockerfileDir) + createDummyGithubWorkflow(t, dockerfileDir) + createDummyGithubWorkflow(t, wd) + return wd, dockerfileDir, func() { + cleanUpWd() + cleanUpTempDir() + } +} + +func createDummyGithubWorkflow(t *testing.T, baseDir string) { + assert.NoError(t, fileutils.CreateDirIfNotExist(filepath.Join(baseDir, GithubBaseWorkflowDir))) + assert.NoError(t, os.WriteFile(filepath.Join(baseDir, GithubBaseWorkflowDir, "workflowFile.yml"), []byte("workflow name"), 0644)) +} + +func createDummyDockerfile(t *testing.T, baseDir string) { + assert.NoError(t, os.WriteFile(filepath.Join(baseDir, "Dockerfile"), []byte("Dockerfile data"), 0644)) +} + +func TestPatchRunsToPassIngestionRules(t *testing.T) { + wd, dockerfileDir, cleanUp := preparePatchTestEnv(t) + defer cleanUp() + + testCases := []struct { + name string + target results.ScanTarget + cmdType utils.CommandType + subScan utils.SubScanType + withEnvVars bool + withDockerfile bool + input []*sarif.Run + expectedResults []*sarif.Run + }{ + { + name: "No runs", + target: results.ScanTarget{Name: "dockerImage:imageVersion"}, + cmdType: utils.DockerImage, + subScan: utils.SecretsScan, + input: []*sarif.Run{}, + expectedResults: []*sarif.Run{}, + }, + { + name: "Build scan - SCA", + target: results.ScanTarget{Name: "buildName (buildNumber)"}, + cmdType: utils.Build, + subScan: utils.ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file")))), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file"))), + }, + }, + { + name: "Docker image scan - SCA", + target: results.ScanTarget{Name: "dockerImage:imageVersion"}, + cmdType: utils.DockerImage, + subScan: utils.ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties( + sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), + []string{"applicability"}, []string{"applicable"}).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}), + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties( + sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), + ), + []string{"applicability"}, []string{"applicable"}).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}), + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), + ), + ), + }, + }, + { + name: "Docker image scan - with env vars", + target: results.ScanTarget{Name: "dockerImage:imageVersion"}, + cmdType: utils.DockerImage, + subScan: utils.ScaScan, + withEnvVars: true, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), + // No location, should be removed in the output + sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "eda26ae830c578197aeda65a82d7f093", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( + sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation(filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml"))), + ), + ), + ), + }, + }, + { + name: "Docker image scan - with Dockerfile in wd", + target: results.ScanTarget{Name: "dockerImage:imageVersion"}, + cmdType: utils.DockerImage, + subScan: utils.ScaScan, + withEnvVars: true, + withDockerfile: true, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(dockerfileDir, + sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(dockerfileDir, + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "8cbd7268a4d20f2358ba2667ebd18956", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( + sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation("Dockerfile")), + ), + ), + ), + }, + }, + { + name: "Docker image scan - Secrets", + target: results.ScanTarget{Name: "dockerImage:imageVersion"}, + cmdType: utils.DockerImage, + subScan: utils.SecretsScan, + input: []*sarif.Run{ + sarifutils.CreateRunNameWithResults("some tool name", + sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "unpacked", "filesystem", "blobs", "sha1", "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "usr", "src", "app", "server", "index.js"))), + ).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), + }), + }, + expectedResults: []*sarif.Run{ + { + Tool: sarif.Tool{ + Driver: sarifutils.CreateDummyDriver(BinarySecretScannerToolName, &sarif.ReportingDescriptor{ + ID: "rule", + ShortDescription: sarif.NewMultiformatMessageString("[Secret in Binary found] "), + }), + }, + Invocations: []*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}, + Results: []*sarif.Result{ + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("🔒 Found Secrets in Binary docker scanning:\nImage: dockerImage:imageVersion\nLayer (sha1): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: %s\nEvidence: snippet", filepath.Join("usr", "src", "app", "server", "index.js")), "", jfrogFingerprintAlgorithmName, "93d660ebfd39b1220c42c0beb6e4e863", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation(filepath.Join("usr", "src", "app", "server", "index.js"), "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "layer", "algorithm", "sha1"), + ), + }, + }, + }, + }, + { + name: "Binary scan - SCA", + target: results.ScanTarget{Target: filepath.Join(wd, "dir", "binary")}, + cmdType: utils.Binary, + subScan: utils.ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "binary"))), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultWithFingerprint("", "", jfrogFingerprintAlgorithmName, "e72a936dc73acbc4283a93230ff9b6e8", sarifutils.CreateDummyLocationInPath(filepath.Join("dir", "binary"))), + ), + }, + }, + { + name: "Audit scan - SCA", + target: results.ScanTarget{Target: wd}, + cmdType: utils.SourceCode, + subScan: utils.ScaScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(filepath.Join(wd, "Package-Descriptor")), + // No location, should be removed in the output + sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath("Package-Descriptor"), + ), + }, + }, + { + name: "Audit scan - Secrets", + target: results.ScanTarget{Target: wd}, + cmdType: utils.SourceCode, + subScan: utils.SecretsScan, + input: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file"))), + // No location, should be removed in the output + sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), + ), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file")), + ), + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.withEnvVars { + cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "workflow name") + defer cleanFileEnv() + cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "123") + defer cleanRunNumEnv() + } else { + // Since the the env are provided by the + cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "") + defer cleanFileEnv() + cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "") + defer cleanRunNumEnv() + } + if tc.withDockerfile { + revertWd := clientTests.ChangeDirWithCallback(t, wd, dockerfileDir) + defer revertWd() + } + patchedRuns := patchRunsToPassIngestionRules(tc.cmdType, tc.subScan, true, tc.target, tc.input...) + assert.ElementsMatch(t, tc.expectedResults, patchedRuns) + }) + } +} diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser.go b/utils/results/conversion/simplejsonparser/simplejsonparser.go new file mode 100644 index 00000000..12e304b6 --- /dev/null +++ b/utils/results/conversion/simplejsonparser/simplejsonparser.go @@ -0,0 +1,624 @@ +package simplejsonparser + +import ( + "sort" + "strconv" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "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-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +type CmdResultsSimpleJsonConverter struct { + // If supported, pretty print the output text + pretty bool + // If true, the output will contain only unique issues (ignoring the same issue in different locations) + uniqueScaIssues bool + // Current stream parse cache information + current *formats.SimpleJsonResults + // General information on the current command results + entitledForJas bool + multipleRoots bool +} + +func NewCmdResultsSimpleJsonConverter(pretty, uniqueScaIssues bool) *CmdResultsSimpleJsonConverter { + return &CmdResultsSimpleJsonConverter{pretty: pretty, uniqueScaIssues: uniqueScaIssues} +} + +func (sjc *CmdResultsSimpleJsonConverter) Get() (formats.SimpleJsonResults, error) { + if sjc.current == nil { + return formats.SimpleJsonResults{}, nil + } + if sjc.uniqueScaIssues { + sjc.current.Vulnerabilities = removeScaDuplications(sjc.current.Vulnerabilities, sjc.multipleRoots) + sjc.current.SecurityViolations = removeScaDuplications(sjc.current.SecurityViolations, sjc.multipleRoots) + } + sortResults(sjc.current) + return *sjc.current, nil +} + +func (sjc *CmdResultsSimpleJsonConverter) Reset(_ utils.CommandType, multiScanId, _ string, entitledForJas, multipleTargets bool) (err error) { + sjc.current = &formats.SimpleJsonResults{MultiScanId: multiScanId} + sjc.entitledForJas = entitledForJas + sjc.multipleRoots = multipleTargets + return +} + +func (sjc *CmdResultsSimpleJsonConverter) ParseNewTargetResults(target results.ScanTarget, errors ...error) (err error) { + if sjc.current == nil { + return results.ErrResetConvertor + } + for _, err := range errors { + if err != nil { + sjc.current.Errors = append(sjc.current.Errors, formats.SimpleJsonError{FilePath: target.Target, ErrorMessage: err.Error()}) + } + } + return +} + +func (sjc *CmdResultsSimpleJsonConverter) ParseViolations(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + if sjc.current == nil { + return results.ErrResetConvertor + } + secViolationsSimpleJson, licViolationsSimpleJson, opRiskViolationsSimpleJson, err := PrepareSimpleJsonViolations(target, scaResponse, sjc.pretty, sjc.entitledForJas, applicabilityRuns...) + if err != nil { + return + } + sjc.current.SecurityViolations = append(sjc.current.SecurityViolations, secViolationsSimpleJson...) + sjc.current.LicensesViolations = append(sjc.current.LicensesViolations, licViolationsSimpleJson...) + sjc.current.OperationalRiskViolations = append(sjc.current.OperationalRiskViolations, opRiskViolationsSimpleJson...) + return +} + +func (sjc *CmdResultsSimpleJsonConverter) ParseVulnerabilities(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + if sjc.current == nil { + return results.ErrResetConvertor + } + vulSimpleJson, err := PrepareSimpleJsonVulnerabilities(target, scaResponse, sjc.pretty, sjc.entitledForJas, applicabilityRuns...) + if err != nil || len(vulSimpleJson) == 0 { + return + } + sjc.current.Vulnerabilities = append(sjc.current.Vulnerabilities, vulSimpleJson...) + return +} + +func (sjc *CmdResultsSimpleJsonConverter) ParseLicenses(target results.ScanTarget, licenses []services.License) (err error) { + if sjc.current == nil { + return results.ErrResetConvertor + } + licSimpleJson, err := PrepareSimpleJsonLicenses(target, licenses) + if err != nil || len(licSimpleJson) == 0 { + return + } + sjc.current.Licenses = append(sjc.current.Licenses, licSimpleJson...) + return +} + +func (sjc *CmdResultsSimpleJsonConverter) ParseSecrets(_ results.ScanTarget, secrets ...*sarif.Run) (err error) { + if !sjc.entitledForJas { + return + } + if sjc.current == nil { + return results.ErrResetConvertor + } + secretsSimpleJson, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, secrets...) + if err != nil || len(secretsSimpleJson) == 0 { + return + } + sjc.current.Secrets = append(sjc.current.Secrets, secretsSimpleJson...) + return +} + +func (sjc *CmdResultsSimpleJsonConverter) ParseIacs(_ results.ScanTarget, iacs ...*sarif.Run) (err error) { + if !sjc.entitledForJas { + return + } + if sjc.current == nil { + return results.ErrResetConvertor + } + iacSimpleJson, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, iacs...) + if err != nil || len(iacSimpleJson) == 0 { + return + } + sjc.current.Iacs = append(sjc.current.Iacs, iacSimpleJson...) + return +} + +func (sjc *CmdResultsSimpleJsonConverter) ParseSast(_ results.ScanTarget, sast ...*sarif.Run) (err error) { + if !sjc.entitledForJas { + return + } + if sjc.current == nil { + return results.ErrResetConvertor + } + sastSimpleJson, err := PrepareSimpleJsonJasIssues(sjc.entitledForJas, sjc.pretty, sast...) + if err != nil || len(sastSimpleJson) == 0 { + return + } + sjc.current.Sast = append(sjc.current.Sast, sastSimpleJson...) + return +} + +func PrepareSimpleJsonViolations(target results.ScanTarget, scaResponse services.ScanResponse, pretty, jasEntitled bool, applicabilityRuns ...*sarif.Run) ([]formats.VulnerabilityOrViolationRow, []formats.LicenseRow, []formats.OperationalRiskViolationRow, error) { + var securityViolationsRows []formats.VulnerabilityOrViolationRow + var licenseViolationsRows []formats.LicenseRow + var operationalRiskViolationsRows []formats.OperationalRiskViolationRow + _, _, err := results.PrepareScaViolations( + target, + scaResponse.Violations, + jasEntitled, + applicabilityRuns, + addSimpleJsonSecurityViolation(&securityViolationsRows, pretty), + addSimpleJsonLicenseViolation(&licenseViolationsRows, pretty), + addSimpleJsonOperationalRiskViolation(&operationalRiskViolationsRows, pretty), + ) + return securityViolationsRows, licenseViolationsRows, operationalRiskViolationsRows, err +} + +func PrepareSimpleJsonVulnerabilities(target results.ScanTarget, scaResponse services.ScanResponse, pretty, entitledForJas bool, applicabilityRuns ...*sarif.Run) ([]formats.VulnerabilityOrViolationRow, error) { + var vulnerabilitiesRows []formats.VulnerabilityOrViolationRow + err := results.PrepareScaVulnerabilities( + target, + scaResponse.Vulnerabilities, + entitledForJas, + applicabilityRuns, + addSimpleJsonVulnerability(&vulnerabilitiesRows, pretty), + ) + return vulnerabilitiesRows, err +} + +func addSimpleJsonVulnerability(vulnerabilitiesRows *[]formats.VulnerabilityOrViolationRow, pretty bool) results.ParseScaVulnerabilityFunc { + return func(vulnerability services.Vulnerability, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + *vulnerabilitiesRows = append(*vulnerabilitiesRows, + formats.VulnerabilityOrViolationRow{ + Summary: vulnerability.Summary, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severity, applicabilityStatus, pretty), + ImpactedDependencyName: impactedPackagesName, + ImpactedDependencyVersion: impactedPackagesVersion, + ImpactedDependencyType: impactedPackagesType, + Components: directComponents, + }, + FixedVersions: fixedVersion, + Cves: cves, + IssueId: vulnerability.IssueId, + References: vulnerability.References, + JfrogResearchInformation: convertJfrogResearchInformation(vulnerability.ExtendedInformation), + ImpactPaths: impactPaths, + Technology: techutils.Technology(vulnerability.Technology), + Applicable: applicabilityStatus.ToString(pretty), + }, + ) + return nil + } +} + +func addSimpleJsonSecurityViolation(securityViolationsRows *[]formats.VulnerabilityOrViolationRow, pretty bool) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + *securityViolationsRows = append(*securityViolationsRows, + formats.VulnerabilityOrViolationRow{ + Summary: violation.Summary, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severity, applicabilityStatus, pretty), + ImpactedDependencyName: impactedPackagesName, + ImpactedDependencyVersion: impactedPackagesVersion, + ImpactedDependencyType: impactedPackagesType, + Components: directComponents, + }, + FixedVersions: fixedVersion, + Cves: cves, + IssueId: violation.IssueId, + References: violation.References, + JfrogResearchInformation: convertJfrogResearchInformation(violation.ExtendedInformation), + ImpactPaths: impactPaths, + Technology: techutils.Technology(violation.Technology), + Applicable: applicabilityStatus.ToString(pretty), + }, + ) + return nil + } +} + +func addSimpleJsonLicenseViolation(licenseViolationsRows *[]formats.LicenseRow, pretty bool) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + *licenseViolationsRows = append(*licenseViolationsRows, + formats.LicenseRow{ + LicenseKey: violation.LicenseKey, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severity, applicabilityStatus, pretty), + ImpactedDependencyName: impactedPackagesName, + ImpactedDependencyVersion: impactedPackagesVersion, + ImpactedDependencyType: impactedPackagesType, + Components: directComponents, + }, + }, + ) + return nil + } +} + +func addSimpleJsonOperationalRiskViolation(operationalRiskViolationsRows *[]formats.OperationalRiskViolationRow, pretty bool) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + violationOpRiskData := getOperationalRiskViolationReadableData(violation) + for compIndex := 0; compIndex < len(impactedPackagesName); compIndex++ { + operationalRiskViolationsRow := &formats.OperationalRiskViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severity, applicabilityStatus, pretty), + ImpactedDependencyName: impactedPackagesName, + ImpactedDependencyVersion: impactedPackagesVersion, + ImpactedDependencyType: impactedPackagesType, + Components: directComponents, + }, + IsEol: violationOpRiskData.isEol, + Cadence: violationOpRiskData.cadence, + Commits: violationOpRiskData.commits, + Committers: violationOpRiskData.committers, + NewerVersions: violationOpRiskData.newerVersions, + LatestVersion: violationOpRiskData.latestVersion, + RiskReason: violationOpRiskData.riskReason, + EolMessage: violationOpRiskData.eolMessage, + } + *operationalRiskViolationsRows = append(*operationalRiskViolationsRows, *operationalRiskViolationsRow) + } + return nil + } +} + +func PrepareSimpleJsonLicenses(target results.ScanTarget, licenses []services.License) ([]formats.LicenseRow, error) { + var licensesRows []formats.LicenseRow + err := results.PrepareLicenses(target, licenses, addSimpleJsonLicense(&licensesRows)) + return licensesRows, err +} + +func addSimpleJsonLicense(licenseViolationsRows *[]formats.LicenseRow) results.ParseLicensesFunc { + return func(license services.License, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) error { + *licenseViolationsRows = append(*licenseViolationsRows, + formats.LicenseRow{ + LicenseKey: license.Key, + ImpactPaths: impactPaths, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: impactedPackagesName, + ImpactedDependencyVersion: impactedPackagesVersion, + ImpactedDependencyType: impactedPackagesType, + Components: directComponents, + }, + }, + ) + return nil + } +} + +func PrepareSimpleJsonJasIssues(entitledForJas, pretty bool, jasIssues ...*sarif.Run) ([]formats.SourceCodeRow, error) { + var rows []formats.SourceCodeRow + err := results.PrepareJasIssues(jasIssues, entitledForJas, func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) error { + scannerDescription := "" + if rule != nil { + scannerDescription = sarifutils.GetRuleFullDescription(rule) + } + rows = append(rows, + formats.SourceCodeRow{ + SeverityDetails: severityutils.GetAsDetails(severity, jasutils.Applicable, pretty), + Finding: sarifutils.GetResultMsgText(result), + ScannerDescription: scannerDescription, + Fingerprint: sarifutils.GetResultFingerprint(result), + Location: formats.Location{ + File: sarifutils.GetRelativeLocationFileName(location, run.Invocations), + StartLine: sarifutils.GetLocationStartLine(location), + StartColumn: sarifutils.GetLocationStartColumn(location), + EndLine: sarifutils.GetLocationEndLine(location), + EndColumn: sarifutils.GetLocationEndColumn(location), + Snippet: sarifutils.GetLocationSnippetText(location), + }, + Applicability: getJasResultApplicability(result), + CodeFlow: codeFlowToLocationFlow(sarifutils.GetLocationRelatedCodeFlowsFromResult(location, result), run.Invocations, pretty), + }, + ) + return nil + }) + return rows, err +} + +func getJasResultApplicability(result *sarif.Result) *formats.Applicability { + status := results.GetResultPropertyTokenValidation(result) + statusDescription := results.GetResultPropertyMetadata(result) + if status == "" && statusDescription == "" { + return nil + } + return &formats.Applicability{Status: status, ScannerDescription: statusDescription} +} + +func codeFlowToLocationFlow(flows []*sarif.CodeFlow, invocations []*sarif.Invocation, isTable bool) (flowRows [][]formats.Location) { + if isTable { + // Not displaying in table + return + } + for _, codeFlow := range flows { + for _, stackTrace := range codeFlow.ThreadFlows { + rowFlow := []formats.Location{} + for _, stackTraceEntry := range stackTrace.Locations { + rowFlow = append(rowFlow, formats.Location{ + File: sarifutils.GetRelativeLocationFileName(stackTraceEntry.Location, invocations), + StartLine: sarifutils.GetLocationStartLine(stackTraceEntry.Location), + StartColumn: sarifutils.GetLocationStartColumn(stackTraceEntry.Location), + EndLine: sarifutils.GetLocationEndLine(stackTraceEntry.Location), + EndColumn: sarifutils.GetLocationEndColumn(stackTraceEntry.Location), + Snippet: sarifutils.GetLocationSnippetText(stackTraceEntry.Location), + }) + } + flowRows = append(flowRows, rowFlow) + } + } + return +} + +func convertJfrogResearchInformation(extendedInfo *services.ExtendedInformation) *formats.JfrogResearchInformation { + if extendedInfo == nil { + return nil + } + var severityReasons []formats.JfrogResearchSeverityReason + for _, severityReason := range extendedInfo.JfrogResearchSeverityReasons { + severityReasons = append(severityReasons, formats.JfrogResearchSeverityReason{ + Name: severityReason.Name, + Description: severityReason.Description, + IsPositive: severityReason.IsPositive, + }) + } + return &formats.JfrogResearchInformation{ + Summary: extendedInfo.ShortDescription, + Details: extendedInfo.FullDescription, + SeverityDetails: formats.SeverityDetails{Severity: extendedInfo.JfrogResearchSeverity}, + SeverityReasons: severityReasons, + Remediation: extendedInfo.Remediation, + } +} + +type operationalRiskViolationReadableData struct { + isEol string + cadence string + commits string + committers string + eolMessage string + riskReason string + latestVersion string + newerVersions string +} + +func getOperationalRiskViolationReadableData(violation services.Violation) *operationalRiskViolationReadableData { + isEol, cadence, commits, committers, newerVersions, latestVersion := "N/A", "N/A", "N/A", "N/A", "N/A", "N/A" + if violation.IsEol != nil { + isEol = strconv.FormatBool(*violation.IsEol) + } + if violation.Cadence != nil { + cadence = strconv.FormatFloat(*violation.Cadence, 'f', -1, 64) + } + if violation.Committers != nil { + committers = strconv.FormatInt(int64(*violation.Committers), 10) + } + if violation.Commits != nil { + commits = strconv.FormatInt(*violation.Commits, 10) + } + if violation.NewerVersions != nil { + newerVersions = strconv.FormatInt(int64(*violation.NewerVersions), 10) + } + if violation.LatestVersion != "" { + latestVersion = violation.LatestVersion + } + return &operationalRiskViolationReadableData{ + isEol: isEol, + cadence: cadence, + commits: commits, + committers: committers, + eolMessage: violation.EolMessage, + riskReason: violation.RiskReason, + latestVersion: latestVersion, + newerVersions: newerVersions, + } +} + +// Returns a new slice that contains only the unique issues from the input slice +// The uniqueness of the violations is determined by the GetUniqueKey function +func removeScaDuplications(issues []formats.VulnerabilityOrViolationRow, multipleRoots bool) []formats.VulnerabilityOrViolationRow { + var uniqueIssues = make(map[string]*formats.VulnerabilityOrViolationRow) + for i := range issues { + packageKey := results.GetUniqueKey(issues[i].ImpactedDependencyDetails.ImpactedDependencyName, issues[i].ImpactedDependencyDetails.ImpactedDependencyVersion, issues[i].IssueId, len(issues[i].FixedVersions) > 0) + if uniqueIssue, exist := uniqueIssues[packageKey]; exist { + // combine attributes from the same issue + uniqueIssue.FixedVersions = utils.UniqueIntersection(uniqueIssue.FixedVersions, issues[i].FixedVersions...) + uniqueIssue.ImpactPaths = AppendImpactPathsIfUnique(uniqueIssue.ImpactPaths, issues[i].ImpactPaths, multipleRoots) + uniqueIssue.ImpactedDependencyDetails.Components = AppendComponentIfUnique(uniqueIssue.ImpactedDependencyDetails.Components, issues[i].ImpactedDependencyDetails.Components) + continue + } + uniqueIssues[packageKey] = &issues[i] + } + // convert map to slice + result := make([]formats.VulnerabilityOrViolationRow, 0, len(uniqueIssues)) + for _, v := range uniqueIssues { + result = append(result, *v) + } + return result +} + +func AppendImpactPathsIfUnique(original [][]formats.ComponentRow, toAdd [][]formats.ComponentRow, multipleRoots bool) [][]formats.ComponentRow { + if multipleRoots { + return AppendImpactPathsIfUniqueForMultipleRoots(original, toAdd) + } + impactPathMap := make(map[string][]formats.ComponentRow) + for _, path := range original { + // The first node component id is the key and the value is the whole path + impactPathMap[getImpactPathKey(path)] = path + } + for _, path := range toAdd { + key := getImpactPathKey(path) + if _, exists := impactPathMap[key]; !exists { + impactPathMap[key] = path + original = append(original, path) + } + } + return original +} + +func getImpactPathKey(path []formats.ComponentRow) string { + key := getComponentKey(path[results.RootIndex]) + if len(path) == results.DirectDependencyPathLength { + key = getComponentKey(path[results.DirectDependencyIndex]) + } + return key +} + +func getComponentKey(component formats.ComponentRow) string { + return results.GetDependencyId(component.Name, component.Version) +} + +// getImpactPathKey return a key that is used as a key to identify and deduplicate impact paths. +// If an impact path length is equal to directDependencyPathLength, then the direct dependency is the key, and it's in the directDependencyIndex place. +func AppendImpactPathsIfUniqueForMultipleRoots(original [][]formats.ComponentRow, toAdd [][]formats.ComponentRow) [][]formats.ComponentRow { + for targetPathIndex, targetPath := range original { + for sourcePathIndex, sourcePath := range toAdd { + var subset []formats.ComponentRow + if len(sourcePath) <= len(targetPath) { + subset = isComponentRowIsSubset(targetPath, sourcePath) + if len(subset) != 0 { + original[targetPathIndex] = subset + } + } else { + subset = isComponentRowIsSubset(sourcePath, targetPath) + if len(subset) != 0 { + toAdd[sourcePathIndex] = subset + } + } + } + } + return AppendImpactPathsIfUnique(original, toAdd, false) +} + +// isComponentRowIsSubset checks if targetPath is a subset of sourcePath, and returns the subset if exists +func isComponentRowIsSubset(target []formats.ComponentRow, source []formats.ComponentRow) []formats.ComponentRow { + var subsetImpactPath []formats.ComponentRow + impactPathNodesMap := make(map[string]bool) + for _, node := range target { + impactPathNodesMap[getComponentKey(node)] = true + } + + for _, node := range source { + if impactPathNodesMap[getComponentKey(node)] { + subsetImpactPath = append(subsetImpactPath, node) + } + } + + if len(subsetImpactPath) == len(target) || len(subsetImpactPath) == len(source) { + return subsetImpactPath + } + return []formats.ComponentRow{} +} + +// AppendComponentIfUnique checks if the component exists in the components (not based on location) +// Removing location information for all entries as well to combine the same components from different locations +func AppendComponentIfUnique(target []formats.ComponentRow, source []formats.ComponentRow) []formats.ComponentRow { + directComponents := make(map[string]formats.ComponentRow) + for i := range target { + // Remove location information + target[i].Location = nil + // Add to the map if not exists + key := getComponentKey(target[i]) + if _, exists := directComponents[key]; !exists { + directComponents[getComponentKey(target[i])] = target[i] + } + } + for i := range source { + // Remove location information + source[i].Location = nil + // Add to the map if not exists + key := getComponentKey(source[i]) + if _, exists := directComponents[key]; !exists { + directComponents[getComponentKey(source[i])] = source[i] + } + } + result := make([]formats.ComponentRow, 0, len(directComponents)) + for _, v := range directComponents { + result = append(result, v) + } + return result +} + +func sortResults(simpleJsonResults *formats.SimpleJsonResults) { + if simpleJsonResults == nil { + return + } + if len(simpleJsonResults.SecurityViolations) > 0 { + sortVulnerabilityOrViolationRows(simpleJsonResults.SecurityViolations) + } + if len(simpleJsonResults.Vulnerabilities) > 0 { + sortVulnerabilityOrViolationRows(simpleJsonResults.Vulnerabilities) + } + if len(simpleJsonResults.Licenses) > 0 { + sort.Slice(simpleJsonResults.Licenses, func(i, j int) bool { + return simpleJsonResults.Licenses[i].LicenseKey < simpleJsonResults.Licenses[j].LicenseKey + }) + } + if len(simpleJsonResults.LicensesViolations) > 0 { + sort.Slice(simpleJsonResults.LicensesViolations, func(i, j int) bool { + return simpleJsonResults.LicensesViolations[i].SeverityNumValue > simpleJsonResults.LicensesViolations[j].SeverityNumValue + }) + } + if len(simpleJsonResults.OperationalRiskViolations) > 0 { + sort.Slice(simpleJsonResults.OperationalRiskViolations, func(i, j int) bool { + return simpleJsonResults.OperationalRiskViolations[i].SeverityNumValue > simpleJsonResults.OperationalRiskViolations[j].SeverityNumValue + }) + } + if len(simpleJsonResults.Secrets) > 0 { + sortSourceCodeRow(simpleJsonResults.Secrets) + } + if len(simpleJsonResults.Iacs) > 0 { + sortSourceCodeRow(simpleJsonResults.Iacs) + } + if len(simpleJsonResults.Sast) > 0 { + sortSourceCodeRow(simpleJsonResults.Sast) + } +} + +// sortVulnerabilityOrViolationRows is sorting in the following order: +// Severity -> Applicability -> JFrog Research Score -> XRAY ID +func sortVulnerabilityOrViolationRows(rows []formats.VulnerabilityOrViolationRow) { + sort.Slice(rows, func(i, j int) bool { + if rows[i].SeverityNumValue != rows[j].SeverityNumValue { + return rows[i].SeverityNumValue > rows[j].SeverityNumValue + } + if rows[i].Applicable != rows[j].Applicable { + return jasutils.ConvertApplicableToScore(rows[i].Applicable) > jasutils.ConvertApplicableToScore(rows[j].Applicable) + } + priorityI := getJfrogResearchPriority(rows[i]) + priorityJ := getJfrogResearchPriority(rows[j]) + if priorityI != priorityJ { + return priorityI > priorityJ + } + return rows[i].IssueId > rows[j].IssueId + }) +} + +// getJfrogResearchPriority returns the score of JFrog Research Severity. +// If there is no such severity will return the normal severity score. +// When vulnerability with JFrog Research to a vulnerability without we'll compare the JFrog Research Severity to the normal severity +func getJfrogResearchPriority(vulnerabilityOrViolation formats.VulnerabilityOrViolationRow) int { + if vulnerabilityOrViolation.JfrogResearchInformation == nil { + return vulnerabilityOrViolation.SeverityNumValue + } + return vulnerabilityOrViolation.JfrogResearchInformation.SeverityNumValue +} + +func sortSourceCodeRow(rows []formats.SourceCodeRow) { + sort.Slice(rows, func(i, j int) bool { + if rows[i].SeverityNumValue != rows[j].SeverityNumValue { + return rows[i].SeverityNumValue > rows[j].SeverityNumValue + } + if rows[i].Applicability != nil && rows[j].Applicability != nil { + return jasutils.TokenValidationOrder[rows[i].Applicability.Status] < jasutils.TokenValidationOrder[rows[j].Applicability.Status] + } + return rows[i].Location.File > rows[j].Location.File + }) +} diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go new file mode 100644 index 00000000..c1f30b3b --- /dev/null +++ b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go @@ -0,0 +1,697 @@ +package simplejsonparser + +import ( + "path/filepath" + "testing" + + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/stretchr/testify/assert" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "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/xray/services" +) + +var ( + testScaScanVulnerabilities = []services.Vulnerability{ + { + IssueId: "XRAY-1", + Summary: "summary-1", + Severity: "High", + Cves: []services.Cve{{Id: "CVE-1"}}, + Components: map[string]services.Component{ + "component-A": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-A"}, + }}, + }, + "component-B": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-B"}, + }}, + }, + }, + }, + { + IssueId: "XRAY-2", + Summary: "summary-2", + Severity: "Low", + Cves: []services.Cve{{Id: "CVE-2"}}, + Components: map[string]services.Component{ + "component-B": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-B"}, + }}, + }, + }, + }, + } + testScaScanViolation = []services.Violation{ + { + IssueId: "XRAY-1", + Summary: "summary-1", + Severity: "High", + WatchName: "watch-name", + ViolationType: "security", + Cves: []services.Cve{{Id: "CVE-1"}}, + Components: map[string]services.Component{ + "component-A": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-A"}, + }}, + }, + "component-B": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-B"}, + }}, + }, + }, + }, + { + IssueId: "XRAY-2", + Summary: "summary-2", + Severity: "Low", + WatchName: "watch-name", + ViolationType: "security", + Cves: []services.Cve{{Id: "CVE-2"}}, + Components: map[string]services.Component{ + "component-B": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-B"}, + }}, + }, + }, + }, + { + IssueId: "XRAY-3", + Summary: "summary-3", + Severity: "Low", + ViolationType: "license", + LicenseKey: "license-1", + Components: map[string]services.Component{ + "component-B": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-B"}, + }}, + }, + }, + }, + } +) + +func TestSortVulnerabilityOrViolationRows(t *testing.T) { + testCases := []struct { + name string + rows []formats.VulnerabilityOrViolationRow + expectedOrder []string + }{ + { + name: "Sort by severity with different severity values", + rows: []formats.VulnerabilityOrViolationRow{ + { + Summary: "Summary 1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 9, + }, + ImpactedDependencyName: "Dependency 1", + ImpactedDependencyVersion: "1.0.0", + }, + FixedVersions: []string{}, + }, + { + Summary: "Summary 2", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Critical", + SeverityNumValue: 12, + }, + ImpactedDependencyName: "Dependency 2", + ImpactedDependencyVersion: "2.0.0", + }, + FixedVersions: []string{"1.0.0"}, + }, + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Medium", + SeverityNumValue: 6, + }, + ImpactedDependencyName: "Dependency 3", + ImpactedDependencyVersion: "3.0.0", + }, + Summary: "Summary 3", + FixedVersions: []string{}, + }, + }, + expectedOrder: []string{"Dependency 2", "Dependency 1", "Dependency 3"}, + }, + { + name: "Sort by severity with same severity values, but different fixed versions", + rows: []formats.VulnerabilityOrViolationRow{ + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Critical", + SeverityNumValue: 12, + }, + ImpactedDependencyName: "Dependency 1", + ImpactedDependencyVersion: "1.0.0", + }, + Summary: "Summary 1", + FixedVersions: []string{"1.0.0"}, + }, + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Critical", + SeverityNumValue: 12, + }, + ImpactedDependencyName: "Dependency 2", + ImpactedDependencyVersion: "2.0.0", + }, + Summary: "Summary 2", + FixedVersions: []string{}, + }, + }, + expectedOrder: []string{"Dependency 1", "Dependency 2"}, + }, + { + name: "Sort by severity with same severity values different applicability", + rows: []formats.VulnerabilityOrViolationRow{ + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Critical", + SeverityNumValue: 13, + }, + ImpactedDependencyName: "Dependency 1", + ImpactedDependencyVersion: "1.0.0", + }, + Summary: "Summary 1", + Applicable: jasutils.Applicable.String(), + FixedVersions: []string{"1.0.0"}, + }, + { + Summary: "Summary 2", + Applicable: jasutils.NotApplicable.String(), + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Critical", + SeverityNumValue: 11, + }, + ImpactedDependencyName: "Dependency 2", + ImpactedDependencyVersion: "2.0.0", + }, + }, + { + Summary: "Summary 3", + Applicable: jasutils.ApplicabilityUndetermined.String(), + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Critical", + SeverityNumValue: 12, + }, + ImpactedDependencyName: "Dependency 3", + ImpactedDependencyVersion: "2.0.0", + }, + }, + }, + expectedOrder: []string{"Dependency 1", "Dependency 3", "Dependency 2"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sortVulnerabilityOrViolationRows(tc.rows) + for i, row := range tc.rows { + assert.Equal(t, tc.expectedOrder[i], row.ImpactedDependencyName) + } + }) + } +} + +func TestGetOperationalRiskReadableData(t *testing.T) { + tests := []struct { + violation services.Violation + expectedResults *operationalRiskViolationReadableData + }{ + { + services.Violation{IsEol: nil, LatestVersion: "", NewerVersions: nil, + Cadence: nil, Commits: nil, Committers: nil, RiskReason: "", EolMessage: ""}, + &operationalRiskViolationReadableData{"N/A", "N/A", "N/A", "N/A", "", "", "N/A", "N/A"}, + }, + { + services.Violation{IsEol: utils.NewBoolPtr(true), LatestVersion: "1.2.3", NewerVersions: utils.NewIntPtr(5), + Cadence: utils.NewFloat64Ptr(3.5), Commits: utils.NewInt64Ptr(55), Committers: utils.NewIntPtr(10), EolMessage: "no maintainers", RiskReason: "EOL"}, + &operationalRiskViolationReadableData{"true", "3.5", "55", "10", "no maintainers", "EOL", "1.2.3", "5"}, + }, + } + + for _, test := range tests { + results := getOperationalRiskViolationReadableData(test.violation) + assert.Equal(t, test.expectedResults, results) + } +} + +func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { + testCases := []struct { + name string + input []services.Vulnerability + target results.ScanTarget + entitledForJas bool + applicabilityRuns []*sarif.Run + expectedOutput []formats.VulnerabilityOrViolationRow + }{ + { + name: "No vulnerabilities", + target: results.ScanTarget{Target: "target"}, + }, + { + name: "Vulnerabilities not entitled for JAS", + input: testScaScanVulnerabilities, + target: results.ScanTarget{Target: "target"}, + expectedOutput: []formats.VulnerabilityOrViolationRow{ + { + Summary: "summary-1", + IssueId: "XRAY-1", + Cves: []formats.CveRow{{Id: "CVE-1"}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, + ImpactedDependencyName: "component-A", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-A", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + }, + { + Summary: "summary-1", + IssueId: "XRAY-1", + Cves: []formats.CveRow{{Id: "CVE-1"}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + { + Summary: "summary-2", + IssueId: "XRAY-2", + Cves: []formats.CveRow{{Id: "CVE-2"}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 10}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + }, + }, + { + name: "Vulnerabilities with Jas", + input: testScaScanVulnerabilities, + target: results.ScanTarget{Target: "target"}, + entitledForJas: true, + applicabilityRuns: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties( + sarifutils.CreateDummyPassingResult("applic_CVE-1"), + []string{"applicability"}, []string{"not_applicable"}, + ).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target"))}), + sarifutils.CreateRunWithDummyResultAndRuleProperties( + sarifutils.CreateResultWithLocations("applic_CVE-2", "applic_CVE-2", "note", sarifutils.CreateLocation("target/file", 0, 0, 0, 0, "snippet")), + []string{"applicability"}, []string{"applicable"}, + ).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target"))}), + }, + expectedOutput: []formats.VulnerabilityOrViolationRow{ + { + Summary: "summary-1", + IssueId: "XRAY-1", + Applicable: jasutils.NotApplicable.String(), + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, + ImpactedDependencyName: "component-A", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-A", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + }, + { + Summary: "summary-1", + IssueId: "XRAY-1", + Applicable: jasutils.NotApplicable.String(), + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + { + Summary: "summary-2", + IssueId: "XRAY-2", + Applicable: jasutils.Applicable.String(), + Cves: []formats.CveRow{{ + Id: "CVE-2", + Applicability: &formats.Applicability{ + Status: jasutils.Applicable.String(), + Evidence: []formats.Evidence{{ + Location: formats.Location{File: "file", StartLine: 0, StartColumn: 0, EndLine: 0, EndColumn: 0, Snippet: "snippet"}, + Reason: "applic_CVE-2", + }}, + }, + }}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 13}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + out, err := PrepareSimpleJsonVulnerabilities(tc.target, services.ScanResponse{Vulnerabilities: tc.input}, false, tc.entitledForJas, tc.applicabilityRuns...) + assert.NoError(t, err) + assert.ElementsMatch(t, tc.expectedOutput, out) + }) + } +} + +func TestPrepareSimpleJsonViolations(t *testing.T) { + testCases := []struct { + name string + input []services.Violation + target results.ScanTarget + entitledForJas bool + applicabilityRuns []*sarif.Run + expectedSecurityOutput []formats.VulnerabilityOrViolationRow + expectedLicenseOutput []formats.LicenseRow + expectedOperationalRiskOutput []formats.OperationalRiskViolationRow + }{ + { + name: "No violations", + target: results.ScanTarget{Target: "target"}, + }, + { + name: "Violations not entitled for JAS", + input: testScaScanViolation, + target: results.ScanTarget{Target: "target"}, + expectedSecurityOutput: []formats.VulnerabilityOrViolationRow{ + { + Summary: "summary-1", + IssueId: "XRAY-1", + Cves: []formats.CveRow{{Id: "CVE-1"}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, + ImpactedDependencyName: "component-A", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-A", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + }, + { + Summary: "summary-1", + IssueId: "XRAY-1", + Cves: []formats.CveRow{{Id: "CVE-1"}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + { + Summary: "summary-2", + IssueId: "XRAY-2", + Cves: []formats.CveRow{{Id: "CVE-2"}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 10}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + }, + expectedLicenseOutput: []formats.LicenseRow{ + { + LicenseKey: "license-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 10}, + ImpactedDependencyName: "component-B", + Components: []formats.ComponentRow{{Name: "component-B", Location: &formats.Location{File: "target"}}}, + }, + }, + }, + }, + { + name: "Violations with applicability", + input: testScaScanViolation, + target: results.ScanTarget{Target: "target"}, + entitledForJas: true, + applicabilityRuns: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultAndRuleProperties( + sarifutils.CreateDummyPassingResult("applic_CVE-1"), + []string{"applicability"}, []string{"not_applicable"}, + ).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target"))}), + sarifutils.CreateRunWithDummyResultAndRuleProperties( + sarifutils.CreateResultWithLocations("applic_CVE-2", "applic_CVE-2", "note", sarifutils.CreateLocation("target/file", 0, 0, 0, 0, "snippet")), + []string{"applicability"}, []string{"applicable"}, + ).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target"))}), + }, + expectedSecurityOutput: []formats.VulnerabilityOrViolationRow{ + { + Summary: "summary-1", + IssueId: "XRAY-1", + Applicable: jasutils.NotApplicable.String(), + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, + ImpactedDependencyName: "component-A", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-A", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-A"}}}, + }, + { + Summary: "summary-1", + IssueId: "XRAY-1", + Applicable: jasutils.NotApplicable.String(), + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + { + Summary: "summary-2", + IssueId: "XRAY-2", + Applicable: jasutils.Applicable.String(), + Cves: []formats.CveRow{{ + Id: "CVE-2", + Applicability: &formats.Applicability{ + Status: jasutils.Applicable.String(), + Evidence: []formats.Evidence{{ + Location: formats.Location{File: "file", StartLine: 0, StartColumn: 0, EndLine: 0, EndColumn: 0, Snippet: "snippet"}, + Reason: "applic_CVE-2", + }}, + }, + }}, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 13}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + }, + expectedLicenseOutput: []formats.LicenseRow{ + { + LicenseKey: "license-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 10}, + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + securityOutput, licenseOutput, operationalRiskOutput, err := PrepareSimpleJsonViolations(tc.target, services.ScanResponse{Violations: tc.input}, false, tc.entitledForJas, tc.applicabilityRuns...) + assert.NoError(t, err) + assert.ElementsMatch(t, tc.expectedSecurityOutput, securityOutput) + assert.ElementsMatch(t, tc.expectedLicenseOutput, licenseOutput) + assert.ElementsMatch(t, tc.expectedOperationalRiskOutput, operationalRiskOutput) + }) + } + +} + +func TestPrepareSimpleJsonLicenses(t *testing.T) { + testCases := []struct { + name string + target results.ScanTarget + licenses []services.License + expectedOutput []formats.LicenseRow + }{ + { + name: "No licenses", + target: results.ScanTarget{Target: "target"}, + }, + { + name: "Licenses", + target: results.ScanTarget{Target: "target"}, + licenses: []services.License{ + { + Key: "license-1", + Name: "license-1-name", + Components: map[string]services.Component{ + "component-B": { + ImpactPaths: [][]services.ImpactPathNode{{ + {ComponentId: "root"}, + {ComponentId: "component-B"}, + }}, + }, + }, + }, + }, + expectedOutput: []formats.LicenseRow{ + { + LicenseKey: "license-1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "component-B", + // Direct + Components: []formats.ComponentRow{{ + Name: "component-B", + Location: &formats.Location{File: "target"}, + }}, + }, + ImpactPaths: [][]formats.ComponentRow{{{Name: "root"}, {Name: "component-B"}}}, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + out, err := PrepareSimpleJsonLicenses(tc.target, tc.licenses) + assert.NoError(t, err) + assert.ElementsMatch(t, tc.expectedOutput, out) + }) + } +} + +func TestPrepareSimpleJsonJasIssues(t *testing.T) { + issues := []*sarif.Run{ + // Secret detection + sarifutils.CreateRunWithDummyResultsInWd("target", + sarifutils.CreateResultWithOneLocation(filepath.Join("target", "file"), 1, 2, 3, 4, "secret-snippet", "secret-rule-id", "note"), + ), + } + testCases := []struct { + name string + target results.ScanTarget + entitledForJas bool + jasIssues []*sarif.Run + expectedOutput []formats.SourceCodeRow + }{ + { + name: "No JAS issues", + entitledForJas: true, + target: results.ScanTarget{Target: filepath.Join("root", "target")}, + }, + { + name: "JAS issues - not entitled", + target: results.ScanTarget{Target: "target"}, + jasIssues: issues, + expectedOutput: []formats.SourceCodeRow{}, + }, + { + name: "JAS issues", + entitledForJas: true, + target: results.ScanTarget{Target: "target"}, + jasIssues: issues, + expectedOutput: []formats.SourceCodeRow{ + { + Location: formats.Location{File: "file", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "secret-snippet"}, + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 13}, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + out, err := PrepareSimpleJsonJasIssues(tc.entitledForJas, false, tc.jasIssues...) + assert.NoError(t, err) + assert.ElementsMatch(t, tc.expectedOutput, out) + }) + } +} diff --git a/utils/results/conversion/summaryparser/summaryparser.go b/utils/results/conversion/summaryparser/summaryparser.go new file mode 100644 index 00000000..e07c99ba --- /dev/null +++ b/utils/results/conversion/summaryparser/summaryparser.go @@ -0,0 +1,297 @@ +package summaryparser + +import ( + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +type CmdResultsSummaryConverter struct { + entitledForJas bool + includeVulnerabilities bool + includeViolations bool + + current *formats.ResultsSummary + currentScan *formats.ScanSummary +} + +func NewCmdResultsSummaryConverter(includeVulnerabilities, hasViolationContext bool) *CmdResultsSummaryConverter { + return &CmdResultsSummaryConverter{includeVulnerabilities: includeVulnerabilities, includeViolations: hasViolationContext} +} + +func (sc *CmdResultsSummaryConverter) Get() (formats.ResultsSummary, error) { + if sc.current == nil { + return formats.ResultsSummary{}, nil + } + // Flush the last scan + if err := sc.ParseNewTargetResults(results.ScanTarget{}, nil); err != nil { + return formats.ResultsSummary{}, err + } + return *sc.current, nil +} + +func (sc *CmdResultsSummaryConverter) Reset(_ utils.CommandType, _, _ string, entitledForJas, _ bool) (err error) { + sc.current = &formats.ResultsSummary{} + sc.entitledForJas = entitledForJas + return +} + +func (sc *CmdResultsSummaryConverter) ParseNewTargetResults(target results.ScanTarget, _ ...error) (err error) { + if sc.current == nil { + return results.ErrResetConvertor + } + if sc.currentScan != nil { + sc.current.Scans = append(sc.current.Scans, *sc.currentScan) + } + sc.currentScan = &formats.ScanSummary{Target: target.Target, Name: target.Name} + if sc.includeVulnerabilities { + sc.currentScan.Vulnerabilities = &formats.ScanResultSummary{} + } + if sc.includeViolations { + sc.currentScan.Violations = &formats.ScanViolationsSummary{ScanResultSummary: formats.ScanResultSummary{}} + } + return +} + +// validateBeforeParse checks if the parser is initialized to parse results (checks if Reset and at least one ParseNewTargetResults was called before) +func (sc *CmdResultsSummaryConverter) validateBeforeParse() (err error) { + if sc.current == nil { + return results.ErrResetConvertor + } + if sc.currentScan == nil { + return results.ErrNoTargetConvertor + } + return +} + +func (sc *CmdResultsSummaryConverter) ParseViolations(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + if err = sc.validateBeforeParse(); err != nil || sc.currentScan.Violations == nil { + return + } + if sc.currentScan.Violations.ScanResultSummary.ScaResults == nil { + sc.currentScan.Violations.ScanResultSummary.ScaResults = &formats.ScaScanResultSummary{} + } + // Parse general SCA results + if scaResponse.ScanId != "" { + sc.currentScan.Violations.ScanResultSummary.ScaResults.ScanIds = utils.UniqueUnion(sc.currentScan.Violations.ScanResultSummary.ScaResults.ScanIds, scaResponse.ScanId) + } + if scaResponse.XrayDataUrl != "" { + sc.currentScan.Violations.ScanResultSummary.ScaResults.MoreInfoUrls = utils.UniqueUnion(sc.currentScan.Violations.ScanResultSummary.ScaResults.MoreInfoUrls, scaResponse.XrayDataUrl) + } + // Parse violations + parsed := datastructures.MakeSet[string]() + watches, failBuild, err := results.PrepareScaViolations( + target, + scaResponse.Violations, + sc.entitledForJas, + applicabilityRuns, + sc.getScaSecurityViolationHandler(parsed), + sc.getScaLicenseViolationHandler(parsed), + sc.getScaOperationalRiskViolationHandler(parsed), + ) + if err != nil { + return + } + sc.currentScan.Violations.Watches = utils.UniqueUnion(sc.currentScan.Violations.Watches, watches...) + sc.currentScan.Violations.FailBuild = sc.currentScan.Violations.FailBuild || failBuild + return +} + +func (sc *CmdResultsSummaryConverter) getScaSecurityViolationHandler(parsed *datastructures.Set[string]) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) (err error) { + for _, id := range getCveIds(cves, violation.IssueId) { + // PrepareScaViolations calls the handler for each violation and impacted component pair, we want to count unique violations + key := violation.WatchName + id + if parsed.Exists(key) { + continue + } + parsed.Add(key) + // Count the violation + scaSecurityHandler(sc.currentScan.Violations.ScanResultSummary.ScaResults, severity, applicabilityStatus) + } + return + } +} + +func (sc *CmdResultsSummaryConverter) getScaLicenseViolationHandler(parsed *datastructures.Set[string]) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) (err error) { + if sc.currentScan.Violations.ScaResults.License == nil { + sc.currentScan.Violations.ScaResults.License = formats.ResultSummary{} + } + // PrepareScaViolations calls the handler for each violation and impacted component pair, we want to count unique violations + key := violation.WatchName + violation.IssueId + if parsed.Exists(key) { + return + } + parsed.Add(key) + if _, ok := sc.currentScan.Violations.ScaResults.License[severity.String()]; !ok { + sc.currentScan.Violations.ScaResults.License[severity.String()] = map[string]int{} + } + sc.currentScan.Violations.ScaResults.License[severity.String()][formats.NoStatus]++ + return + } +} + +func (sc *CmdResultsSummaryConverter) getScaOperationalRiskViolationHandler(parsed *datastructures.Set[string]) results.ParseScaViolationFunc { + return func(violation services.Violation, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) (err error) { + if sc.currentScan.Violations.ScaResults.OperationalRisk == nil { + sc.currentScan.Violations.ScaResults.OperationalRisk = formats.ResultSummary{} + } + // PrepareScaViolations calls the handler for each violation and impacted component pair, we want to count unique violations + key := violation.WatchName + violation.IssueId + if parsed.Exists(key) { + return + } + parsed.Add(key) + if _, ok := sc.currentScan.Violations.ScaResults.OperationalRisk[severity.String()]; !ok { + sc.currentScan.Violations.ScaResults.OperationalRisk[severity.String()] = map[string]int{} + } + sc.currentScan.Violations.ScaResults.OperationalRisk[severity.String()][formats.NoStatus]++ + return + } +} + +func (sc *CmdResultsSummaryConverter) ParseVulnerabilities(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + if err = sc.validateBeforeParse(); err != nil || sc.currentScan.Vulnerabilities == nil { + return + } + if sc.currentScan.Vulnerabilities.ScaResults == nil { + sc.currentScan.Vulnerabilities.ScaResults = &formats.ScaScanResultSummary{} + } + // Parse general SCA results + if scaResponse.ScanId != "" { + sc.currentScan.Vulnerabilities.ScaResults.ScanIds = utils.UniqueUnion(sc.currentScan.Vulnerabilities.ScaResults.ScanIds, scaResponse.ScanId) + } + if scaResponse.XrayDataUrl != "" { + sc.currentScan.Vulnerabilities.ScaResults.MoreInfoUrls = utils.UniqueUnion(sc.currentScan.Vulnerabilities.ScaResults.MoreInfoUrls, scaResponse.XrayDataUrl) + } + // Parse vulnerabilities + parsed := datastructures.MakeSet[string]() + err = results.PrepareScaVulnerabilities( + target, + scaResponse.Vulnerabilities, + sc.entitledForJas, + applicabilityRuns, + sc.getScaVulnerabilityHandler(parsed), + ) + return +} + +func (sc *CmdResultsSummaryConverter) getScaVulnerabilityHandler(parsed *datastructures.Set[string]) results.ParseScaVulnerabilityFunc { + return func(vulnerability services.Vulnerability, cves []formats.CveRow, applicabilityStatus jasutils.ApplicabilityStatus, severity severityutils.Severity, impactedPackagesName, impactedPackagesVersion, impactedPackagesType string, fixedVersion []string, directComponents []formats.ComponentRow, impactPaths [][]formats.ComponentRow) (err error) { + for _, id := range getCveIds(cves, vulnerability.IssueId) { + // PrepareScaVulnerabilities calls the handler for each vulnerability and impacted component pair, we want to count unique vulnerabilities + if parsed.Exists(id) { + continue + } + parsed.Add(id) + // Count the vulnerability + scaSecurityHandler(sc.currentScan.Vulnerabilities.ScaResults, severity, applicabilityStatus) + } + return + } +} + +func scaSecurityHandler(scaResults *formats.ScaScanResultSummary, severity severityutils.Severity, applicabilityStatus jasutils.ApplicabilityStatus) { + if scaResults.Security == nil { + scaResults.Security = formats.ResultSummary{} + } + if _, ok := scaResults.Security[severity.String()]; !ok { + scaResults.Security[severity.String()] = map[string]int{} + } + if _, ok := scaResults.Security[severity.String()][applicabilityStatus.String()]; !ok { + scaResults.Security[severity.String()][applicabilityStatus.String()] = 0 + } + scaResults.Security[severity.String()][applicabilityStatus.String()]++ +} + +func getCveIds(cves []formats.CveRow, issueId string) []string { + ids := []string{} + for _, cve := range cves { + ids = append(ids, cve.Id) + } + if len(ids) == 0 { + ids = append(ids, issueId) + } + return ids +} + +func (sc *CmdResultsSummaryConverter) ParseLicenses(target results.ScanTarget, licenses []services.License) (err error) { + // Not supported in the summary + return +} + +func (sc *CmdResultsSummaryConverter) ParseSecrets(_ results.ScanTarget, secrets ...*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.SecretsResults == nil { + sc.currentScan.Vulnerabilities.SecretsResults = &formats.ResultSummary{} + } + return results.PrepareJasIssues(secrets, sc.entitledForJas, sc.getJasHandler(jasutils.Secrets)) +} + +func (sc *CmdResultsSummaryConverter) ParseIacs(_ results.ScanTarget, iacs ...*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.IacResults == nil { + sc.currentScan.Vulnerabilities.IacResults = &formats.ResultSummary{} + } + return results.PrepareJasIssues(iacs, sc.entitledForJas, sc.getJasHandler(jasutils.IaC)) +} + +func (sc *CmdResultsSummaryConverter) ParseSast(_ results.ScanTarget, sast ...*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.SastResults == nil { + sc.currentScan.Vulnerabilities.SastResults = &formats.ResultSummary{} + } + return results.PrepareJasIssues(sast, sc.entitledForJas, sc.getJasHandler(jasutils.Sast)) +} + +func (sc *CmdResultsSummaryConverter) getJasHandler(scanType jasutils.JasScanType) results.ParseJasFunc { + return func(run *sarif.Run, rule *sarif.ReportingDescriptor, severity severityutils.Severity, result *sarif.Result, location *sarif.Location) (err error) { + if location == nil { + // Only count the issue if it has a location + return + } + // Get the scanType count + var count *formats.ResultSummary + switch scanType { + case jasutils.Secrets: + count = sc.currentScan.Vulnerabilities.SecretsResults + case jasutils.IaC: + count = sc.currentScan.Vulnerabilities.IacResults + case jasutils.Sast: + count = sc.currentScan.Vulnerabilities.SastResults + } + if count == nil { + return + } + // PrepareJasIssues calls the handler for each issue (location) + if _, ok := (*count)[severity.String()]; !ok { + (*count)[severity.String()] = map[string]int{} + } + (*count)[severity.String()][formats.NoStatus] += 1 + return + } +} diff --git a/utils/results/conversion/summaryparser/summaryparser_test.go b/utils/results/conversion/summaryparser/summaryparser_test.go new file mode 100644 index 00000000..9f4234d0 --- /dev/null +++ b/utils/results/conversion/summaryparser/summaryparser_test.go @@ -0,0 +1,111 @@ +package summaryparser + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/stretchr/testify/assert" +) + +func TestGetCveIds(t *testing.T) { + testCases := []struct { + name string + cves []formats.CveRow + issueId string + expected []string + }{ + { + name: "No cves", + cves: []formats.CveRow{}, + issueId: "issueId", + expected: []string{"issueId"}, + }, + { + name: "One cve", + cves: []formats.CveRow{{Id: "CVE-1"}}, + issueId: "issueId", + expected: []string{"CVE-1"}, + }, + { + name: "Multiple cves", + cves: []formats.CveRow{{Id: "CVE-1"}, {Id: "CVE-2"}}, + issueId: "issueId", + expected: []string{"CVE-1", "CVE-2"}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := getCveIds(testCase.cves, testCase.issueId) + assert.Equal(t, testCase.expected, result) + }) + } +} + +func TestScaSecurityHandler(t *testing.T) { + testCases := []struct { + name string + severityCountsToProcess []map[severityutils.Severity]map[jasutils.ApplicabilityStatus]int + expected formats.ScaScanResultSummary + }{ + { + name: "No results", + severityCountsToProcess: []map[severityutils.Severity]map[jasutils.ApplicabilityStatus]int{}, + expected: formats.ScaScanResultSummary{Security: formats.ResultSummary{}}, + }, + { + name: "One result", + severityCountsToProcess: []map[severityutils.Severity]map[jasutils.ApplicabilityStatus]int{ + { + severityutils.Critical: {jasutils.Applicable: 1}, + }, + }, + expected: formats.ScaScanResultSummary{Security: formats.ResultSummary{"Critical": map[string]int{"Applicable": 1}}}, + }, + { + name: "Multiple results", + severityCountsToProcess: []map[severityutils.Severity]map[jasutils.ApplicabilityStatus]int{ + { + severityutils.Critical: {jasutils.Applicable: 1, jasutils.NotApplicable: 1}, + }, + { + severityutils.High: {jasutils.Applicable: 1}, + severityutils.Medium: {jasutils.NotScanned: 1}, + }, + { + severityutils.Low: {jasutils.NotCovered: 1}, + severityutils.High: {jasutils.Applicable: 1}, + }, + { + severityutils.Critical: {jasutils.Applicable: 1, jasutils.NotApplicable: 2}, + severityutils.Low: {jasutils.Applicable: 1}, + }, + }, + expected: formats.ScaScanResultSummary{Security: formats.ResultSummary{ + "Critical": {jasutils.Applicable.String(): 2, jasutils.NotApplicable.String(): 3}, + "High": {jasutils.Applicable.String(): 2}, + "Medium": {jasutils.NotScanned.String(): 1}, + "Low": {jasutils.Applicable.String(): 1, jasutils.NotCovered.String(): 1}, + }}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + scaSummaryResults := &formats.ScaScanResultSummary{Security: formats.ResultSummary{}} + assert.NotNil(t, scaSummaryResults) + for _, severityCounts := range testCase.severityCountsToProcess { + for severity, statusCounts := range severityCounts { + for status, count := range statusCounts { + for i := 0; i < count; i++ { + scaSecurityHandler(scaSummaryResults, severity, status) + } + } + } + } + assert.Equal(t, testCase.expected, *scaSummaryResults) + }) + } +} diff --git a/utils/results/conversion/tableparser/tableparser.go b/utils/results/conversion/tableparser/tableparser.go new file mode 100644 index 00000000..5383946f --- /dev/null +++ b/utils/results/conversion/tableparser/tableparser.go @@ -0,0 +1,70 @@ +package tableparser + +import ( + "github.com/owenrumney/go-sarif/v2/sarif" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion/simplejsonparser" + + "github.com/jfrog/jfrog-client-go/xray/services" +) + +type CmdResultsTableConverter struct { + simpleJsonConvertor *simplejsonparser.CmdResultsSimpleJsonConverter + // If supported, pretty print the output in the tables + pretty bool +} + +func NewCmdResultsTableConverter(pretty bool) *CmdResultsTableConverter { + return &CmdResultsTableConverter{pretty: pretty, simpleJsonConvertor: simplejsonparser.NewCmdResultsSimpleJsonConverter(pretty, true)} +} + +func (tc *CmdResultsTableConverter) Get() (formats.ResultsTables, error) { + simpleJsonFormat, err := tc.simpleJsonConvertor.Get() + if err != nil { + return formats.ResultsTables{}, err + } + return formats.ResultsTables{ + SecurityVulnerabilitiesTable: formats.ConvertToVulnerabilityTableRow(simpleJsonFormat.Vulnerabilities), + SecurityViolationsTable: formats.ConvertToVulnerabilityTableRow(simpleJsonFormat.SecurityViolations), + LicenseViolationsTable: formats.ConvertToLicenseViolationTableRow(simpleJsonFormat.LicensesViolations), + OperationalRiskViolationsTable: formats.ConvertToOperationalRiskViolationTableRow(simpleJsonFormat.OperationalRiskViolations), + SecretsTable: formats.ConvertToSecretsTableRow(simpleJsonFormat.Secrets), + IacTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.Iacs), + SastTable: formats.ConvertToIacOrSastTableRow(simpleJsonFormat.Sast), + }, nil +} + +func (tc *CmdResultsTableConverter) Reset(cmdType utils.CommandType, multiScanId, xrayVersion string, entitledForJas, multipleTargets bool) (err error) { + return tc.simpleJsonConvertor.Reset(cmdType, multiScanId, xrayVersion, entitledForJas, multipleTargets) +} + +func (tc *CmdResultsTableConverter) ParseNewTargetResults(target results.ScanTarget, errors ...error) (err error) { + return tc.simpleJsonConvertor.ParseNewTargetResults(target, errors...) +} + +func (tc *CmdResultsTableConverter) ParseViolations(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseViolations(target, scaResponse, applicabilityRuns...) +} + +func (tc *CmdResultsTableConverter) ParseVulnerabilities(target results.ScanTarget, scaResponse services.ScanResponse, applicabilityRuns ...*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseVulnerabilities(target, scaResponse, applicabilityRuns...) +} + +func (tc *CmdResultsTableConverter) ParseLicenses(target results.ScanTarget, licenses []services.License) (err error) { + return tc.simpleJsonConvertor.ParseLicenses(target, licenses) +} + +func (tc *CmdResultsTableConverter) ParseSecrets(target results.ScanTarget, secrets ...*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseSecrets(target, secrets...) +} + +func (tc *CmdResultsTableConverter) ParseIacs(target results.ScanTarget, iacs ...*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseIacs(target, iacs...) +} + +func (tc *CmdResultsTableConverter) ParseSast(target results.ScanTarget, sast ...*sarif.Run) (err error) { + return tc.simpleJsonConvertor.ParseSast(target, sast...) +} diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go new file mode 100644 index 00000000..5fecff97 --- /dev/null +++ b/utils/results/output/resultwriter.go @@ -0,0 +1,360 @@ +package output + +import ( + "fmt" + "os" + + "github.com/jfrog/jfrog-cli-core/v2/common/format" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +type ResultsWriter struct { + // The scan commandResults. + commandResults *results.SecurityCommandResults + // Format The output format. + format format.OutputFormat + // IncludeVulnerabilities If true, include all vulnerabilities as part of the output. Else, include violations only. + includeVulnerabilities bool + // If true, will print violation results. + hasViolationContext bool + // IncludeLicenses If true, also include license violations as part of the output. + includeLicenses bool + // IsMultipleRoots multipleRoots is set to true, in case the given results array contains (or may contain) results of several projects (like in binary scan). + isMultipleRoots *bool + // PrintExtended, If true, show extended results. + printExtended bool + // For table format - show table only for the given subScansPreformed + subScansPreformed []utils.SubScanType + // Messages - Option array of messages, to be displayed if the format is Table + messages []string +} + +func NewResultsWriter(scanResults *results.SecurityCommandResults) *ResultsWriter { + return &ResultsWriter{commandResults: scanResults} +} + +func (rw *ResultsWriter) SetOutputFormat(f format.OutputFormat) *ResultsWriter { + rw.format = f + return rw +} + +func (rw *ResultsWriter) SetIsMultipleRootProject(isMultipleRootProject bool) *ResultsWriter { + rw.isMultipleRoots = &isMultipleRootProject + return rw +} + +func (rw *ResultsWriter) SetSubScansPreformed(subScansPreformed []utils.SubScanType) *ResultsWriter { + rw.subScansPreformed = subScansPreformed + return rw +} + +func (rw *ResultsWriter) SetHasViolationContext(hasViolationContext bool) *ResultsWriter { + rw.hasViolationContext = hasViolationContext + return rw +} + +func (rw *ResultsWriter) SetIncludeVulnerabilities(includeVulnerabilities bool) *ResultsWriter { + rw.includeVulnerabilities = includeVulnerabilities + return rw +} + +func (rw *ResultsWriter) SetIncludeLicenses(licenses bool) *ResultsWriter { + rw.includeLicenses = licenses + return rw +} + +func (rw *ResultsWriter) SetPrintExtendedTable(extendedTable bool) *ResultsWriter { + rw.printExtended = extendedTable + return rw +} + +func (rw *ResultsWriter) SetExtraMessages(messages []string) *ResultsWriter { + rw.messages = messages + return rw +} + +func printMessages(messages []string) { + if len(messages) > 0 { + log.Output() + } + for _, m := range messages { + printMessage(m) + } +} + +func printMessage(message string) { + log.Output("💬" + message) +} + +func isPrettyOutputSupported() bool { + return log.IsStdOutTerminal() && log.IsColorsSupported() || os.Getenv("GITLAB_CI") != "" +} + +// PrintScanResults prints the scan results in the specified format. +// Note that errors are printed only with SimpleJson format. +func (rw *ResultsWriter) PrintScanResults() error { + if rw.commandResults.GetErrors() != nil && !rw.commandResults.HasInformation() { + // Don't print if there are no results and only errors. + return nil + } + switch rw.format { + case format.Table: + return rw.printTables() + case format.SimpleJson: + // Helper for Debugging purposes, print the raw results to the log + if err := rw.printOrSaveRawResults(false); err != nil { + return err + } + simpleJson, err := rw.createResultsConvertor(false).ConvertToSimpleJson(rw.commandResults) + if err != nil { + return err + } + return PrintJson(simpleJson) + case format.Json: + return PrintJson(rw.commandResults.GetScaScansXrayResults()) + case format.Sarif: + // Helper for Debugging purposes, print the raw results to the log + if err := rw.printOrSaveRawResults(false); err != nil { + return err + } + return rw.printSarif() + } + return nil +} + +func (rw *ResultsWriter) createResultsConvertor(pretty bool) *conversion.CommandResultsConvertor { + return conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + IsMultipleRoots: rw.isMultipleRoots, + IncludeLicenses: rw.includeLicenses, + IncludeVulnerabilities: rw.includeVulnerabilities, + HasViolationContext: rw.hasViolationContext, + RequestedScans: rw.subScansPreformed, + Pretty: pretty, + }) +} + +func (rw *ResultsWriter) printSarif() error { + sarifContent, err := rw.createResultsConvertor(false).ConvertToSarif(rw.commandResults) + if err != nil { + return err + } + sarifFile, err := WriteSarifResultsAsString(sarifContent, false) + if err != nil { + return err + } + log.Output(sarifFile) + return nil +} + +func PrintJson(output interface{}) (err error) { + results, err := utils.GetAsJsonString(output, true, true) + if err != nil { + return + } + log.Output(results) + return nil +} + +// If "CI" env var is true, print raw JSON of the results. Otherwise, save it as a file and print a link to it. +// If printMsg is true, print it to the console. Otherwise, print the message to the log. +func (rw *ResultsWriter) printOrSaveRawResults(printMsg bool) (err error) { + if !rw.commandResults.HasInformation() { + log.Debug("No information to print") + return + } + if printMsg && !utils.IsCI() { + // Save the results to a file and print a link to it. + var resultsPath string + if resultsPath, err = WriteJsonResults(rw.commandResults); err != nil { + return + } + printMessage(coreutils.PrintTitle("The full scan results are available here: ") + coreutils.PrintLink(resultsPath)) + return + } + // Print the raw results to console. + var msg string + if msg, err = utils.GetAsJsonString(rw.commandResults, false, true); err != nil { + return + } + log.Debug(fmt.Sprintf("Raw scan results:\n%s", msg)) + return +} + +func (rw *ResultsWriter) printTables() (err error) { + tableContent, err := rw.createResultsConvertor(isPrettyOutputSupported()).ConvertToTable(rw.commandResults) + if err != nil { + return + } + printMessages(rw.messages) + if err = rw.printOrSaveRawResults(true); err != nil { + return + } + if utils.IsScanRequested(rw.commandResults.CmdType, utils.ScaScan, rw.subScansPreformed...) { + if rw.hasViolationContext { + if err = PrintViolationsTable(tableContent, rw.commandResults.CmdType, rw.printExtended); err != nil { + return + } + } + if rw.includeVulnerabilities { + if err = PrintVulnerabilitiesTable(tableContent, rw.commandResults.CmdType, len(rw.commandResults.GetTechnologies()) > 0, rw.printExtended); err != nil { + return + } + } + if rw.includeLicenses { + if err = PrintLicensesTable(tableContent, rw.printExtended, rw.commandResults.CmdType); err != nil { + return + } + } + } + if utils.IsScanRequested(rw.commandResults.CmdType, utils.SecretsScan, rw.subScansPreformed...) { + if err = PrintSecretsTable(tableContent, rw.commandResults.EntitledForJas, rw.commandResults.SecretValidation); err != nil { + return + } + } + if utils.IsScanRequested(rw.commandResults.CmdType, utils.IacScan, rw.subScansPreformed...) { + if err = PrintJasTable(tableContent, rw.commandResults.EntitledForJas, jasutils.IaC); err != nil { + return + } + } + if !utils.IsScanRequested(rw.commandResults.CmdType, utils.SastScan, rw.subScansPreformed...) { + return nil + } + return PrintJasTable(tableContent, rw.commandResults.EntitledForJas, jasutils.Sast) +} + +// PrintVulnerabilitiesTable prints the vulnerabilities in a table. +// Set printExtended to true to print fields with 'extended' tag. +// If the scan argument is set to true, print the scan tables. +func PrintVulnerabilitiesTable(tables formats.ResultsTables, cmdType utils.CommandType, techDetected, printExtended bool) error { + // Space before the tables + log.Output() + if cmdType.IsTargetBinary() { + return coreutils.PrintTable(formats.ConvertSecurityTableRowToScanTableRow(tables.SecurityVulnerabilitiesTable), + "Vulnerable Components", + "✨ No vulnerable components were found ✨", + printExtended, + ) + } + emptyTableMessage := "✨ No vulnerable dependencies were found ✨" + if !techDetected { + emptyTableMessage = coreutils.PrintYellow("🔧 Couldn't determine a package manager or build tool used by this project 🔧") + } + return coreutils.PrintTable(tables.SecurityVulnerabilitiesTable, "Vulnerable Dependencies", emptyTableMessage, printExtended) +} + +// PrintViolationsTable prints the violations in 4 tables: security violations, license compliance violations, operational risk violations and ignore rule URLs. +// Set printExtended to true to print fields with 'extended' tag. +// If the scan argument is set to true, print the scan tables. +func PrintViolationsTable(tables formats.ResultsTables, cmdType utils.CommandType, printExtended bool) (err error) { + // Space before the tables + log.Output() + if cmdType.IsTargetBinary() { + err = coreutils.PrintTable(formats.ConvertSecurityTableRowToScanTableRow(tables.SecurityViolationsTable), "Security Violations", "No security violations were found", printExtended) + if err != nil { + return err + } + err = coreutils.PrintTable(formats.ConvertLicenseViolationTableRowToScanTableRow(tables.LicenseViolationsTable), "License Compliance Violations", "No license compliance violations were found", printExtended) + if err != nil { + return err + } + if len(tables.OperationalRiskViolationsTable) > 0 { + return coreutils.PrintTable(formats.ConvertOperationalRiskTableRowToScanTableRow(tables.OperationalRiskViolationsTable), "Operational Risk Violations", "No operational risk violations were found", printExtended) + } + } else { + err = coreutils.PrintTable(tables.SecurityViolationsTable, "Security Violations", "No security violations were found", printExtended) + if err != nil { + return err + } + err = coreutils.PrintTable(tables.LicenseViolationsTable, "License Compliance Violations", "No license compliance violations were found", printExtended) + if err != nil { + return err + } + if len(tables.OperationalRiskViolationsTable) > 0 { + return coreutils.PrintTable(tables.OperationalRiskViolationsTable, "Operational Risk Violations", "No operational risk violations were found", printExtended) + } + } + return nil +} + +// PrintLicensesTable prints the licenses in a table. +// Set multipleRoots to true in case the given licenses array contains (or may contain) results of several projects or files (like in binary scan). +// In case multipleRoots is true, the field Component will show the root of each impact path, otherwise it will show the root's child. +// Set printExtended to true to print fields with 'extended' tag. +// If the scan argument is set to true, print the scan tables. +func PrintLicensesTable(tables formats.ResultsTables, printExtended bool, cmdType utils.CommandType) error { + // Space before the tables + log.Output() + if cmdType.IsTargetBinary() { + return coreutils.PrintTable(formats.ConvertLicenseTableRowToScanTableRow(tables.LicensesTable), "Licenses", "No licenses were found", printExtended) + } + return coreutils.PrintTable(tables.LicensesTable, "Licenses", "No licenses were found", printExtended) +} + +func PrintSecretsTable(tables formats.ResultsTables, entitledForJas, tokenValidationEnabled bool) (err error) { + if !entitledForJas { + return + } + if err = PrintJasTable(tables, entitledForJas, jasutils.Secrets); err != nil { + return + } + if tokenValidationEnabled { + log.Output("This table contains multiple secret types, such as tokens, generic password, ssh keys and more, token validation is only supported on tokens.") + } + return +} + +func PrintJasTable(tables formats.ResultsTables, entitledForJas bool, scanType jasutils.JasScanType) error { + if !entitledForJas { + return nil + } + // Space before the tables + log.Output() + switch scanType { + case jasutils.Secrets: + return coreutils.PrintTable(tables.SecretsTable, "Secret Detection", + "✨ No secrets were found ✨", false) + case jasutils.IaC: + return coreutils.PrintTable(tables.IacTable, "Infrastructure as Code Vulnerabilities", + "✨ No Infrastructure as Code vulnerabilities were found ✨", false) + case jasutils.Sast: + return coreutils.PrintTable(tables.SastTable, "Static Application Security Testing (SAST)", + "✨ No Static Application Security Testing vulnerabilities were found ✨", false) + } + return nil +} + +func WriteJsonResults(results *results.SecurityCommandResults) (resultsPath string, err error) { + out, err := fileutils.CreateTempFile() + if errorutils.CheckError(err) != nil { + return + } + defer func() { + e := out.Close() + if err == nil { + err = e + } + }() + content, err := utils.GetAsJsonBytes(results, true, true) + if err != nil { + return + } + _, err = out.Write(content) + if errorutils.CheckError(err) != nil { + return + } + resultsPath = out.Name() + return +} + +func WriteSarifResultsAsString(report *sarif.Report, escape bool) (sarifStr string, err error) { + return utils.GetAsJsonString(report, escape, true) +} diff --git a/utils/securityJobSummary.go b/utils/results/output/securityJobSummary.go similarity index 85% rename from utils/securityJobSummary.go rename to utils/results/output/securityJobSummary.go index 67b0a25b..ae15da2c 100644 --- a/utils/securityJobSummary.go +++ b/utils/results/output/securityJobSummary.go @@ -1,4 +1,4 @@ -package utils +package output import ( "errors" @@ -14,10 +14,13 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/commandsummary" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/resources" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "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-cli-security/utils/results/conversion" "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -66,36 +69,44 @@ func getStatusIcon(failed bool) string { type SecurityJobSummary struct{} -func newResultSummary(cmdResults *Results, cmdType CommandType, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary) { - summary.ResultType = cmdType +func newResultSummary(cmdResults *results.SecurityCommandResults, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary, err error) { + summary.ResultType = cmdResults.CmdType summary.Args = &ResultSummaryArgs{BaseJfrogUrl: serverDetails.Url} - summary.Summary = ToSummary(cmdResults, vulnerabilitiesRequested, violationsRequested) + summary.Summary, err = conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + IncludeVulnerabilities: vulnerabilitiesRequested, + HasViolationContext: violationsRequested, + Pretty: true, + }).ConvertToSummary(cmdResults) return } -func NewBuildScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested bool, buildName, buildNumber string) (summary ScanCommandResultSummary) { - summary = newResultSummary(cmdResults, Build, serverDetails, vulnerabilitiesRequested, true) +func NewBuildScanSummary(cmdResults *results.SecurityCommandResults, serverDetails *config.ServerDetails, vulnerabilitiesRequested bool, buildName, buildNumber string) (summary ScanCommandResultSummary, err error) { + if summary, err = newResultSummary(cmdResults, serverDetails, vulnerabilitiesRequested, true); err != nil { + return + } summary.Args.BuildName = buildName summary.Args.BuildNumbers = []string{buildNumber} return } -func NewDockerScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool, dockerImage string) (summary ScanCommandResultSummary) { - summary = newResultSummary(cmdResults, DockerImage, serverDetails, vulnerabilitiesRequested, violationsRequested) +func NewDockerScanSummary(cmdResults *results.SecurityCommandResults, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool, dockerImage string) (summary ScanCommandResultSummary, err error) { + if summary, err = newResultSummary(cmdResults, serverDetails, vulnerabilitiesRequested, violationsRequested); err != nil { + return + } summary.Args.DockerImage = dockerImage return } -func NewBinaryScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary) { - return newResultSummary(cmdResults, Binary, serverDetails, vulnerabilitiesRequested, violationsRequested) +func NewBinaryScanSummary(cmdResults *results.SecurityCommandResults, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary, err error) { + return newResultSummary(cmdResults, serverDetails, vulnerabilitiesRequested, violationsRequested) } -func NewAuditScanSummary(cmdResults *Results, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary) { - return newResultSummary(cmdResults, SourceCode, serverDetails, vulnerabilitiesRequested, violationsRequested) +func NewAuditScanSummary(cmdResults *results.SecurityCommandResults, serverDetails *config.ServerDetails, vulnerabilitiesRequested, violationsRequested bool) (summary ScanCommandResultSummary, err error) { + return newResultSummary(cmdResults, serverDetails, vulnerabilitiesRequested, violationsRequested) } func NewCurationSummary(cmdResult formats.ResultsSummary) (summary ScanCommandResultSummary) { - summary.ResultType = Curation + summary.ResultType = utils.Curation summary.Summary = cmdResult return } @@ -139,7 +150,7 @@ func (rsa ResultSummaryArgs) ToArgs(index commandsummary.Index) (args []string) } type ScanCommandResultSummary struct { - ResultType CommandType `json:"resultType"` + ResultType utils.CommandType `json:"resultType"` Args *ResultSummaryArgs `json:"args,omitempty"` Summary formats.ResultsSummary `json:"summary"` } @@ -173,25 +184,30 @@ func RecordSecurityCommandSummary(content ScanCommandResultSummary) (err error) return manager.Record(content) } -func RecordSarifOutput(cmdResults *Results, supportedScans []SubScanType) (err error) { +func RecordSarifOutput(cmdResults *results.SecurityCommandResults, includeVulnerabilities, hasViolationContext bool, requestedScans ...utils.SubScanType) (err error) { + // Verify if we should record the results manager, err := getRecordManager() if err != nil || manager == nil { return } - if cmdResults.ExtendedScanResults == nil || !cmdResults.ExtendedScanResults.EntitledForJas { + if !cmdResults.EntitledForJas || !commandsummary.StaticMarkdownConfig.IsExtendedSummary() { // If no JAS no GHAS - return - } - extended := true - if !extended && !commandsummary.StaticMarkdownConfig.IsExtendedSummary() { log.Info("Results can be uploaded to Github security tab automatically by upgrading your JFrog subscription.") return } - sarifReport, err := GenerateSarifReportFromResults(cmdResults, true, false, nil, supportedScans) + // Convert the results to SARIF format + sarifReport, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + IncludeVulnerabilities: includeVulnerabilities, + HasViolationContext: hasViolationContext, + PatchBinaryPaths: true, + RequestedScans: requestedScans, + Pretty: true, + }).ConvertToSarif(cmdResults) if err != nil { return err } - out, err := JSONMarshalNotEscaped(sarifReport) + // Record the SARIF report + out, err := utils.GetAsJsonBytes(sarifReport, false, false) if err != nil { return errorutils.CheckError(err) } @@ -218,7 +234,7 @@ func CombineSarifOutputFiles(dataFilePaths []string) (data []byte, err error) { if err != nil { return } - return JSONMarshalNotEscaped(combined) + return utils.GetAsJsonBytes(combined, false, false) } func loadSarifReport(dataFilePath string) (report *sarif.Report, err error) { @@ -244,15 +260,15 @@ func updateSummaryNamesToRelativePath(summary *formats.ResultsSummary, wd string } } -func getDataIndexFromCommandType(cmdType CommandType) commandsummary.Index { +func getDataIndexFromCommandType(cmdType utils.CommandType) commandsummary.Index { switch cmdType { - case Build: + case utils.Build: return commandsummary.BuildScan - case Binary: + case utils.Binary: return commandsummary.BinariesScan - case SourceCode: + case utils.SourceCode: return commandsummary.BinariesScan - case DockerImage: + case utils.DockerImage: return commandsummary.DockerScan } // No index for the section @@ -273,11 +289,11 @@ func recordIndexData(manager *commandsummary.CommandSummary, content ScanCommand return } -func newScanCommandResultSummary(resultType CommandType, args *ResultSummaryArgs, scans ...formats.ScanSummary) ScanCommandResultSummary { +func newScanCommandResultSummary(resultType utils.CommandType, args *ResultSummaryArgs, scans ...formats.ScanSummary) ScanCommandResultSummary { return ScanCommandResultSummary{ResultType: resultType, Args: args, Summary: formats.ResultsSummary{Scans: scans}} } -func loadContent(dataFiles []string, filterSections ...CommandType) ([]formats.ResultsSummary, ResultSummaryArgs, error) { +func loadContent(dataFiles []string, filterSections ...utils.CommandType) ([]formats.ResultsSummary, ResultSummaryArgs, error) { data := []formats.ResultsSummary{} args := ResultSummaryArgs{} for _, dataFilePath := range dataFiles { @@ -330,7 +346,7 @@ func (js *SecurityJobSummary) GetNonScannedResult() (generator EmptyMarkdownGene // Generate the Security section (Curation) func (js *SecurityJobSummary) GenerateMarkdownFromFiles(dataFilePaths []string) (markdown string, err error) { - curationData, _, err := loadContent(dataFilePaths, Curation) + curationData, _, err := loadContent(dataFilePaths, utils.Curation) if err != nil { return } diff --git a/utils/securityJobSummary_test.go b/utils/results/output/securityJobSummary_test.go similarity index 84% rename from utils/securityJobSummary_test.go rename to utils/results/output/securityJobSummary_test.go index abcc4f3b..ef744c81 100644 --- a/utils/securityJobSummary_test.go +++ b/utils/results/output/securityJobSummary_test.go @@ -1,12 +1,15 @@ -package utils +package output import ( "fmt" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/commandsummary" coreUtils "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - "github.com/jfrog/jfrog-cli-security/formats" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/validations" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" clientTests "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" @@ -17,9 +20,7 @@ import ( ) var ( - summaryExpectedContentDir = filepath.Join("..", "tests", "testdata", "other", "jobSummary") - testPlatformUrl = "https://test-platform-url.jfrog.io/" - testMoreInfoUrl = "https://test-more-info-url.jfrog.io/" + summaryExpectedContentDir = filepath.Join("..", "..", "..", "tests", "testdata", "output", "jobSummary") securityScaResults = formats.ResultSummary{ "Critical": map[string]int{jasutils.Applicable.String(): 2, jasutils.NotApplicable.String(): 2, jasutils.NotCovered.String(): 3, jasutils.ApplicabilityUndetermined.String(): 1}, @@ -29,8 +30,8 @@ var ( } violationResults = formats.ScanResultSummary{ ScaResults: &formats.ScaScanResultSummary{ - ScanIds: []string{TestScaScanId}, - MoreInfoUrls: []string{testMoreInfoUrl}, + ScanIds: []string{validations.TestScaScanId}, + MoreInfoUrls: []string{validations.TestMoreInfoUrl}, Security: securityScaResults, License: formats.ResultSummary{"High": map[string]int{formats.NoStatus: 1}}, OperationalRisk: formats.ResultSummary{"Low": map[string]int{formats.NoStatus: 2}}, @@ -44,10 +45,6 @@ func TestSaveSarifOutputOnlyForJasEntitled(t *testing.T) { name string isJasEntitled bool }{ - { - name: "JAS entitled", - isJasEntitled: true, - }, { name: "JAS not entitled", isJasEntitled: false, @@ -61,16 +58,14 @@ func TestSaveSarifOutputOnlyForJasEntitled(t *testing.T) { cleanUp := clientTests.SetEnvWithCallbackAndAssert(t, coreUtils.SummaryOutputDirPathEnv, tempDir) defer cleanUp() - assert.NoError(t, RecordSarifOutput(createDummyJasResult(testCase.isJasEntitled), GetAllSupportedScans())) + assert.NoError(t, RecordSarifOutput(createDummyJasResult(testCase.isJasEntitled), true, true, utils.GetAllSupportedScans()...)) assert.Equal(t, testCase.isJasEntitled, hasFilesInDir(t, filepath.Join(tempDir, commandsummary.OutputDirName, "security", string(commandsummary.SarifReport)))) }) } } -func createDummyJasResult(entitled bool) *Results { - return &Results{ - ExtendedScanResults: &ExtendedScanResults{EntitledForJas: entitled}, - } +func createDummyJasResult(entitled bool) *results.SecurityCommandResults { + return &results.SecurityCommandResults{EntitledForJas: entitled} } func hasFilesInDir(t *testing.T, dir string) bool { @@ -86,9 +81,9 @@ func hasFilesInDir(t *testing.T, dir string) bool { func TestSaveLoadData(t *testing.T) { testDockerScanSummary := ScanCommandResultSummary{ - ResultType: DockerImage, + ResultType: utils.DockerImage, Args: &ResultSummaryArgs{ - BaseJfrogUrl: testPlatformUrl, + BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version", }, Summary: formats.ResultsSummary{ @@ -97,8 +92,8 @@ func TestSaveLoadData(t *testing.T) { Target: filepath.Join("path", "to", "image.tar"), Vulnerabilities: &formats.ScanResultSummary{ ScaResults: &formats.ScaScanResultSummary{ - ScanIds: []string{TestScaScanId}, - MoreInfoUrls: []string{testMoreInfoUrl}, + ScanIds: []string{validations.TestScaScanId}, + MoreInfoUrls: []string{validations.TestMoreInfoUrl}, Security: securityScaResults, }, }, @@ -111,9 +106,9 @@ func TestSaveLoadData(t *testing.T) { }, } testBinaryScanSummary := ScanCommandResultSummary{ - ResultType: Binary, + ResultType: utils.Binary, Args: &ResultSummaryArgs{ - BaseJfrogUrl: testPlatformUrl, + BaseJfrogUrl: validations.TestPlatformUrl, }, Summary: formats.ResultsSummary{ Scans: []formats.ScanSummary{ @@ -138,9 +133,9 @@ func TestSaveLoadData(t *testing.T) { }, } testBuildScanSummary := ScanCommandResultSummary{ - ResultType: Build, + ResultType: utils.Build, Args: &ResultSummaryArgs{ - BaseJfrogUrl: testPlatformUrl, + BaseJfrogUrl: validations.TestPlatformUrl, BuildName: "build-name", BuildNumbers: []string{"build-number"}, }, @@ -157,7 +152,7 @@ func TestSaveLoadData(t *testing.T) { }, } testCurationSummary := ScanCommandResultSummary{ - ResultType: Curation, + ResultType: utils.Curation, Summary: formats.ResultsSummary{ Scans: []formats.ScanSummary{ { @@ -180,7 +175,7 @@ func TestSaveLoadData(t *testing.T) { testCases := []struct { name string content []ScanCommandResultSummary - filterSections []CommandType + filterSections []utils.CommandType expectedArgs ResultSummaryArgs expectedContent []formats.ResultsSummary }{ @@ -194,7 +189,7 @@ func TestSaveLoadData(t *testing.T) { name: "Multiple scans", content: []ScanCommandResultSummary{testDockerScanSummary, testBinaryScanSummary, testBuildScanSummary}, expectedArgs: ResultSummaryArgs{ - BaseJfrogUrl: testPlatformUrl, + BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version", BuildName: "build-name", BuildNumbers: []string{"build-number"}, @@ -203,7 +198,7 @@ func TestSaveLoadData(t *testing.T) { }, { name: "Multiple scans with filter", - filterSections: []CommandType{Curation}, + filterSections: []utils.CommandType{utils.Curation}, content: []ScanCommandResultSummary{testDockerScanSummary, testBinaryScanSummary, testBuildScanSummary, testCurationSummary}, expectedContent: []formats.ResultsSummary{testCurationSummary.Summary}, }, @@ -216,7 +211,7 @@ func TestSaveLoadData(t *testing.T) { // Save the data for i := range testCase.content { updateSummaryNamesToRelativePath(&testCase.content[i].Summary, tempDir) - data, err := JSONMarshalNotEscaped(&testCase.content[i]) + data, err := utils.GetAsJsonBytes(&testCase.content[i], false, false) assert.NoError(t, err) dataFilePath := filepath.Join(tempDir, fmt.Sprintf("data_%s_%d.json", testCase.name, i)) assert.NoError(t, os.WriteFile(dataFilePath, data, 0644)) @@ -292,11 +287,11 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { name: "No vulnerabilities", index: commandsummary.BinariesScan, expectedContentPath: filepath.Join(summaryExpectedContentDir, "no_vulnerabilities.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "binary-name"), - Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ScanIds: []string{TestScaScanId}, MoreInfoUrls: []string{testMoreInfoUrl}}}, + Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ScanIds: []string{validations.TestScaScanId}, MoreInfoUrls: []string{validations.TestMoreInfoUrl}}}, }}, }}, }, @@ -305,11 +300,11 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { index: commandsummary.BinariesScan, violations: true, expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations_not_defined.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "binary-name"), - Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ScanIds: []string{TestScaScanId}}}, + Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ScanIds: []string{validations.TestScaScanId}}}, }}, }}, }, @@ -318,13 +313,13 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { index: commandsummary.BinariesScan, violations: true, expectedContentPath: filepath.Join(summaryExpectedContentDir, "no_violations.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "other-binary-name"), Violations: &formats.ScanViolationsSummary{ Watches: []string{}, - ScanResultSummary: formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ScanIds: []string{TestScaScanId}, MoreInfoUrls: []string{testMoreInfoUrl}}}, + ScanResultSummary: formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ScanIds: []string{validations.TestScaScanId}, MoreInfoUrls: []string{validations.TestMoreInfoUrl}}}, }, }}, }}, @@ -333,13 +328,13 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { name: "Build Scan Vulnerabilities", index: commandsummary.BuildScan, expectedContentPath: filepath.Join(summaryExpectedContentDir, "build_scan_vulnerabilities.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl, BuildName: "build-name", BuildNumbers: []string{"build-number"}}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, BuildName: "build-name", BuildNumbers: []string{"build-number"}}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: "build-name (build-number)", Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ - ScanIds: []string{TestScaScanId}, - MoreInfoUrls: []string{testMoreInfoUrl}, + ScanIds: []string{validations.TestScaScanId}, + MoreInfoUrls: []string{validations.TestMoreInfoUrl}, Security: formats.ResultSummary{"High": map[string]int{formats.NoStatus: 3}, "Medium": map[string]int{formats.NoStatus: 1}, "Unknown": map[string]int{formats.NoStatus: 20}}, }}, }}, @@ -349,12 +344,12 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { name: "Binary Scan Vulnerabilities", index: commandsummary.BinariesScan, expectedContentPath: filepath.Join(summaryExpectedContentDir, "binary_vulnerabilities.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "binary-with-issues"), Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ - ScanIds: []string{TestScaScanId, "scan-id-2"}, + ScanIds: []string{validations.TestScaScanId, "scan-id-2"}, MoreInfoUrls: []string{""}, Security: formats.ResultSummary{"Critical": map[string]int{formats.NoStatus: 33}, "Low": map[string]int{formats.NoStatus: 11}}, }}, @@ -365,13 +360,13 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { name: "Docker Scan Vulnerabilities", index: commandsummary.DockerScan, expectedContentPath: filepath.Join(summaryExpectedContentDir, "docker_vulnerabilities.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl, DockerImage: "dockerImage:version"}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "image.tar"), Vulnerabilities: &formats.ScanResultSummary{ ScaResults: &formats.ScaScanResultSummary{ - ScanIds: []string{TestScaScanId}, + ScanIds: []string{validations.TestScaScanId}, MoreInfoUrls: []string{""}, Security: securityScaResults, }, @@ -387,7 +382,7 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { index: commandsummary.DockerScan, violations: true, expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl, DockerImage: "dockerImage:version"}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "image.tar"), @@ -404,7 +399,7 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { violations: true, NoExtendedView: true, expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations_not_extended_view.md"), - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl, DockerImage: "dockerImage:version"}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "image.tar"), @@ -418,7 +413,7 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { { name: "Vulnerability not requested", index: commandsummary.DockerScan, - args: &ResultSummaryArgs{BaseJfrogUrl: testPlatformUrl, DockerImage: "dockerImage:version"}, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "image.tar"), diff --git a/utils/results/results.go b/utils/results/results.go new file mode 100644 index 00000000..cc495fa0 --- /dev/null +++ b/utils/results/results.go @@ -0,0 +1,362 @@ +package results + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +// SecurityCommandResults is a struct that holds the results of a security scan/audit command. +type SecurityCommandResults struct { + // General fields describing the command metadata + XrayVersion string `json:"xray_version"` + EntitledForJas bool `json:"jas_entitled"` + SecretValidation bool `json:"secret_validation,omitempty"` + CmdType utils.CommandType `json:"command_type"` + // MultiScanId is a unique identifier that is used to group multiple scans together. + MultiScanId string `json:"multi_scan_id,omitempty"` + // Results for each target in the command + Targets []*TargetResults `json:"targets"` + targetsMutex sync.Mutex `json:"-"` + // Error that occurred during the command execution + Error error `json:"error,omitempty"` +} + +type TargetResults struct { + ScanTarget + // All scan results for the target + ScaResults *ScaScanResults `json:"sca_scans,omitempty"` + JasResults *JasScansResults `json:"jas_scans,omitempty"` + // Errors that occurred during the scans + Errors []error `json:"errors,omitempty"` + errorsMutex sync.Mutex `json:"-"` +} + +type ScaScanResults struct { + IsMultipleRootProject *bool `json:"is_multiple_root_project,omitempty"` + // Target of the scan + Descriptors []string `json:"descriptors,omitempty"` + // Sca scan results + XrayResults []services.ScanResponse `json:"xray_scan,omitempty"` +} + +type JasScansResults struct { + ApplicabilityScanResults []*sarif.Run `json:"contextual_analysis,omitempty"` + SecretsScanResults []*sarif.Run `json:"secrets,omitempty"` + IacScanResults []*sarif.Run `json:"iac,omitempty"` + SastScanResults []*sarif.Run `json:"sast,omitempty"` +} + +type ScanTarget struct { + // Physical location of the target: Working directory (audit) / binary to scan (scan / docker scan) + Target string `json:"target,omitempty"` + // Logical name of the target (build name / module name / docker image name...) + Name string `json:"name,omitempty"` + // Optional field (not used only in build scan) to provide the technology of the target + Technology techutils.Technology `json:"technology,omitempty"` +} + +func (st ScanTarget) Copy(newTarget string) ScanTarget { + return ScanTarget{Target: newTarget, Name: st.Name, Technology: st.Technology} +} + +func (st ScanTarget) String() (str string) { + str = st.Target + if st.Name != "" { + str = st.Name + } + if st.Technology != "" { + str += fmt.Sprintf(" [%s]", st.Technology) + } + return +} + +func NewCommandResults(cmdType utils.CommandType, xrayVersion string, entitledForJas, secretValidation bool) *SecurityCommandResults { + return &SecurityCommandResults{CmdType: cmdType, XrayVersion: xrayVersion, EntitledForJas: entitledForJas, SecretValidation: secretValidation, targetsMutex: sync.Mutex{}} +} + +func (r *SecurityCommandResults) SetMultiScanId(multiScanId string) *SecurityCommandResults { + r.MultiScanId = multiScanId + return r +} + +// --- Aggregated results for all targets --- + +func (r *SecurityCommandResults) GetTargetsPaths() (paths []string) { + for _, scan := range r.Targets { + paths = append(paths, scan.Target) + } + return +} + +func (r *SecurityCommandResults) GetScaScansXrayResults() (results []services.ScanResponse) { + for _, scan := range r.Targets { + results = append(results, scan.GetScaScansXrayResults()...) + } + return +} + +func (r *SecurityCommandResults) GetJasScansResults(scanType jasutils.JasScanType) (results []*sarif.Run) { + if !r.EntitledForJas { + return + } + for _, scan := range r.Targets { + results = append(results, scan.GetJasScansResults(scanType)...) + } + return +} + +func (r *SecurityCommandResults) GetErrors() (err error) { + err = r.Error + for _, target := range r.Targets { + if targetErr := target.GetErrors(); targetErr != nil { + err = errors.Join(err, fmt.Errorf("target '%s' errors:\n%s", target.String(), targetErr)) + } + } + return +} + +func (r *SecurityCommandResults) GetTechnologies(additionalTechs ...techutils.Technology) []techutils.Technology { + technologies := datastructures.MakeSetFromElements(additionalTechs...) + for _, scan := range r.Targets { + technologies.AddElements(scan.GetTechnologies()...) + } + return technologies.ToSlice() +} + +// In case multipleRoots is true, the field Component will show the root of each impact path, otherwise it will show the root's child. +// Set multipleRoots to true in case the given vulnerabilities array contains (or may contain) results of several projects or files (like in binary scan). +func (r *SecurityCommandResults) HasMultipleTargets() bool { + if len(r.Targets) > 1 { + return true + } + for _, scanTarget := range r.Targets { + // If there is more than one SCA scan target (i.e multiple files with dependencies information) + if scanTarget.ScaResults != nil && (len(scanTarget.ScaResults.XrayResults) > 1 || (scanTarget.ScaResults.IsMultipleRootProject != nil && *scanTarget.ScaResults.IsMultipleRootProject)) { + return true + } + } + return false +} + +func (r *SecurityCommandResults) HasInformation() bool { + for _, scan := range r.Targets { + if scan.HasInformation() { + return true + } + } + return false +} + +func (r *SecurityCommandResults) HasFindings() bool { + for _, scan := range r.Targets { + if scan.HasFindings() { + return true + } + } + return false +} + +// --- Scan on a target --- + +func (r *SecurityCommandResults) NewScanResults(target ScanTarget) *TargetResults { + targetResults := &TargetResults{ScanTarget: target, errorsMutex: sync.Mutex{}} + if r.EntitledForJas { + targetResults.JasResults = &JasScansResults{} + } + + r.targetsMutex.Lock() + r.Targets = append(r.Targets, targetResults) + r.targetsMutex.Unlock() + return targetResults +} + +func (sr *TargetResults) GetErrors() (err error) { + for _, targetErr := range sr.Errors { + err = errors.Join(err, targetErr) + } + return +} + +func (sr *TargetResults) GetWatches() []string { + watches := datastructures.MakeSet[string]() + for _, xrayResults := range sr.GetScaScansXrayResults() { + for _, violation := range xrayResults.Violations { + if violation.WatchName != "" { + watches.Add(violation.WatchName) + } + } + } + return watches.ToSlice() +} + +func (sr *TargetResults) GetScanIds() []string { + scanIds := datastructures.MakeSet[string]() + for _, xrayResults := range sr.GetScaScansXrayResults() { + if xrayResults.ScanId != "" { + scanIds.Add(xrayResults.ScanId) + } + } + return scanIds.ToSlice() +} + +func (sr *TargetResults) GetScaScansXrayResults() (results []services.ScanResponse) { + if sr.ScaResults == nil { + return + } + results = append(results, sr.ScaResults.XrayResults...) + return +} + +func (sr *TargetResults) GetTechnologies() []techutils.Technology { + technologiesSet := datastructures.MakeSet[techutils.Technology]() + if sr.Technology != "" { + technologiesSet.Add(sr.Technology) + } + if sr.ScaResults == nil { + return technologiesSet.ToSlice() + } + for _, scaResult := range sr.ScaResults.XrayResults { + for _, vulnerability := range scaResult.Vulnerabilities { + if tech := techutils.Technology(strings.ToLower(vulnerability.Technology)); tech != "" { + technologiesSet.Add(tech) + } + } + for _, violation := range scaResult.Violations { + if tech := techutils.Technology(strings.ToLower(violation.Technology)); tech != "" { + technologiesSet.Add(tech) + } + } + } + return technologiesSet.ToSlice() +} + +func (sr *TargetResults) GetJasScansResults(scanType jasutils.JasScanType) (results []*sarif.Run) { + if sr.JasResults == nil { + return + } + return sr.JasResults.GetResults(scanType) +} + +func (sr *TargetResults) HasInformation() bool { + if sr.JasResults != nil && sr.JasResults.HasInformation() { + return true + } + if sr.ScaResults != nil && sr.ScaResults.HasInformation() { + return true + } + return false +} + +func (sr *TargetResults) HasFindings() bool { + if sr.JasResults != nil && sr.JasResults.HasFindings() { + return true + } + if sr.ScaResults != nil && sr.ScaResults.HasFindings() { + return true + } + return false +} + +func (sr *TargetResults) AddError(err error) { + sr.errorsMutex.Lock() + sr.Errors = append(sr.Errors, err) + sr.errorsMutex.Unlock() +} + +func (sr *TargetResults) SetDescriptors(descriptors ...string) *TargetResults { + if sr.ScaResults == nil { + sr.ScaResults = &ScaScanResults{} + } + sr.ScaResults.Descriptors = descriptors + return sr +} + +func (sr *TargetResults) NewScaScanResults(responses ...services.ScanResponse) *ScaScanResults { + if sr.ScaResults == nil { + sr.ScaResults = &ScaScanResults{} + } + sr.ScaResults.XrayResults = append(sr.ScaResults.XrayResults, responses...) + return sr.ScaResults +} + +func (ssr *ScaScanResults) HasInformation() bool { + if ssr.HasFindings() { + return true + } + for _, scanResults := range ssr.XrayResults { + if len(scanResults.Licenses) > 0 { + return true + } + } + return false +} + +func (ssr *ScaScanResults) HasFindings() bool { + for _, scanResults := range ssr.XrayResults { + if len(scanResults.Vulnerabilities) > 0 || len(scanResults.Violations) > 0 { + return true + } + } + return false +} + +func (jsr *JasScansResults) GetResults(scanType jasutils.JasScanType) (results []*sarif.Run) { + switch scanType { + case jasutils.Applicability: + results = jsr.ApplicabilityScanResults + case jasutils.Secrets: + results = jsr.SecretsScanResults + case jasutils.IaC: + results = jsr.IacScanResults + case jasutils.Sast: + results = jsr.SastScanResults + } + return +} + +func (jsr *JasScansResults) HasFindings() bool { + for _, scanType := range jasutils.GetJasScanTypes() { + if jsr.HasFindingsByType(scanType) { + return true + } + } + return false +} + +func (jsr *JasScansResults) HasFindingsByType(scanType jasutils.JasScanType) bool { + for _, run := range jsr.GetResults(scanType) { + for _, result := range run.Results { + if len(result.Locations) > 0 { + return true + } + } + } + return false +} + +func (jsr *JasScansResults) HasInformation() bool { + for _, scanType := range jasutils.GetJasScanTypes() { + if jsr.HasInformationByType(scanType) { + return true + } + } + return false +} + +func (jsr *JasScansResults) HasInformationByType(scanType jasutils.JasScanType) bool { + for _, run := range jsr.GetResults(scanType) { + if len(run.Results) > 0 { + return true + } + } + return false +} diff --git a/utils/results_test.go b/utils/results_test.go deleted file mode 100644 index c85e7132..00000000 --- a/utils/results_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package utils - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestGetScaScanResultByTarget(t *testing.T) { - target1 := &ScaScanResult{Target: "target1"} - target2 := &ScaScanResult{Target: "target2"} - testCases := []struct { - name string - results Results - target string - expected *ScaScanResult - }{ - { - name: "Sca scan result by target", - results: Results{ - ScaResults: []*ScaScanResult{ - target1, - target2, - }, - }, - target: "target1", - expected: target1, - }, - { - name: "Sca scan result by target not found", - results: Results{ - ScaResults: []*ScaScanResult{ - target1, - target2, - }, - }, - target: "target3", - expected: nil, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - result := testCase.results.getScaScanResultByTarget(testCase.target) - assert.Equal(t, testCase.expected, result) - }) - } -} diff --git a/utils/resultstable.go b/utils/resultstable.go deleted file mode 100644 index 6d25da70..00000000 --- a/utils/resultstable.go +++ /dev/null @@ -1,1123 +0,0 @@ -package utils - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - - "github.com/jfrog/gofrog/datastructures" - "github.com/owenrumney/go-sarif/v2/sarif" - - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" - "github.com/jfrog/jfrog-cli-security/utils/jasutils" - "github.com/jfrog/jfrog-cli-security/utils/severityutils" - "github.com/jfrog/jfrog-cli-security/utils/techutils" - - "github.com/gookit/color" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/jfrog/jfrog-client-go/xray/services" -) - -const ( - rootIndex = 0 - directDependencyIndex = 1 - directDependencyPathLength = 2 - nodeModules = "node_modules" - NpmPackageTypeIdentifier = "npm://" -) - -// PrintViolationsTable prints the violations in 4 tables: security violations, license compliance violations, operational risk violations and ignore rule URLs. -// Set multipleRoots to true in case the given violations array contains (or may contain) results of several projects or files (like in binary scan). -// In case multipleRoots is true, the field Component will show the root of each impact path, otherwise it will show the root's child. -// In case one (or more) of the violations contains the field FailBuild set to true, CliError with exit code 3 will be returned. -// Set printExtended to true to print fields with 'extended' tag. -// If the scan argument is set to true, print the scan tables. -func PrintViolationsTable(violations []services.Violation, results *Results, multipleRoots, printExtended bool) error { - securityViolationsRows, licenseViolationsRows, operationalRiskViolationsRows, err := prepareViolations(violations, results, multipleRoots, true, true) - if err != nil { - return err - } - // Print tables, if scan is true; print the scan tables. - if results.ResultType.IsTargetBinary() { - err = coreutils.PrintTable(formats.ConvertToVulnerabilityScanTableRow(securityViolationsRows), "Security Violations", "No security violations were found", printExtended) - if err != nil { - return err - } - err = coreutils.PrintTable(formats.ConvertToLicenseViolationScanTableRow(licenseViolationsRows), "License Compliance Violations", "No license compliance violations were found", printExtended) - if err != nil { - return err - } - if len(operationalRiskViolationsRows) > 0 { - return coreutils.PrintTable(formats.ConvertToOperationalRiskViolationScanTableRow(operationalRiskViolationsRows), "Operational Risk Violations", "No operational risk violations were found", printExtended) - } - } else { - err = coreutils.PrintTable(formats.ConvertToVulnerabilityTableRow(securityViolationsRows), "Security Violations", "No security violations were found", printExtended) - if err != nil { - return err - } - err = coreutils.PrintTable(formats.ConvertToLicenseViolationTableRow(licenseViolationsRows), "License Compliance Violations", "No license compliance violations were found", printExtended) - if err != nil { - return err - } - if len(operationalRiskViolationsRows) > 0 { - return coreutils.PrintTable(formats.ConvertToOperationalRiskViolationTableRow(operationalRiskViolationsRows), "Operational Risk Violations", "No operational risk violations were found", printExtended) - } - } - return nil -} - -// Prepare violations for all non-table formats (without style or emoji) -func PrepareViolations(violations []services.Violation, results *Results, multipleRoots, simplifiedOutput bool) ([]formats.VulnerabilityOrViolationRow, []formats.LicenseRow, []formats.OperationalRiskViolationRow, error) { - return prepareViolations(violations, results, multipleRoots, false, simplifiedOutput) -} - -func prepareViolations(violations []services.Violation, results *Results, multipleRoots, isTable, simplifiedOutput bool) ([]formats.VulnerabilityOrViolationRow, []formats.LicenseRow, []formats.OperationalRiskViolationRow, error) { - if simplifiedOutput { - violations = simplifyViolations(violations, multipleRoots) - } - var securityViolationsRows []formats.VulnerabilityOrViolationRow - var licenseViolationsRows []formats.LicenseRow - var operationalRiskViolationsRows []formats.OperationalRiskViolationRow - for _, violation := range violations { - impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes, fixedVersions, components, impactPaths, err := splitComponents(violation.Components) - if err != nil { - return nil, nil, nil, err - } - switch violation.ViolationType { - case ViolationTypeSecurity.String(): - cves := convertCves(violation.Cves) - if results.ExtendedScanResults.EntitledForJas { - for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i].Id, results.ExtendedScanResults.ApplicabilityScanResults, violation.Components) - } - } - applicabilityStatus := getApplicableCveStatus(results.ExtendedScanResults.EntitledForJas, results.ExtendedScanResults.ApplicabilityScanResults, cves) - currSeverity, err := severityutils.ParseSeverity(violation.Severity, false) - if err != nil { - return nil, nil, nil, err - } - jfrogResearchInfo := convertJfrogResearchInformation(violation.ExtendedInformation) - for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { - securityViolationsRows = append(securityViolationsRows, - formats.VulnerabilityOrViolationRow{ - Summary: violation.Summary, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(currSeverity, applicabilityStatus, isTable), - ImpactedDependencyName: impactedPackagesNames[compIndex], - ImpactedDependencyVersion: impactedPackagesVersions[compIndex], - ImpactedDependencyType: impactedPackagesTypes[compIndex], - Components: components[compIndex], - }, - FixedVersions: fixedVersions[compIndex], - Cves: cves, - IssueId: violation.IssueId, - References: violation.References, - JfrogResearchInformation: jfrogResearchInfo, - ImpactPaths: impactPaths[compIndex], - Technology: techutils.Technology(violation.Technology), - Applicable: printApplicabilityCveValue(applicabilityStatus, isTable), - }, - ) - } - case ViolationTypeLicense.String(): - currSeverity, err := severityutils.ParseSeverity(violation.Severity, false) - if err != nil { - return nil, nil, nil, err - } - for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { - licenseViolationsRows = append(licenseViolationsRows, - formats.LicenseRow{ - LicenseKey: violation.LicenseKey, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(currSeverity, jasutils.NotScanned, isTable), - ImpactedDependencyName: impactedPackagesNames[compIndex], - ImpactedDependencyVersion: impactedPackagesVersions[compIndex], - ImpactedDependencyType: impactedPackagesTypes[compIndex], - Components: components[compIndex], - }, - }, - ) - } - case ViolationTypeOperationalRisk.String(): - currSeverity, err := severityutils.ParseSeverity(violation.Severity, false) - if err != nil { - return nil, nil, nil, err - } - violationOpRiskData := getOperationalRiskViolationReadableData(violation) - for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { - operationalRiskViolationsRow := &formats.OperationalRiskViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(currSeverity, jasutils.NotScanned, isTable), - ImpactedDependencyName: impactedPackagesNames[compIndex], - ImpactedDependencyVersion: impactedPackagesVersions[compIndex], - ImpactedDependencyType: impactedPackagesTypes[compIndex], - Components: components[compIndex], - }, - IsEol: violationOpRiskData.isEol, - Cadence: violationOpRiskData.cadence, - Commits: violationOpRiskData.commits, - Committers: violationOpRiskData.committers, - NewerVersions: violationOpRiskData.newerVersions, - LatestVersion: violationOpRiskData.latestVersion, - RiskReason: violationOpRiskData.riskReason, - EolMessage: violationOpRiskData.eolMessage, - } - operationalRiskViolationsRows = append(operationalRiskViolationsRows, *operationalRiskViolationsRow) - } - default: - // Unsupported type, ignore - } - } - - // Sort the rows by severity and whether the row contains fixed versions - sortVulnerabilityOrViolationRows(securityViolationsRows) - sort.Slice(licenseViolationsRows, func(i, j int) bool { - return licenseViolationsRows[i].SeverityNumValue > licenseViolationsRows[j].SeverityNumValue - }) - sort.Slice(operationalRiskViolationsRows, func(i, j int) bool { - return operationalRiskViolationsRows[i].SeverityNumValue > operationalRiskViolationsRows[j].SeverityNumValue - }) - - return securityViolationsRows, licenseViolationsRows, operationalRiskViolationsRows, nil -} - -// PrintVulnerabilitiesTable prints the vulnerabilities in a table. -// Set multipleRoots to true in case the given vulnerabilities array contains (or may contain) results of several projects or files (like in binary scan). -// In case multipleRoots is true, the field Component will show the root of each impact path, otherwise it will show the root's child. -// Set printExtended to true to print fields with 'extended' tag. -// If the scan argument is set to true, print the scan tables. -func PrintVulnerabilitiesTable(vulnerabilities []services.Vulnerability, results *Results, multipleRoots, printExtended bool, scanType CommandType) error { - vulnerabilitiesRows, err := prepareVulnerabilities(vulnerabilities, results, multipleRoots, true, true) - if err != nil { - return err - } - - if scanType.IsTargetBinary() { - return coreutils.PrintTable(formats.ConvertToVulnerabilityScanTableRow(vulnerabilitiesRows), "Vulnerable Components", "✨ No vulnerable components were found ✨", printExtended) - } - var emptyTableMessage string - if len(results.ScaResults) > 0 { - emptyTableMessage = "✨ No vulnerable dependencies were found ✨" - } else { - emptyTableMessage = coreutils.PrintYellow("🔧 Couldn't determine a package manager or build tool used by this project 🔧") - } - return coreutils.PrintTable(formats.ConvertToVulnerabilityTableRow(vulnerabilitiesRows), "Vulnerable Dependencies", emptyTableMessage, printExtended) -} - -// Prepare vulnerabilities for all non-table formats (without style or emoji) -func PrepareVulnerabilities(vulnerabilities []services.Vulnerability, results *Results, multipleRoots, simplifiedOutput bool) ([]formats.VulnerabilityOrViolationRow, error) { - return prepareVulnerabilities(vulnerabilities, results, multipleRoots, false, simplifiedOutput) -} - -func prepareVulnerabilities(vulnerabilities []services.Vulnerability, results *Results, multipleRoots, isTable, simplifiedOutput bool) ([]formats.VulnerabilityOrViolationRow, error) { - if simplifiedOutput { - vulnerabilities = simplifyVulnerabilities(vulnerabilities, multipleRoots) - } - var vulnerabilitiesRows []formats.VulnerabilityOrViolationRow - for _, vulnerability := range vulnerabilities { - impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes, fixedVersions, components, impactPaths, err := splitComponents(vulnerability.Components) - if err != nil { - return nil, err - } - cves := convertCves(vulnerability.Cves) - if results.ExtendedScanResults.EntitledForJas { - for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i].Id, results.ExtendedScanResults.ApplicabilityScanResults, vulnerability.Components) - } - } - applicabilityStatus := getApplicableCveStatus(results.ExtendedScanResults.EntitledForJas, results.ExtendedScanResults.ApplicabilityScanResults, cves) - currSeverity, err := severityutils.ParseSeverity(vulnerability.Severity, false) - if err != nil { - return nil, err - } - jfrogResearchInfo := convertJfrogResearchInformation(vulnerability.ExtendedInformation) - for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { - vulnerabilitiesRows = append(vulnerabilitiesRows, - formats.VulnerabilityOrViolationRow{ - Summary: vulnerability.Summary, - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(currSeverity, applicabilityStatus, isTable), - ImpactedDependencyName: impactedPackagesNames[compIndex], - ImpactedDependencyVersion: impactedPackagesVersions[compIndex], - ImpactedDependencyType: impactedPackagesTypes[compIndex], - Components: components[compIndex], - }, - FixedVersions: fixedVersions[compIndex], - Cves: cves, - IssueId: vulnerability.IssueId, - References: vulnerability.References, - JfrogResearchInformation: jfrogResearchInfo, - ImpactPaths: impactPaths[compIndex], - Technology: techutils.Technology(vulnerability.Technology), - Applicable: printApplicabilityCveValue(applicabilityStatus, isTable), - }, - ) - } - } - - sortVulnerabilityOrViolationRows(vulnerabilitiesRows) - return vulnerabilitiesRows, nil -} - -// sortVulnerabilityOrViolationRows is sorting in the following order: -// Severity -> Applicability -> JFrog Research Score -> XRAY ID -func sortVulnerabilityOrViolationRows(rows []formats.VulnerabilityOrViolationRow) { - sort.Slice(rows, func(i, j int) bool { - if rows[i].SeverityNumValue != rows[j].SeverityNumValue { - return rows[i].SeverityNumValue > rows[j].SeverityNumValue - } - if rows[i].Applicable != rows[j].Applicable { - return jasutils.ConvertApplicableToScore(rows[i].Applicable) > jasutils.ConvertApplicableToScore(rows[j].Applicable) - } - priorityI := getJfrogResearchPriority(rows[i]) - priorityJ := getJfrogResearchPriority(rows[j]) - if priorityI != priorityJ { - return priorityI > priorityJ - } - return rows[i].IssueId > rows[j].IssueId - }) -} - -// getJfrogResearchPriority returns the score of JFrog Research Severity. -// If there is no such severity will return the normal severity score. -// When vulnerability with JFrog Reasearch to a vulnerability without we'll compare the JFrog Research Severity to the normal severity -func getJfrogResearchPriority(vulnerabilityOrViolation formats.VulnerabilityOrViolationRow) int { - if vulnerabilityOrViolation.JfrogResearchInformation == nil { - return vulnerabilityOrViolation.SeverityNumValue - } - - return vulnerabilityOrViolation.JfrogResearchInformation.SeverityNumValue -} - -// PrintLicensesTable prints the licenses in a table. -// Set multipleRoots to true in case the given licenses array contains (or may contain) results of several projects or files (like in binary scan). -// In case multipleRoots is true, the field Component will show the root of each impact path, otherwise it will show the root's child. -// Set printExtended to true to print fields with 'extended' tag. -// If the scan argument is set to true, print the scan tables. -func PrintLicensesTable(licenses []services.License, printExtended bool, scanType CommandType) error { - licensesRows, err := PrepareLicenses(licenses) - if err != nil { - return err - } - if scanType.IsTargetBinary() { - return coreutils.PrintTable(formats.ConvertToLicenseScanTableRow(licensesRows), "Licenses", "No licenses were found", printExtended) - } - return coreutils.PrintTable(formats.ConvertToLicenseTableRow(licensesRows), "Licenses", "No licenses were found", printExtended) -} - -func PrepareLicenses(licenses []services.License) ([]formats.LicenseRow, error) { - var licensesRows []formats.LicenseRow - for _, license := range licenses { - impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes, _, components, impactPaths, err := splitComponents(license.Components) - if err != nil { - return nil, err - } - for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { - licensesRows = append(licensesRows, - formats.LicenseRow{ - LicenseKey: license.Key, - ImpactPaths: impactPaths[compIndex], - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: impactedPackagesNames[compIndex], - ImpactedDependencyVersion: impactedPackagesVersions[compIndex], - ImpactedDependencyType: impactedPackagesTypes[compIndex], - Components: components[compIndex], - }, - }, - ) - } - } - - return licensesRows, nil -} - -// Prepare secrets for all non-table formats (without style or emoji) -func PrepareSecrets(secrets []*sarif.Run) []formats.SourceCodeRow { - return prepareSecrets(secrets, false) -} - -func prepareSecrets(secrets []*sarif.Run, isTable bool) []formats.SourceCodeRow { - var secretsRows []formats.SourceCodeRow - for _, secretRun := range secrets { - for _, secretResult := range secretRun.Results { - currSeverity, err := severityutils.ParseSeverity(sarifutils.GetResultLevel(secretResult), true) - if err != nil { - log.Warn(fmt.Sprintf("Failed to parse severity `%s` for secret result: %s", sarifutils.GetResultLevel(secretResult), *secretResult.RuleID)) - currSeverity = severityutils.Unknown - } - for _, location := range secretResult.Locations { - var applicability *formats.Applicability - status := GetResultPropertyTokenValidation(secretResult) - statusDescription := GetResultPropertyMetadata(secretResult) - if status != "" || statusDescription != "" { - applicability = &formats.Applicability{Status: status, - ScannerDescription: statusDescription} - } - secretsRows = append(secretsRows, - formats.SourceCodeRow{ - SeverityDetails: severityutils.GetAsDetails(currSeverity, jasutils.Applicable, isTable), - Finding: sarifutils.GetResultMsgText(secretResult), - Fingerprint: sarifutils.GetResultFingerprint(secretResult), - Location: formats.Location{ - File: sarifutils.GetRelativeLocationFileName(location, secretRun.Invocations), - StartLine: sarifutils.GetLocationStartLine(location), - StartColumn: sarifutils.GetLocationStartColumn(location), - EndLine: sarifutils.GetLocationEndLine(location), - EndColumn: sarifutils.GetLocationEndColumn(location), - Snippet: sarifutils.GetLocationSnippet(location), - }, - Applicability: applicability, - }, - ) - } - } - } - - sort.Slice(secretsRows, func(i, j int) bool { - if secretsRows[i].SeverityNumValue != secretsRows[j].SeverityNumValue { - return secretsRows[i].SeverityNumValue > secretsRows[j].SeverityNumValue - } - if secretsRows[i].Applicability != nil && secretsRows[j].Applicability != nil { - return jasutils.TokenValidationOrder[secretsRows[i].Applicability.Status] < jasutils.TokenValidationOrder[secretsRows[j].Applicability.Status] - } - return true - }) - - return secretsRows -} - -func PrintSecretsTable(secrets []*sarif.Run, entitledForSecretsScan bool, tokenValidationEnabled bool) error { - if entitledForSecretsScan { - secretsRows := prepareSecrets(secrets, true) - log.Output() - err := coreutils.PrintTable(formats.ConvertToSecretsTableRow(secretsRows), "Secret Detection", - "✨ No secrets were found ✨", false) - if err == nil && tokenValidationEnabled { - log.Output("This table contains multiple secret types, such as tokens, generic password, ssh keys and more, token validation is only supported on tokens.") - } - return err - } - return nil -} - -// Prepare iacs for all non-table formats (without style or emoji) -func PrepareIacs(iacs []*sarif.Run) []formats.SourceCodeRow { - return prepareIacs(iacs, false) -} - -func prepareIacs(iacs []*sarif.Run, isTable bool) []formats.SourceCodeRow { - var iacRows []formats.SourceCodeRow - for _, iacRun := range iacs { - for _, iacResult := range iacRun.Results { - scannerDescription := "" - if rule, err := iacRun.GetRuleById(*iacResult.RuleID); err == nil { - scannerDescription = sarifutils.GetRuleFullDescriptionText(rule) - } - currSeverity, err := severityutils.ParseSeverity(sarifutils.GetResultLevel(iacResult), true) - if err != nil { - log.Warn(fmt.Sprintf("Failed to parse severity `%s` for iac result: %s", sarifutils.GetResultLevel(iacResult), *iacResult.RuleID)) - currSeverity = severityutils.Unknown - } - for _, location := range iacResult.Locations { - iacRows = append(iacRows, - formats.SourceCodeRow{ - SeverityDetails: severityutils.GetAsDetails(currSeverity, jasutils.Applicable, isTable), - Finding: sarifutils.GetResultMsgText(iacResult), - Fingerprint: sarifutils.GetResultFingerprint(iacResult), - ScannerDescription: scannerDescription, - Location: formats.Location{ - File: sarifutils.GetRelativeLocationFileName(location, iacRun.Invocations), - StartLine: sarifutils.GetLocationStartLine(location), - StartColumn: sarifutils.GetLocationStartColumn(location), - EndLine: sarifutils.GetLocationEndLine(location), - EndColumn: sarifutils.GetLocationEndColumn(location), - Snippet: sarifutils.GetLocationSnippet(location), - }, - }, - ) - } - } - } - - sort.Slice(iacRows, func(i, j int) bool { - return iacRows[i].SeverityNumValue > iacRows[j].SeverityNumValue - }) - - return iacRows -} - -func PrintIacTable(iacs []*sarif.Run, entitledForIacScan bool) error { - if entitledForIacScan { - iacRows := prepareIacs(iacs, true) - log.Output() - return coreutils.PrintTable(formats.ConvertToIacOrSastTableRow(iacRows), "Infrastructure as Code Vulnerabilities", - "✨ No Infrastructure as Code vulnerabilities were found ✨", false) - } - return nil -} - -func PrepareSast(sasts []*sarif.Run) []formats.SourceCodeRow { - return prepareSast(sasts, false) -} - -func prepareSast(sasts []*sarif.Run, isTable bool) []formats.SourceCodeRow { - var sastRows []formats.SourceCodeRow - for _, sastRun := range sasts { - for _, sastResult := range sastRun.Results { - scannerDescription := "" - if rule, err := sastRun.GetRuleById(*sastResult.RuleID); err == nil { - scannerDescription = sarifutils.GetRuleFullDescriptionText(rule) - } - currSeverity, err := severityutils.ParseSeverity(sarifutils.GetResultLevel(sastResult), true) - if err != nil { - log.Warn(fmt.Sprintf("Failed to parse severity `%s` for sast result: %s", sarifutils.GetResultLevel(sastResult), *sastResult.RuleID)) - currSeverity = severityutils.Unknown - } - for _, location := range sastResult.Locations { - codeFlows := sarifutils.GetLocationRelatedCodeFlowsFromResult(location, sastResult) - sastRows = append(sastRows, - formats.SourceCodeRow{ - SeverityDetails: severityutils.GetAsDetails(currSeverity, jasutils.Applicable, isTable), - ScannerDescription: scannerDescription, - Finding: sarifutils.GetResultMsgText(sastResult), - Fingerprint: sarifutils.GetResultFingerprint(sastResult), - Location: formats.Location{ - File: sarifutils.GetRelativeLocationFileName(location, sastRun.Invocations), - StartLine: sarifutils.GetLocationStartLine(location), - StartColumn: sarifutils.GetLocationStartColumn(location), - EndLine: sarifutils.GetLocationEndLine(location), - EndColumn: sarifutils.GetLocationEndColumn(location), - Snippet: sarifutils.GetLocationSnippet(location), - }, - CodeFlow: codeFlowToLocationFlow(codeFlows, sastRun.Invocations, isTable), - }, - ) - } - } - } - - sort.Slice(sastRows, func(i, j int) bool { - return sastRows[i].SeverityNumValue > sastRows[j].SeverityNumValue - }) - - return sastRows -} - -func codeFlowToLocationFlow(flows []*sarif.CodeFlow, invocations []*sarif.Invocation, isTable bool) (flowRows [][]formats.Location) { - if isTable { - // Not displaying in table - return - } - for _, codeFlow := range flows { - for _, stackTrace := range codeFlow.ThreadFlows { - rowFlow := []formats.Location{} - for _, stackTraceEntry := range stackTrace.Locations { - rowFlow = append(rowFlow, formats.Location{ - File: sarifutils.GetRelativeLocationFileName(stackTraceEntry.Location, invocations), - StartLine: sarifutils.GetLocationStartLine(stackTraceEntry.Location), - StartColumn: sarifutils.GetLocationStartColumn(stackTraceEntry.Location), - EndLine: sarifutils.GetLocationEndLine(stackTraceEntry.Location), - EndColumn: sarifutils.GetLocationEndColumn(stackTraceEntry.Location), - Snippet: sarifutils.GetLocationSnippet(stackTraceEntry.Location), - }) - } - flowRows = append(flowRows, rowFlow) - } - } - return -} - -func PrintSastTable(sast []*sarif.Run, entitledForSastScan bool) error { - if entitledForSastScan { - sastRows := prepareSast(sast, true) - log.Output() - return coreutils.PrintTable(formats.ConvertToIacOrSastTableRow(sastRows), "Static Application Security Testing (SAST)", - "✨ No Static Application Security Testing vulnerabilities were found ✨", false) - } - return nil -} - -func convertJfrogResearchInformation(extendedInfo *services.ExtendedInformation) *formats.JfrogResearchInformation { - if extendedInfo == nil { - return nil - } - var severityReasons []formats.JfrogResearchSeverityReason - for _, severityReason := range extendedInfo.JfrogResearchSeverityReasons { - severityReasons = append(severityReasons, formats.JfrogResearchSeverityReason{ - Name: severityReason.Name, - Description: severityReason.Description, - IsPositive: severityReason.IsPositive, - }) - } - return &formats.JfrogResearchInformation{ - Summary: extendedInfo.ShortDescription, - Details: extendedInfo.FullDescription, - SeverityDetails: formats.SeverityDetails{Severity: extendedInfo.JfrogResearchSeverity}, - SeverityReasons: severityReasons, - Remediation: extendedInfo.Remediation, - } -} - -func splitComponents(impactedPackages map[string]services.Component) (impactedPackagesNames, impactedPackagesVersions, impactedPackagesTypes []string, fixedVersions [][]string, directComponents [][]formats.ComponentRow, impactPaths [][][]formats.ComponentRow, err error) { - if len(impactedPackages) == 0 { - err = errorutils.CheckErrorf("failed while parsing the response from Xray: violation doesn't have any components") - return - } - for currCompId, currComp := range impactedPackages { - currCompName, currCompVersion, currCompType := SplitComponentId(currCompId) - impactedPackagesNames = append(impactedPackagesNames, currCompName) - impactedPackagesVersions = append(impactedPackagesVersions, currCompVersion) - impactedPackagesTypes = append(impactedPackagesTypes, currCompType) - fixedVersions = append(fixedVersions, currComp.FixedVersions) - currDirectComponents, currImpactPaths := getDirectComponentsAndImpactPaths(currComp.ImpactPaths) - directComponents = append(directComponents, currDirectComponents) - impactPaths = append(impactPaths, currImpactPaths) - } - return -} - -var packageTypes = map[string]string{ - "gav": "Maven", - "docker": "Docker", - "rpm": "RPM", - "deb": "Debian", - "nuget": "NuGet", - "generic": "Generic", - "npm": "npm", - "pip": "Python", - "pypi": "Python", - "composer": "Composer", - "go": "Go", - "alpine": "Alpine", -} - -// SplitComponentId splits a Xray component ID to the component name, version and package type. -// In case componentId doesn't contain a version, the returned version will be an empty string. -// In case componentId's format is invalid, it will be returned as the component name -// and empty strings will be returned instead of the version and the package type. -// Examples: -// 1. componentId: "gav://antparent:ant:1.6.5" -// Returned values: -// Component name: "antparent:ant" -// Component version: "1.6.5" -// Package type: "Maven" -// 2. componentId: "generic://sha256:244fd47e07d1004f0aed9c156aa09083c82bf8944eceb67c946ff7430510a77b/foo.jar" -// Returned values: -// Component name: "foo.jar" -// Component version: "" -// Package type: "Generic" -// 3. componentId: "invalid-comp-id" -// Returned values: -// Component name: "invalid-comp-id" -// Component version: "" -// Package type: "" -func SplitComponentId(componentId string) (string, string, string) { - compIdParts := strings.Split(componentId, "://") - // Invalid component ID - if len(compIdParts) != 2 { - return componentId, "", "" - } - - packageType := compIdParts[0] - packageId := compIdParts[1] - - // Generic identifier structure: generic://sha256:/name - if packageType == "generic" { - lastSlashIndex := strings.LastIndex(packageId, "/") - return packageId[lastSlashIndex+1:], "", packageTypes[packageType] - } - - var compName, compVersion string - switch packageType { - case "rpm": - // RPM identifier structure: rpm://os-version:package:epoch-version:version - // os-version is optional. - splitCompId := strings.Split(packageId, ":") - if len(splitCompId) >= 3 { - compName = splitCompId[len(splitCompId)-3] - compVersion = fmt.Sprintf("%s:%s", splitCompId[len(splitCompId)-2], splitCompId[len(splitCompId)-1]) - } - default: - // All other identifiers look like this: package-type://package-name:version. - // Sometimes there's a namespace or a group before the package name, separated by a '/' or a ':'. - lastColonIndex := strings.LastIndex(packageId, ":") - - if lastColonIndex != -1 { - compName = packageId[:lastColonIndex] - compVersion = packageId[lastColonIndex+1:] - } - } - - // If there's an error while parsing the component ID - if compName == "" { - compName = packageId - } - - return compName, compVersion, packageTypes[packageType] -} - -// Gets a slice of the direct dependencies or packages of the scanned component, that depends on the vulnerable package, and converts the impact paths. -func getDirectComponentsAndImpactPaths(impactPaths [][]services.ImpactPathNode) (components []formats.ComponentRow, impactPathsRows [][]formats.ComponentRow) { - componentsMap := make(map[string]formats.ComponentRow) - - // The first node in the impact path is the scanned component itself. The second one is the direct dependency. - impactPathLevel := 1 - for _, impactPath := range impactPaths { - impactPathIndex := impactPathLevel - if len(impactPath) <= impactPathLevel { - impactPathIndex = len(impactPath) - 1 - } - componentId := impactPath[impactPathIndex].ComponentId - if _, exist := componentsMap[componentId]; !exist { - compName, compVersion, _ := SplitComponentId(componentId) - componentsMap[componentId] = formats.ComponentRow{Name: compName, Version: compVersion} - } - - // Convert the impact path - var compImpactPathRows []formats.ComponentRow - for _, pathNode := range impactPath { - nodeCompName, nodeCompVersion, _ := SplitComponentId(pathNode.ComponentId) - compImpactPathRows = append(compImpactPathRows, formats.ComponentRow{ - Name: nodeCompName, - Version: nodeCompVersion, - }) - } - impactPathsRows = append(impactPathsRows, compImpactPathRows) - } - - for _, row := range componentsMap { - components = append(components, row) - } - return -} - -type operationalRiskViolationReadableData struct { - isEol string - cadence string - commits string - committers string - eolMessage string - riskReason string - latestVersion string - newerVersions string -} - -func getOperationalRiskViolationReadableData(violation services.Violation) *operationalRiskViolationReadableData { - isEol, cadence, commits, committers, newerVersions, latestVersion := "N/A", "N/A", "N/A", "N/A", "N/A", "N/A" - if violation.IsEol != nil { - isEol = strconv.FormatBool(*violation.IsEol) - } - if violation.Cadence != nil { - cadence = strconv.FormatFloat(*violation.Cadence, 'f', -1, 64) - } - if violation.Committers != nil { - committers = strconv.FormatInt(int64(*violation.Committers), 10) - } - if violation.Commits != nil { - commits = strconv.FormatInt(*violation.Commits, 10) - } - if violation.NewerVersions != nil { - newerVersions = strconv.FormatInt(int64(*violation.NewerVersions), 10) - } - if violation.LatestVersion != "" { - latestVersion = violation.LatestVersion - } - return &operationalRiskViolationReadableData{ - isEol: isEol, - cadence: cadence, - commits: commits, - committers: committers, - eolMessage: violation.EolMessage, - riskReason: violation.RiskReason, - latestVersion: latestVersion, - newerVersions: newerVersions, - } -} - -// simplifyVulnerabilities returns a new slice of services.Vulnerability that contains only the unique vulnerabilities from the input slice -// The uniqueness of the vulnerabilities is determined by the GetUniqueKey function -func simplifyVulnerabilities(scanVulnerabilities []services.Vulnerability, multipleRoots bool) []services.Vulnerability { - var uniqueVulnerabilities = make(map[string]*services.Vulnerability) - for _, vulnerability := range scanVulnerabilities { - for vulnerableComponentId := range vulnerability.Components { - vulnerableDependency, vulnerableVersion, _ := SplitComponentId(vulnerableComponentId) - packageKey := GetUniqueKey(vulnerableDependency, vulnerableVersion, vulnerability.IssueId, len(vulnerability.Components[vulnerableComponentId].FixedVersions) > 0) - if uniqueVulnerability, exist := uniqueVulnerabilities[packageKey]; exist { - fixedVersions := appendUniqueFixVersions(uniqueVulnerability.Components[vulnerableComponentId].FixedVersions, vulnerability.Components[vulnerableComponentId].FixedVersions...) - impactPaths := appendUniqueImpactPaths(uniqueVulnerability.Components[vulnerableComponentId].ImpactPaths, vulnerability.Components[vulnerableComponentId].ImpactPaths, multipleRoots) - uniqueVulnerabilities[packageKey].Components[vulnerableComponentId] = services.Component{ - FixedVersions: fixedVersions, - ImpactPaths: impactPaths, - } - continue - } - uniqueVulnerabilities[packageKey] = &services.Vulnerability{ - Cves: vulnerability.Cves, - Severity: vulnerability.Severity, - Components: map[string]services.Component{vulnerableComponentId: vulnerability.Components[vulnerableComponentId]}, - IssueId: vulnerability.IssueId, - Technology: vulnerability.Technology, - ExtendedInformation: vulnerability.ExtendedInformation, - Summary: vulnerability.Summary, - } - } - } - // convert map to slice - result := make([]services.Vulnerability, 0, len(uniqueVulnerabilities)) - for _, v := range uniqueVulnerabilities { - result = append(result, *v) - } - return result -} - -// simplifyViolations returns a new slice of services.Violations that contains only the unique violations from the input slice -// The uniqueness of the violations is determined by the GetUniqueKey function -func simplifyViolations(scanViolations []services.Violation, multipleRoots bool) []services.Violation { - var uniqueViolations = make(map[string]*services.Violation) - for _, violation := range scanViolations { - for vulnerableComponentId := range violation.Components { - vulnerableDependency, vulnerableVersion, _ := SplitComponentId(vulnerableComponentId) - packageKey := GetUniqueKey(vulnerableDependency, vulnerableVersion, violation.IssueId, len(violation.Components[vulnerableComponentId].FixedVersions) > 0) - if uniqueVulnerability, exist := uniqueViolations[packageKey]; exist { - fixedVersions := appendUniqueFixVersions(uniqueVulnerability.Components[vulnerableComponentId].FixedVersions, violation.Components[vulnerableComponentId].FixedVersions...) - impactPaths := appendUniqueImpactPaths(uniqueVulnerability.Components[vulnerableComponentId].ImpactPaths, violation.Components[vulnerableComponentId].ImpactPaths, multipleRoots) - uniqueViolations[packageKey].Components[vulnerableComponentId] = services.Component{ - FixedVersions: fixedVersions, - ImpactPaths: impactPaths, - } - continue - } - uniqueViolations[packageKey] = &services.Violation{ - Summary: violation.Summary, - Severity: violation.Severity, - ViolationType: violation.ViolationType, - Components: map[string]services.Component{vulnerableComponentId: violation.Components[vulnerableComponentId]}, - WatchName: violation.WatchName, - IssueId: violation.IssueId, - Cves: violation.Cves, - LicenseKey: violation.LicenseKey, - LicenseName: violation.LicenseName, - RiskReason: violation.RiskReason, - IsEol: violation.IsEol, - EolMessage: violation.EolMessage, - LatestVersion: violation.LatestVersion, - NewerVersions: violation.NewerVersions, - Cadence: violation.Cadence, - Commits: violation.Commits, - Committers: violation.Committers, - ExtendedInformation: violation.ExtendedInformation, - Technology: violation.Technology, - } - } - } - // convert map to slice - result := make([]services.Violation, 0, len(uniqueViolations)) - for _, v := range uniqueViolations { - result = append(result, *v) - } - return result -} - -// appendImpactPathsWithoutDuplicates appends the elements of a source [][]ImpactPathNode struct to a target [][]ImpactPathNode, without adding any duplicate elements. -// This implementation uses the ComponentId field of the ImpactPathNode struct to check for duplicates, as it is guaranteed to be unique. -func appendUniqueImpactPaths(target [][]services.ImpactPathNode, source [][]services.ImpactPathNode, multipleRoots bool) [][]services.ImpactPathNode { - if multipleRoots { - return appendUniqueImpactPathsForMultipleRoots(target, source) - } - impactPathMap := make(map[string][]services.ImpactPathNode) - for _, path := range target { - // The first node component id is the key and the value is the whole path - key := getImpactPathKey(path) - impactPathMap[key] = path - } - - for _, path := range source { - key := getImpactPathKey(path) - if _, exists := impactPathMap[key]; !exists { - impactPathMap[key] = path - target = append(target, path) - } - } - return target -} - -// getImpactPathKey return a key that is used as a key to identify and deduplicate impact paths. -// If an impact path length is equal to directDependencyPathLength, then the direct dependency is the key, and it's in the directDependencyIndex place. -func getImpactPathKey(path []services.ImpactPathNode) string { - key := path[rootIndex].ComponentId - if len(path) == directDependencyPathLength { - key = path[directDependencyIndex].ComponentId - } - return key -} - -// appendUniqueImpactPathsForMultipleRoots appends the source impact path to the target impact path while avoiding duplicates. -// Specifically, it is designed for handling multiple root projects, such as Maven or Gradle, by comparing each pair of paths and identifying the path that is closest to the direct dependency. -func appendUniqueImpactPathsForMultipleRoots(target [][]services.ImpactPathNode, source [][]services.ImpactPathNode) [][]services.ImpactPathNode { - for targetPathIndex, targetPath := range target { - for sourcePathIndex, sourcePath := range source { - var subset []services.ImpactPathNode - if len(sourcePath) <= len(targetPath) { - subset = isImpactPathIsSubset(targetPath, sourcePath) - if len(subset) != 0 { - target[targetPathIndex] = subset - } - } else { - subset = isImpactPathIsSubset(sourcePath, targetPath) - if len(subset) != 0 { - source[sourcePathIndex] = subset - } - } - } - } - - return appendUniqueImpactPaths(target, source, false) -} - -// isImpactPathIsSubset checks if targetPath is a subset of sourcePath, and returns the subset if exists -func isImpactPathIsSubset(target []services.ImpactPathNode, source []services.ImpactPathNode) []services.ImpactPathNode { - var subsetImpactPath []services.ImpactPathNode - impactPathNodesMap := make(map[string]bool) - for _, node := range target { - impactPathNodesMap[node.ComponentId] = true - } - - for _, node := range source { - if impactPathNodesMap[node.ComponentId] { - subsetImpactPath = append(subsetImpactPath, node) - } - } - - if len(subsetImpactPath) == len(target) || len(subsetImpactPath) == len(source) { - return subsetImpactPath - } - return []services.ImpactPathNode{} -} - -// appendUniqueFixVersions returns a new slice of strings that contains elements from both input slices without duplicates -func appendUniqueFixVersions(targetFixVersions []string, sourceFixVersions ...string) []string { - fixVersionsSet := datastructures.MakeSet[string]() - var result []string - for _, fixVersion := range sourceFixVersions { - fixVersionsSet.Add(fixVersion) - result = append(result, fixVersion) - } - - for _, fixVersion := range targetFixVersions { - if exist := fixVersionsSet.Exists(fixVersion); !exist { - result = append(result, fixVersion) - } - } - return result -} - -// GetUniqueKey returns a unique string key of format "vulnerableDependency:vulnerableVersion:xrayID:fixVersionExist" -func GetUniqueKey(vulnerableDependency, vulnerableVersion, xrayID string, fixVersionExist bool) string { - return strings.Join([]string{vulnerableDependency, vulnerableVersion, xrayID, strconv.FormatBool(fixVersionExist)}, ":") -} - -func convertCves(cves []services.Cve) []formats.CveRow { - var cveRows []formats.CveRow - for _, cveObj := range cves { - cveRows = append(cveRows, formats.CveRow{Id: cveObj.Id, CvssV2: cveObj.CvssV2Score, CvssV3: cveObj.CvssV3Score}) - } - return cveRows -} - -func getApplicableCveStatus(entitledForJas bool, applicabilityScanResults []*sarif.Run, cves []formats.CveRow) jasutils.ApplicabilityStatus { - if !entitledForJas || len(applicabilityScanResults) == 0 { - return jasutils.NotScanned - } - if len(cves) == 0 { - return jasutils.NotCovered - } - var applicableStatuses []jasutils.ApplicabilityStatus - for _, cve := range cves { - if cve.Applicability != nil { - applicableStatuses = append(applicableStatuses, jasutils.ApplicabilityStatus(cve.Applicability.Status)) - } - } - return getFinalApplicabilityStatus(applicableStatuses) -} - -func getCveApplicabilityField(cveId string, applicabilityScanResults []*sarif.Run, components map[string]services.Component) *formats.Applicability { - if len(applicabilityScanResults) == 0 { - return nil - } - - applicability := formats.Applicability{} - resultFound := false - var applicabilityStatuses []jasutils.ApplicabilityStatus - for _, applicabilityRun := range applicabilityScanResults { - if rule, _ := applicabilityRun.GetRuleById(jasutils.CveToApplicabilityRuleId(cveId)); rule != nil { - applicability.ScannerDescription = sarifutils.GetRuleFullDescriptionText(rule) - status := getApplicabilityStatusFromRule(rule) - applicability.UndeterminedReason = GetRuleUndeterminedReason(rule) - if status != "" { - applicabilityStatuses = append(applicabilityStatuses, status) - } - } - result, _ := applicabilityRun.GetResultByRuleId(jasutils.CveToApplicabilityRuleId(cveId)) - if result == nil { - continue - } - resultFound = true - // Add new evidences from locations - for _, location := range result.Locations { - fileName := sarifutils.GetRelativeLocationFileName(location, applicabilityRun.Invocations) - if shouldDisqualifyEvidence(components, fileName) { - continue - } - applicability.Evidence = append(applicability.Evidence, formats.Evidence{ - Location: formats.Location{ - File: fileName, - StartLine: sarifutils.GetLocationStartLine(location), - StartColumn: sarifutils.GetLocationStartColumn(location), - EndLine: sarifutils.GetLocationEndLine(location), - EndColumn: sarifutils.GetLocationEndColumn(location), - Snippet: sarifutils.GetLocationSnippet(location), - }, - Reason: sarifutils.GetResultMsgText(result), - }) - } - } - switch { - case len(applicabilityStatuses) > 0: - applicability.Status = getFinalApplicabilityStatus(applicabilityStatuses).String() - case !resultFound: - applicability.Status = jasutils.ApplicabilityUndetermined.String() - case len(applicability.Evidence) == 0: - applicability.Status = jasutils.NotApplicable.String() - default: - applicability.Status = jasutils.Applicable.String() - } - return &applicability -} - -func printApplicabilityCveValue(applicabilityStatus jasutils.ApplicabilityStatus, isTable bool) string { - if isTable && (log.IsStdOutTerminal() && log.IsColorsSupported() || os.Getenv("GITLAB_CI") != "") { - if applicabilityStatus == jasutils.Applicable { - return color.New(color.Red).Render(applicabilityStatus) - } else if applicabilityStatus == jasutils.NotApplicable { - return color.New(color.Green).Render(applicabilityStatus) - } - } - return applicabilityStatus.String() -} - -// Relevant only when "third-party-contextual-analysis" flag is on, -// which mean we scan the environment folders as well (node_modules for example...) -// When a certain package is reported applicable, and the evidence found -// is inside the source code of the same package, we should disqualify it. -// -// For example, -// Cve applicability was found inside the 'mquery' package. -// filePath = myProject/node_modules/mquery/badCode.js , disqualify = True. -// Disqualify the above evidence, as the reported applicability is used inside its own package. -// -// filePath = myProject/node_modules/mpath/badCode.js , disqualify = False. -// Found use of a badCode inside the node_modules from a different package, report applicable. -func shouldDisqualifyEvidence(components map[string]services.Component, evidenceFilePath string) (disqualify bool) { - for key := range components { - if !strings.HasPrefix(key, NpmPackageTypeIdentifier) { - return - } - dependencyName := extractDependencyNameFromComponent(key, NpmPackageTypeIdentifier) - // Check both Unix & Windows paths. - if strings.Contains(evidenceFilePath, nodeModules+"/"+dependencyName) || strings.Contains(evidenceFilePath, filepath.Join(nodeModules, dependencyName)) { - return true - } - } - return -} - -func extractDependencyNameFromComponent(key string, techIdentifier string) (dependencyName string) { - packageAndVersion := strings.TrimPrefix(key, techIdentifier) - split := strings.Split(packageAndVersion, ":") - if len(split) < 2 { - return - } - dependencyName = split[0] - return -} - -func GetRuleUndeterminedReason(rule *sarif.ReportingDescriptor) string { - return sarifutils.GetRuleProperty("undetermined_reason", rule) -} - -func GetResultPropertyTokenValidation(result *sarif.Result) string { - return sarifutils.GetResultProperty("tokenValidation", result) -} - -func GetResultPropertyMetadata(result *sarif.Result) string { - return sarifutils.GetResultProperty("metadata", result) -} - -func getApplicabilityStatusFromRule(rule *sarif.ReportingDescriptor) jasutils.ApplicabilityStatus { - if rule.Properties["applicability"] != nil { - status, ok := rule.Properties["applicability"].(string) - if !ok { - log.Debug(fmt.Sprintf("Failed to get applicability status from rule properties for rule_id %s", rule.ID)) - } - switch status { - case "not_covered": - return jasutils.NotCovered - case "undetermined": - return jasutils.ApplicabilityUndetermined - case "not_applicable": - return jasutils.NotApplicable - case "applicable": - return jasutils.Applicable - case "missing_context": - return jasutils.MissingContext - } - } - return "" -} - -// If we don't get any statues it means the applicability scanner didn't run -> final value is not scanned -// If at least one cve is applicable -> final value is applicable -// Else if at least one cve is undetermined -> final value is undetermined -// Else if at least one cve is missing context -> final value is missing context -// Else if all cves are not covered -> final value is not covered -// Else (case when all cves aren't applicable) -> final value is not applicable -func getFinalApplicabilityStatus(applicabilityStatuses []jasutils.ApplicabilityStatus) jasutils.ApplicabilityStatus { - if len(applicabilityStatuses) == 0 { - return jasutils.NotScanned - } - foundUndetermined := false - foundMissingContext := false - foundNotCovered := false - for _, status := range applicabilityStatuses { - if status == jasutils.Applicable { - return jasutils.Applicable - } - if status == jasutils.ApplicabilityUndetermined { - foundUndetermined = true - } - if status == jasutils.MissingContext { - foundMissingContext = true - } - if status == jasutils.NotCovered { - foundNotCovered = true - } - - } - if foundUndetermined { - return jasutils.ApplicabilityUndetermined - } - if foundMissingContext { - return jasutils.MissingContext - } - if foundNotCovered { - return jasutils.NotCovered - } - - return jasutils.NotApplicable -} diff --git a/utils/resultstable_test.go b/utils/resultstable_test.go deleted file mode 100644 index 6c300cea..00000000 --- a/utils/resultstable_test.go +++ /dev/null @@ -1,1417 +0,0 @@ -package utils - -import ( - "fmt" - "sort" - "testing" - - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" - "github.com/jfrog/jfrog-cli-security/utils/jasutils" - "github.com/owenrumney/go-sarif/v2/sarif" - - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/stretchr/testify/assert" -) - -// The test only checks cases of returning an error in case of a violation with FailBuild == true -func TestPrintViolationsTable(t *testing.T) { - components := map[string]services.Component{"gav://antparent:ant:1.6.5": {}} - tests := []struct { - violations []services.Violation - expectedError bool - }{ - {[]services.Violation{{Components: components, FailBuild: false}, {Components: components, FailBuild: false}, {Components: components, FailBuild: false}}, false}, - {[]services.Violation{{Components: components, FailBuild: false}, {Components: components, FailBuild: true}, {Components: components, FailBuild: false}}, true}, - {[]services.Violation{{Components: components, FailBuild: true}, {Components: components, FailBuild: true}, {Components: components, FailBuild: true}}, true}, - } - - for _, test := range tests { - err := PrintViolationsTable(test.violations, NewAuditResults(Binary), false, true) - assert.NoError(t, err) - if CheckIfFailBuild([]services.ScanResponse{{Violations: test.violations}}) { - err = NewFailBuildError() - } - assert.Equal(t, test.expectedError, err != nil) - } -} - -func TestSplitComponentId(t *testing.T) { - tests := []struct { - componentId string - expectedCompName string - expectedCompVersion string - expectedCompType string - }{ - {"gav://antparent:ant:1.6.5", "antparent:ant", "1.6.5", "Maven"}, - {"docker://jfrog/artifactory-oss:latest", "jfrog/artifactory-oss", "latest", "Docker"}, - {"rpm://7:rpm-python:7:4.11.3-43.el7", "rpm-python", "7:4.11.3-43.el7", "RPM"}, - {"rpm://rpm-python:7:4.11.3-43.el7", "rpm-python", "7:4.11.3-43.el7", "RPM"}, - {"deb://ubuntu:trustee:acl:2.2.49-2", "ubuntu:trustee:acl", "2.2.49-2", "Debian"}, - {"nuget://log4net:9.0.1", "log4net", "9.0.1", "NuGet"}, - {"generic://sha256:244fd47e07d1004f0aed9c156aa09083c82bf8944eceb67c946ff7430510a77b/foo.jar", "foo.jar", "", "Generic"}, - {"npm://mocha:2.4.5", "mocha", "2.4.5", "npm"}, - {"pip://raven:5.13.0", "raven", "5.13.0", "Python"}, - {"composer://nunomaduro/collision:1.1", "nunomaduro/collision", "1.1", "Composer"}, - {"go://github.com/ethereum/go-ethereum:1.8.2", "github.com/ethereum/go-ethereum", "1.8.2", "Go"}, - {"alpine://3.7:htop:2.0.2-r0", "3.7:htop", "2.0.2-r0", "Alpine"}, - {"invalid-component-id:1.0.0", "invalid-component-id:1.0.0", "", ""}, - } - - for _, test := range tests { - actualCompName, actualCompVersion, actualCompType := SplitComponentId(test.componentId) - assert.Equal(t, test.expectedCompName, actualCompName) - assert.Equal(t, test.expectedCompVersion, actualCompVersion) - assert.Equal(t, test.expectedCompType, actualCompType) - } -} - -func TestGetDirectComponents(t *testing.T) { - tests := []struct { - impactPaths [][]services.ImpactPathNode - expectedComponentRows []formats.ComponentRow - expectedConvImpactPaths [][]formats.ComponentRow - }{ - {[][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack:1.2.3"}}}, []formats.ComponentRow{{Name: "jfrog:pack", Version: "1.2.3"}}, [][]formats.ComponentRow{{{Name: "jfrog:pack", Version: "1.2.3"}}}}, - {[][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack2:1.2.3"}}}, []formats.ComponentRow{{Name: "jfrog:pack2", Version: "1.2.3"}}, [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack2", Version: "1.2.3"}}}}, - {[][]services.ImpactPathNode{{services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack21:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}, {services.ImpactPathNode{ComponentId: "gav://jfrog:pack1:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack22:1.2.3"}, services.ImpactPathNode{ComponentId: "gav://jfrog:pack3:1.2.3"}}}, []formats.ComponentRow{{Name: "jfrog:pack21", Version: "1.2.3"}, {Name: "jfrog:pack22", Version: "1.2.3"}}, [][]formats.ComponentRow{{{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack21", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}, {{Name: "jfrog:pack1", Version: "1.2.3"}, {Name: "jfrog:pack22", Version: "1.2.3"}, {Name: "jfrog:pack3", Version: "1.2.3"}}}}, - } - - for _, test := range tests { - actualComponentRows, actualConvImpactPaths := getDirectComponentsAndImpactPaths(test.impactPaths) - assert.ElementsMatch(t, test.expectedComponentRows, actualComponentRows) - assert.ElementsMatch(t, test.expectedConvImpactPaths, actualConvImpactPaths) - } -} - -func TestSimplifyVulnerability(t *testing.T) { - vulnerabilities := []services.Vulnerability{ - {Components: map[string]services.Component{"gav://jfrogpack:1.0.0": {ImpactPaths: [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "generic://sha256:1bcd6597181d476796e206e176ccc185b4709ff28fb069c42e7f7f67c6a0ff28/multi3-3.7-20240806.082023-11.war", - FullPath: "multi3-3.7-20240806.082023-11.war"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }}}}, - {Components: map[string]services.Component{"gav://jfrogpack:1.0.1": {}}}, - {Components: map[string]services.Component{"gav://jfrogpack:1.0.0": {ImpactPaths: [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }}}}, - {Components: map[string]services.Component{"gav://jfrogpack:1.0.2": {}}}, - } - tests := []struct { - testName string - expectedImpactPathRoots [][]services.ImpactPathNode - isMultipleRoots bool - }{ - { - "Test multiple roots false", - [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "generic://sha256:1bcd6597181d476796e206e176ccc185b4709ff28fb069c42e7f7f67c6a0ff28/multi3-3.7-20240806.082023-11.war", - FullPath: "multi3-3.7-20240806.082023-11.war"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - {{ComponentId: "build://dort:1"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }, - false, - }, - { - "Test multiple roots true", - [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }, - true, - }, - } - - for _, test := range tests { - t.Run(test.testName, func(t *testing.T) { - testSimplifyVulnerabilityRoot(t, vulnerabilities, test.isMultipleRoots, test.expectedImpactPathRoots) - }) - } - -} - -func testSimplifyVulnerabilityRoot(t *testing.T, vulnerabilities []services.Vulnerability, multipleRoots bool, expectedImpactPath [][]services.ImpactPathNode) { - simplifiedVulnerabilities := simplifyVulnerabilities(vulnerabilities, multipleRoots) - assert.Equal(t, len(vulnerabilities)-1, len(simplifiedVulnerabilities)) - for _, vulnerability := range simplifiedVulnerabilities { - for key := range vulnerability.Components { - if key == "gav://jfrogpack:1.0.0" { - assert.Equal(t, expectedImpactPath, vulnerability.Components[key].ImpactPaths) - } - } - } -} - -func TestSimplifyViolation(t *testing.T) { - violations := []services.Violation{ - {Components: map[string]services.Component{"gav://jfrogpack:1.0.0": {ImpactPaths: [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "generic://sha256:1bcd6597181d476796e206e176ccc185b4709ff28fb069c42e7f7f67c6a0ff28/multi3-3.7-20240806.082023-11.war", - FullPath: "multi3-3.7-20240806.082023-11.war"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }}}, FailBuild: true}, - {Components: map[string]services.Component{"gav://jfrogpack:1.0.1": {}}, FailBuild: true}, - {Components: map[string]services.Component{"gav://jfrogpack:1.0.0": {ImpactPaths: [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }}}, FailBuild: true}, - {Components: map[string]services.Component{"gav://jfrogpack:1.0.2": {}}, FailBuild: true}, - } - tests := []struct { - testName string - expectedImpactPathRoots [][]services.ImpactPathNode - isMultipleRoots bool - }{ - { - "Test multiple roots false", - [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "generic://sha256:1bcd6597181d476796e206e176ccc185b4709ff28fb069c42e7f7f67c6a0ff28/multi3-3.7-20240806.082023-11.war", - FullPath: "multi3-3.7-20240806.082023-11.war"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - {{ComponentId: "build://dort:1"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }, - false, - }, - { - "Test multiple roots true", - [][]services.ImpactPathNode{ - {{ComponentId: "build://dort:1"}, - {ComponentId: "gav://jfrogpack:1.0.0", FullPath: "jfrogpack:-1.0.0.jar"}}, - }, - true, - }, - } - - for _, test := range tests { - t.Run(test.testName, func(t *testing.T) { - testSimplifyViolationRoot(t, violations, test.isMultipleRoots, test.expectedImpactPathRoots) - }) - } -} - -func testSimplifyViolationRoot(t *testing.T, violations []services.Violation, multipleRoots bool, expectedImpactPath [][]services.ImpactPathNode) { - simplifiedViolations := simplifyViolations(violations, multipleRoots) - assert.Equal(t, len(violations)-1, len(simplifiedViolations)) - for _, violation := range simplifiedViolations { - for key := range violation.Components { - if key == "gav://jfrogpack:1.0.0" { - assert.Equal(t, expectedImpactPath, violation.Components[key].ImpactPaths) - } - } - } -} - -func TestGetOperationalRiskReadableData(t *testing.T) { - tests := []struct { - testName string - violation services.Violation - expectedResults *operationalRiskViolationReadableData - }{ - { - "Empty Operational Risk Violation", - services.Violation{IsEol: nil, LatestVersion: "", NewerVersions: nil, - Cadence: nil, Commits: nil, Committers: nil, RiskReason: "", EolMessage: ""}, - &operationalRiskViolationReadableData{"N/A", "N/A", "N/A", "N/A", "", "", "N/A", "N/A"}, - }, - { - "Detailed Operational Risk Violation with all fields", - services.Violation{IsEol: newBoolPtr(true), LatestVersion: "1.2.3", NewerVersions: newIntPtr(5), - Cadence: newFloat64Ptr(3.5), Commits: newInt64Ptr(55), Committers: newIntPtr(10), EolMessage: "no maintainers", RiskReason: "EOL"}, - &operationalRiskViolationReadableData{"true", "3.5", "55", "10", "no maintainers", "EOL", "1.2.3", "5"}, - }, - } - - for _, test := range tests { - t.Run(test.testName, func(t *testing.T) { - results := getOperationalRiskViolationReadableData(test.violation) - assert.Equal(t, test.expectedResults, results) - }) - } -} - -// Test Simplified Violations as this is the data we eventually parse in the tables -func TestGetOperationalRiskSimplifiedViolations(t *testing.T) { - violations := []services.Violation{ - {Components: map[string]services.Component{"gav://antparent:ant:1.6.4": {}}, IsEol: nil, LatestVersion: "", NewerVersions: nil, - Cadence: nil, Commits: nil, Committers: nil, RiskReason: "", EolMessage: ""}, - {Components: map[string]services.Component{"gav://antparent:ant:1.6.5": {}}, IsEol: newBoolPtr(true), LatestVersion: "1.2.3", NewerVersions: newIntPtr(5), - Cadence: newFloat64Ptr(3.5), Commits: newInt64Ptr(55), Committers: newIntPtr(10), EolMessage: "no maintainers", RiskReason: "EOL"}, - } - simplifiedViolations := simplifyViolations(violations, true) - - // Sorting the violations based on component key so that the order will be unified for the expected results - // gav://antparent:ant:1.6.4 -> gav://antparent:ant:1.6.5 (if other tests are added, add it in order) - // In the code the violations returned from the function are being sorted at the end of the logic for printing - sortViolationsSliceBasedOnComponentVersion(simplifiedViolations) - tests := []struct { - testName string - violation services.Violation - expectedResults *operationalRiskViolationReadableData - }{ - { - "Empty Operational Risk Violation", - simplifiedViolations[0], - &operationalRiskViolationReadableData{"N/A", "N/A", "N/A", "N/A", "", "", "N/A", "N/A"}, - }, - { - "Detailed Operational Risk Violation with all fields", - simplifiedViolations[1], - &operationalRiskViolationReadableData{"true", "3.5", "55", "10", "no maintainers", "EOL", "1.2.3", "5"}, - }, - } - - for _, test := range tests { - t.Run(test.testName, func(t *testing.T) { - results := getOperationalRiskViolationReadableData(test.violation) - assert.Equal(t, test.expectedResults, results) - }) - } -} - -func getFirstKey(components map[string]services.Component) string { - for key := range components { - return key - } - return "" -} -func sortViolationsSliceBasedOnComponentVersion(simplifiedViolations []services.Violation) { - sort.Slice(simplifiedViolations, func(i, j int) bool { - return getFirstKey(simplifiedViolations[i].Components) < getFirstKey(simplifiedViolations[j].Components) - }) -} - -func TestIsImpactPathIsSubset(t *testing.T) { - testCases := []struct { - name string - target, source, expectedResult []services.ImpactPathNode - }{ - {"subset found in both target and source", - []services.ImpactPathNode{{ComponentId: "B"}, {ComponentId: "C"}}, - []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, - []services.ImpactPathNode{{ComponentId: "B"}, {ComponentId: "C"}}, - }, - {"subset not found in both target and source", - []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "D"}}, - []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, - []services.ImpactPathNode{}, - }, - {"target and source are identical", - []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}}, - []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}}, - []services.ImpactPathNode{{ComponentId: "A"}, {ComponentId: "B"}}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := isImpactPathIsSubset(tc.target, tc.source) - assert.Equal(t, tc.expectedResult, result) - }) - } -} - -func TestAppendUniqueFixVersions(t *testing.T) { - testCases := []struct { - targetFixVersions []string - sourceFixVersions []string - expectedResult []string - }{ - { - targetFixVersions: []string{"1.0", "1.1"}, - sourceFixVersions: []string{"2.0", "2.1"}, - expectedResult: []string{"1.0", "1.1", "2.0", "2.1"}, - }, - { - targetFixVersions: []string{"1.0", "1.1"}, - sourceFixVersions: []string{"1.1", "2.0"}, - expectedResult: []string{"1.0", "1.1", "2.0"}, - }, - { - targetFixVersions: []string{}, - sourceFixVersions: []string{"1.0", "1.1"}, - expectedResult: []string{"1.0", "1.1"}, - }, - { - targetFixVersions: []string{"1.0", "1.1"}, - sourceFixVersions: []string{}, - expectedResult: []string{"1.0", "1.1"}, - }, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("target:%v, source:%v", tc.targetFixVersions, tc.sourceFixVersions), func(t *testing.T) { - result := appendUniqueFixVersions(tc.targetFixVersions, tc.sourceFixVersions...) - assert.ElementsMatch(t, tc.expectedResult, result) - }) - } -} - -func TestGetUniqueKey(t *testing.T) { - vulnerableDependency := "test-dependency" - vulnerableVersion := "1.0" - expectedKey := "test-dependency:1.0:XRAY-12234:true" - key := GetUniqueKey(vulnerableDependency, vulnerableVersion, "XRAY-12234", true) - assert.Equal(t, expectedKey, key) - - expectedKey = "test-dependency:1.0:XRAY-12143:false" - key = GetUniqueKey(vulnerableDependency, vulnerableVersion, "XRAY-12143", false) - assert.Equal(t, expectedKey, key) -} - -func TestAppendUniqueImpactPathsForMultipleRoots(t *testing.T) { - testCases := []struct { - name string - target [][]services.ImpactPathNode - source [][]services.ImpactPathNode - expectedResult [][]services.ImpactPathNode - }{ - { - name: "subset is found in both target and source", - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, - {{ComponentId: "D"}, {ComponentId: "E"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "B"}, {ComponentId: "C"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - expectedResult: [][]services.ImpactPathNode{ - {{ComponentId: "B"}, {ComponentId: "C"}}, - {{ComponentId: "D"}, {ComponentId: "E"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - }, - { - name: "subset is not found in both target and source", - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "C"}}, - {{ComponentId: "D"}, {ComponentId: "E"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "B"}, {ComponentId: "C"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - expectedResult: [][]services.ImpactPathNode{ - {{ComponentId: "B"}, {ComponentId: "C"}}, - {{ComponentId: "D"}, {ComponentId: "E"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - }, - { - name: "target slice is empty", - target: [][]services.ImpactPathNode{}, - source: [][]services.ImpactPathNode{ - {{ComponentId: "E"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - expectedResult: [][]services.ImpactPathNode{ - {{ComponentId: "E"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - }, - { - name: "source slice is empty", - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - source: [][]services.ImpactPathNode{}, - expectedResult: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - }, - { - name: "target and source slices are identical", - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - expectedResult: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - }, - { - name: "target and source slices contain multiple subsets", - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}, {ComponentId: "E"}}, - {{ComponentId: "C"}, {ComponentId: "D"}, {ComponentId: "F"}}, - {{ComponentId: "G"}, {ComponentId: "H"}}, - }, - expectedResult: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - {{ComponentId: "G"}, {ComponentId: "H"}}, - }, - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expectedResult, appendUniqueImpactPathsForMultipleRoots(test.target, test.source)) - }) - } -} - -func TestGetImpactPathKey(t *testing.T) { - testCases := []struct { - path []services.ImpactPathNode - expectedKey string - }{ - { - path: []services.ImpactPathNode{ - {ComponentId: "A"}, - {ComponentId: "B"}, - }, - expectedKey: "B", - }, - { - path: []services.ImpactPathNode{ - {ComponentId: "A"}, - }, - expectedKey: "A", - }, - } - - for _, test := range testCases { - key := getImpactPathKey(test.path) - assert.Equal(t, test.expectedKey, key) - } -} - -func TestAppendUniqueImpactPaths(t *testing.T) { - testCases := []struct { - name string - multipleRoots bool - target [][]services.ImpactPathNode - source [][]services.ImpactPathNode - expected [][]services.ImpactPathNode - }{ - { - name: "Test case 1: Unique impact paths found", - multipleRoots: false, - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}}, - {{ComponentId: "B"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "C"}}, - {{ComponentId: "D"}}, - }, - expected: [][]services.ImpactPathNode{ - {{ComponentId: "A"}}, - {{ComponentId: "B"}}, - {{ComponentId: "C"}}, - {{ComponentId: "D"}}, - }, - }, - { - name: "Test case 2: No unique impact paths found", - multipleRoots: false, - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}}, - {{ComponentId: "B"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "A"}}, - {{ComponentId: "B"}}, - }, - expected: [][]services.ImpactPathNode{ - {{ComponentId: "A"}}, - {{ComponentId: "B"}}, - }, - }, - { - name: "Test case 3: paths in source are not in target", - multipleRoots: false, - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "E"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - expected: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - {{ComponentId: "E"}}, - {{ComponentId: "F"}, {ComponentId: "G"}}, - }, - }, - { - name: "Test case 4: paths in source are already in target", - multipleRoots: false, - target: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - source: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - expected: [][]services.ImpactPathNode{ - {{ComponentId: "A"}, {ComponentId: "B"}}, - {{ComponentId: "C"}, {ComponentId: "D"}}, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := appendUniqueImpactPaths(tc.target, tc.source, tc.multipleRoots) - assert.Equal(t, tc.expected, result) - }) - } -} - -func TestGetApplicableCveValue(t *testing.T) { - testCases := []struct { - name string - scanResults *ExtendedScanResults - cves []services.Cve - expectedResult jasutils.ApplicabilityStatus - expectedCves []formats.CveRow - }{ - { - name: "not entitled for jas", - scanResults: &ExtendedScanResults{EntitledForJas: false}, - expectedResult: jasutils.NotScanned, - }, - { - name: "no cves", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithOneLocation("fileName1", 0, 1, 0, 0, "snippet1", "applic_testCve1", "info"), - sarifutils.CreateDummyPassingResult("applic_testCve2"), - ), - }, - EntitledForJas: true, - }, - cves: nil, - expectedResult: jasutils.NotCovered, - expectedCves: nil, - }, - { - name: "applicable cve", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateDummyPassingResult("applic_testCve1"), - sarifutils.CreateResultWithOneLocation("fileName2", 1, 0, 0, 0, "snippet2", "applic_testCve2", "warning"), - ), - }, - EntitledForJas: true, - }, - cves: []services.Cve{{Id: "testCve2"}}, - expectedResult: jasutils.Applicable, - expectedCves: []formats.CveRow{{Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.Applicable.String()}}}, - }, - { - name: "missing context cve", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"missing_context"}), - }, - EntitledForJas: true, - }, - cves: []services.Cve{{Id: "testCve1"}}, - expectedResult: jasutils.MissingContext, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.MissingContext.String()}}}, - }, - { - name: "undetermined cve", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateDummyPassingResult("applic_testCve1"), - sarifutils.CreateResultWithOneLocation("fileName3", 0, 1, 0, 0, "snippet3", "applic_testCve2", "info"), - ), - }, - EntitledForJas: true, - }, - cves: []services.Cve{{Id: "testCve3"}}, - expectedResult: jasutils.ApplicabilityUndetermined, - expectedCves: []formats.CveRow{{Id: "testCve3"}}, - }, - { - name: "not applicable cve", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateDummyPassingResult("applic_testCve1"), - sarifutils.CreateDummyPassingResult("applic_testCve2"), - ), - }, - EntitledForJas: true, - }, - cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, - expectedResult: jasutils.NotApplicable, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, - }, - { - name: "applicable and not applicable cves", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateDummyPassingResult("applic_testCve1"), - sarifutils.CreateResultWithOneLocation("fileName4", 1, 0, 0, 0, "snippet", "applic_testCve2", "warning"), - ), - }, - EntitledForJas: true, - }, - cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, - expectedResult: jasutils.Applicable, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.Applicable.String()}}}, - }, - { - name: "undetermined and not applicable cves", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyPassingResult("applic_testCve1")), - }, - EntitledForJas: true}, - cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, - expectedResult: jasutils.ApplicabilityUndetermined, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}, {Id: "testCve2"}}, - }, - { - name: "new scan statuses - applicable wins all statuses", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"applicable"}), - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"not_applicable"}), - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve3"), []string{"applicability"}, []string{"not_covered"}), - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve4"), []string{"applicability"}, []string{"missing_context"}), - }, - EntitledForJas: true}, - cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}, {Id: "testCve4"}}, - expectedResult: jasutils.Applicable, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.Applicable.String()}}, - {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}, - {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotCovered.String()}}, - {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.MissingContext.String()}}, - }, - }, - { - name: "new scan statuses - not covered wins not applicable", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"not_covered"}), - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"not_applicable"}), - }, - EntitledForJas: true}, - cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, - expectedResult: jasutils.NotCovered, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.NotCovered.String()}}, - {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}, - }, - }, - { - name: "new scan statuses - undetermined wins missing-context", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"missing_context"}), - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"undetermined"}), - }, - EntitledForJas: true}, - cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, - expectedResult: jasutils.ApplicabilityUndetermined, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.MissingContext.String()}}, - {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.ApplicabilityUndetermined.String()}}, - }, - }, - { - name: "undetermined with undetermined reason", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability", "undetermined_reason"}, []string{"undetermined", "however"}), - }, - EntitledForJas: true}, - cves: []services.Cve{{Id: "testCve2"}}, - expectedResult: jasutils.ApplicabilityUndetermined, - expectedCves: []formats.CveRow{ - {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.ApplicabilityUndetermined.String(), UndeterminedReason: "however"}}, - }, - }, - { - name: "new scan statuses - missing context wins not covered", - scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve1"), []string{"applicability"}, []string{"missing_context"}), - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyPassingResult("applic_testCve2"), []string{"applicability"}, []string{"not_covered"}), - }, - EntitledForJas: true}, - cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, - expectedResult: jasutils.MissingContext, - expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: jasutils.MissingContext.String()}}, - {Id: "testCve2", Applicability: &formats.Applicability{Status: jasutils.NotCovered.String()}}, - }, - }, - } - - for _, testCase := range testCases { - cves := convertCves(testCase.cves) - for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i].Id, testCase.scanResults.ApplicabilityScanResults, nil) - } - applicableValue := getApplicableCveStatus(testCase.scanResults.EntitledForJas, testCase.scanResults.ApplicabilityScanResults, cves) - assert.Equal(t, testCase.expectedResult, applicableValue) - if assert.True(t, len(testCase.expectedCves) == len(cves)) { - for i := range cves { - if testCase.expectedCves[i].Applicability != nil && assert.NotNil(t, cves[i].Applicability) { - assert.Equal(t, testCase.expectedCves[i].Applicability.Status, cves[i].Applicability.Status) - } - } - } - } -} - -func TestSortVulnerabilityOrViolationRows(t *testing.T) { - testCases := []struct { - name string - rows []formats.VulnerabilityOrViolationRow - expectedOrder []string - }{ - { - name: "Sort by severity with different severity values", - rows: []formats.VulnerabilityOrViolationRow{ - { - Summary: "Summary 1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "High", - SeverityNumValue: 9, - }, - ImpactedDependencyName: "Dependency 1", - ImpactedDependencyVersion: "1.0.0", - }, - FixedVersions: []string{}, - }, - { - Summary: "Summary 2", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Critical", - SeverityNumValue: 12, - }, - ImpactedDependencyName: "Dependency 2", - ImpactedDependencyVersion: "2.0.0", - }, - FixedVersions: []string{"1.0.0"}, - }, - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 6, - }, - ImpactedDependencyName: "Dependency 3", - ImpactedDependencyVersion: "3.0.0", - }, - Summary: "Summary 3", - FixedVersions: []string{}, - }, - }, - expectedOrder: []string{"Dependency 2", "Dependency 1", "Dependency 3"}, - }, - { - name: "Sort by severity with same severity values, but different fixed versions", - rows: []formats.VulnerabilityOrViolationRow{ - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Critical", - SeverityNumValue: 12, - }, - ImpactedDependencyName: "Dependency 1", - ImpactedDependencyVersion: "1.0.0", - }, - Summary: "Summary 1", - FixedVersions: []string{"1.0.0"}, - }, - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Critical", - SeverityNumValue: 12, - }, - ImpactedDependencyName: "Dependency 2", - ImpactedDependencyVersion: "2.0.0", - }, - Summary: "Summary 2", - FixedVersions: []string{}, - }, - }, - expectedOrder: []string{"Dependency 1", "Dependency 2"}, - }, - { - name: "Sort by severity with same severity values different applicability", - rows: []formats.VulnerabilityOrViolationRow{ - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Critical", - SeverityNumValue: 13, - }, - ImpactedDependencyName: "Dependency 1", - ImpactedDependencyVersion: "1.0.0", - }, - Summary: "Summary 1", - Applicable: jasutils.Applicable.String(), - FixedVersions: []string{"1.0.0"}, - }, - { - Summary: "Summary 2", - Applicable: jasutils.NotApplicable.String(), - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Critical", - SeverityNumValue: 11, - }, - ImpactedDependencyName: "Dependency 2", - ImpactedDependencyVersion: "2.0.0", - }, - }, - { - Summary: "Summary 3", - Applicable: jasutils.ApplicabilityUndetermined.String(), - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Critical", - SeverityNumValue: 12, - }, - ImpactedDependencyName: "Dependency 3", - ImpactedDependencyVersion: "2.0.0", - }, - }, - }, - expectedOrder: []string{"Dependency 1", "Dependency 3", "Dependency 2"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - sortVulnerabilityOrViolationRows(tc.rows) - - for i, row := range tc.rows { - assert.Equal(t, tc.expectedOrder[i], row.ImpactedDependencyName) - } - }) - } -} - -func TestShouldDisqualifyEvidence(t *testing.T) { - testCases := []struct { - name string - component map[string]services.Component - filePath string - disqualify bool - }{ - { - name: "package folders", - component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, - filePath: "file:///Users/jfrog/test/node_modules/protobufjs/src/badCode.js", - disqualify: true, - }, { - name: "nested folders", - component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, - filePath: "file:///Users/jfrog/test/node_modules/someDep/node_modules/protobufjs/src/badCode.js", - disqualify: true, - }, { - name: "applicability in node modules", - component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, - filePath: "file:///Users/jfrog/test/node_modules/mquery/src/badCode.js", - disqualify: false, - }, { - // Only npm supported - name: "not npm", - component: map[string]services.Component{"yarn://protobufjs:6.11.2": {}}, - filePath: "file:///Users/jfrog/test/node_modules/protobufjs/src/badCode.js", - disqualify: false, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.disqualify, shouldDisqualifyEvidence(tc.component, tc.filePath)) - }) - } -} - -func TestPrepareIac(t *testing.T) { - testCases := []struct { - name string - input []*sarif.Run - expectedOutput []formats.SourceCodeRow - }{ - { - name: "No Iac run", - input: []*sarif.Run{}, - expectedOutput: []formats.SourceCodeRow{}, - }, - { - name: "Prepare Iac run - no results", - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults(), - }, - expectedOutput: []formats.SourceCodeRow{}, - }, - { - name: "Prepare Iac run - with results", - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("iac finding", "rule1", "info", - sarifutils.CreateLocation("file://wd/file", 1, 2, 3, 4, "snippet"), - sarifutils.CreateLocation("file://wd/file2", 5, 6, 7, 8, "other-snippet"), - ), - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("wd")), - }), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("other iac finding", "rule2", "error", - sarifutils.CreateLocation("file://wd2/file3", 1, 2, 3, 4, "snippet"), - ), - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("wd2")), - }), - }, - expectedOutput: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{ - Severity: "High", - SeverityNumValue: 21, - }, - Finding: "other iac finding", - Location: formats.Location{ - File: "file3", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "snippet", - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 17, - }, - Finding: "iac finding", - Location: formats.Location{ - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "snippet", - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 17, - }, - Finding: "iac finding", - Location: formats.Location{ - File: "file2", - StartLine: 5, - StartColumn: 6, - EndLine: 7, - EndColumn: 8, - Snippet: "other-snippet", - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.ElementsMatch(t, tc.expectedOutput, prepareIacs(tc.input, false)) - }) - } -} - -func TestPrepareSecrets(t *testing.T) { - testCases := []struct { - name string - isTokenValidationRun bool - input []*sarif.Run - expectedOutput []formats.SourceCodeRow - }{ - { - name: "No Secret run", - input: []*sarif.Run{}, - expectedOutput: []formats.SourceCodeRow{}, - }, - { - name: "Prepare Secret run - no results", - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults(), - }, - expectedOutput: []formats.SourceCodeRow{}, - }, - { - name: "Prepare Secret run - with results", - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("secret finding", "rule1", "info", - sarifutils.CreateLocation("file://wd/file", 1, 2, 3, 4, "some-secret-snippet"), - sarifutils.CreateLocation("file://wd/file2", 5, 6, 7, 8, "other-secret-snippet"), - ), - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("wd")), - }), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("other secret finding", "rule2", "note", - sarifutils.CreateLocation("file://wd2/file3", 1, 2, 3, 4, "some-secret-snippet"), - ), - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("wd2")), - }), - }, - expectedOutput: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Low", - SeverityNumValue: 13, - }, - Finding: "other secret finding", - Location: formats.Location{ - File: "file3", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "some-secret-snippet", - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 17, - }, - Finding: "secret finding", - Location: formats.Location{ - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "some-secret-snippet", - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 17, - }, - Finding: "secret finding", - Location: formats.Location{ - File: "file2", - StartLine: 5, - StartColumn: 6, - EndLine: 7, - EndColumn: 8, - Snippet: "other-secret-snippet", - }, - }, - }, - }, - { - name: "Prepare Secret run - with results and tokens validation", - isTokenValidationRun: true, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("secret finding", "rule2", "note", sarifutils.CreateLocation("file://file", 1, 2, 3, 4, "some-secret-snippet"))), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithProperties("other secret finding", "rule2", "note", map[string]string{"tokenValidation": "Inactive", "metadata": ""}, sarifutils.CreateLocation("file://file", 1, 2, 3, 4, "some-secret-snippet")), - ), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithProperties("another secret finding", "rule2", "note", map[string]string{"tokenValidation": "Active", "metadata": "testmetadata"}, sarifutils.CreateLocation("file://file", 1, 2, 3, 4, "some-secret-snippet")), - ), - }, - expectedOutput: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Low", - SeverityNumValue: 13, - }, - Applicability: nil, - Finding: "secret finding", - Location: formats.Location{ - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "some-secret-snippet", - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Low", - SeverityNumValue: 13, - }, - Applicability: &formats.Applicability{Status: "Inactive", ScannerDescription: ""}, - Finding: "other secret finding", - Location: formats.Location{ - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "some-secret-snippet", - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Low", - SeverityNumValue: 13, - }, - Applicability: &formats.Applicability{Status: "Active", ScannerDescription: "testmetadata"}, - Finding: "another secret finding", - Location: formats.Location{ - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "some-secret-snippet", - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - rows := prepareSecrets(tc.input, false) - assert.ElementsMatch(t, tc.expectedOutput, rows) - if tc.isTokenValidationRun { - assert.Equal(t, "Active", rows[0].Applicability.Status) - assert.Equal(t, "Inactive", rows[1].Applicability.Status) - assert.Nil(t, rows[2].Applicability) - } - }) - } -} - -func TestPrepareSast(t *testing.T) { - testCases := []struct { - name string - input []*sarif.Run - expectedOutput []formats.SourceCodeRow - }{ - { - name: "No Sast run", - input: []*sarif.Run{}, - expectedOutput: []formats.SourceCodeRow{}, - }, - { - name: "Prepare Sast run - no results", - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults(), - }, - expectedOutput: []formats.SourceCodeRow{}, - }, - { - name: "Prepare Sast run - with results", - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("sast finding", "rule1", "info", - sarifutils.CreateLocation("file://wd/file", 1, 2, 3, 4, "snippet"), - sarifutils.CreateLocation("file://wd/file2", 5, 6, 7, 8, "other-snippet"), - ).WithCodeFlows([]*sarif.CodeFlow{ - sarifutils.CreateCodeFlow(sarifutils.CreateThreadFlow( - sarifutils.CreateLocation("file://wd/file2", 0, 2, 0, 2, "snippetA"), - sarifutils.CreateLocation("file://wd/file", 1, 2, 3, 4, "snippet"), - )), - sarifutils.CreateCodeFlow(sarifutils.CreateThreadFlow( - sarifutils.CreateLocation("file://wd/file4", 1, 0, 1, 8, "snippetB"), - sarifutils.CreateLocation("file://wd/file", 1, 2, 3, 4, "snippet"), - )), - }), - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("wd")), - }), - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("other sast finding", "rule2", "error", - sarifutils.CreateLocation("file://wd2/file3", 1, 2, 3, 4, "snippet"), - ), - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("wd2")), - }), - }, - expectedOutput: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{ - Severity: "High", - SeverityNumValue: 21, - }, - Finding: "other sast finding", - Location: formats.Location{ - File: "file3", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "snippet", - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 17, - }, - Finding: "sast finding", - Location: formats.Location{ - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "snippet", - }, - CodeFlow: [][]formats.Location{ - { - { - File: "file2", - StartLine: 0, - StartColumn: 2, - EndLine: 0, - EndColumn: 2, - Snippet: "snippetA", - }, - { - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "snippet", - }, - }, - { - { - File: "file4", - StartLine: 1, - StartColumn: 0, - EndLine: 1, - EndColumn: 8, - Snippet: "snippetB", - }, - { - File: "file", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "snippet", - }, - }, - }, - }, - { - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 17, - }, - Finding: "sast finding", - Location: formats.Location{ - File: "file2", - StartLine: 5, - StartColumn: 6, - EndLine: 7, - EndColumn: 8, - Snippet: "other-snippet", - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.ElementsMatch(t, tc.expectedOutput, prepareSast(tc.input, false)) - }) - } -} - -func TestGetFinalApplicabilityStatus(t *testing.T) { - testCases := []struct { - name string - input []jasutils.ApplicabilityStatus - expectedOutput jasutils.ApplicabilityStatus - }{ - { - name: "applicable wins all statuses", - input: []jasutils.ApplicabilityStatus{jasutils.ApplicabilityUndetermined, jasutils.Applicable, jasutils.NotCovered, jasutils.NotApplicable}, - expectedOutput: jasutils.Applicable, - }, - { - name: "undetermined wins not covered", - input: []jasutils.ApplicabilityStatus{jasutils.NotCovered, jasutils.ApplicabilityUndetermined, jasutils.NotCovered, jasutils.NotApplicable}, - expectedOutput: jasutils.ApplicabilityUndetermined, - }, - { - name: "not covered wins not applicable", - input: []jasutils.ApplicabilityStatus{jasutils.NotApplicable, jasutils.NotCovered, jasutils.NotApplicable}, - expectedOutput: jasutils.NotCovered, - }, - { - name: "all statuses are not applicable", - input: []jasutils.ApplicabilityStatus{jasutils.NotApplicable, jasutils.NotApplicable, jasutils.NotApplicable}, - expectedOutput: jasutils.NotApplicable, - }, - { - name: "no statuses", - input: []jasutils.ApplicabilityStatus{}, - expectedOutput: jasutils.NotScanned, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expectedOutput, getFinalApplicabilityStatus(tc.input)) - }) - } -} - -func newBoolPtr(v bool) *bool { - return &v -} - -func newIntPtr(v int) *int { - return &v -} - -func newInt64Ptr(v int64) *int64 { - return &v -} - -func newFloat64Ptr(v float64) *float64 { - return &v -} diff --git a/utils/resultwriter.go b/utils/resultwriter.go deleted file mode 100644 index cb5f72d6..00000000 --- a/utils/resultwriter.go +++ /dev/null @@ -1,1173 +0,0 @@ -package utils - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/jfrog/gofrog/datastructures" - "github.com/jfrog/jfrog-cli-core/v2/common/format" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" - "github.com/jfrog/jfrog-cli-security/utils/jasutils" - "github.com/jfrog/jfrog-cli-security/utils/severityutils" - "github.com/jfrog/jfrog-cli-security/utils/techutils" - clientUtils "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/owenrumney/go-sarif/v2/sarif" - "golang.org/x/exp/slices" -) - -const ( - BaseDocumentationURL = "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/" - CurrentWorkflowNameEnvVar = "GITHUB_WORKFLOW" - CurrentWorkflowRunNumberEnvVar = "GITHUB_RUN_NUMBER" - CurrentWorkflowWorkspaceEnvVar = "GITHUB_WORKSPACE" - - MissingCveScore = "0" - maxPossibleCve = 10.0 - - // #nosec G101 -- Not credentials. - patchedBinarySecretScannerToolName = "JFrog Binary Secrets Scanner" - jfrogFingerprintAlgorithmName = "jfrogFingerprintHash" -) - -var ( - GithubBaseWorkflowDir = filepath.Join(".github", "workflows") - dockerJasLocationPathPattern = regexp.MustCompile(`.*[\\/](?P[^\\/]+)[\\/](?P[0-9a-fA-F]+)[\\/](?P.*)`) - dockerScaComponentNamePattern = regexp.MustCompile(`(?P[^__]+)__(?P[0-9a-fA-F]+)\.tar`) -) - -type ResultsWriter struct { - // The scan results. - results *Results - // SimpleJsonError Errors to be added to output of the SimpleJson format. - simpleJsonError []formats.SimpleJsonError - // Format The output format. - format format.OutputFormat - // IncludeVulnerabilities If true, include all vulnerabilities as part of the output. - includeVulnerabilities bool - // If true, include violations as part of the output. - hasViolationContext bool - // IncludeLicenses If true, also include license violations as part of the output. - includeLicenses bool - // IsMultipleRoots multipleRoots is set to true, in case the given results array contains (or may contain) results of several projects (like in binary scan). - isMultipleRoots bool - // PrintExtended, If true, show extended results. - printExtended bool - // For table format - show table only for the given subScansPreformed - subScansPreformed []SubScanType - // Messages - Option array of messages, to be displayed if the format is Table - messages []string -} - -func NewResultsWriter(scanResults *Results) *ResultsWriter { - return &ResultsWriter{results: scanResults} -} - -func GetScaScanFileName(r *Results) string { - if len(r.ScaResults) > 0 { - return r.ScaResults[0].Target - } - return "" -} - -func (rw *ResultsWriter) SetHasViolationContext(hasViolationContext bool) *ResultsWriter { - rw.hasViolationContext = hasViolationContext - return rw -} - -func (rw *ResultsWriter) SetOutputFormat(f format.OutputFormat) *ResultsWriter { - rw.format = f - return rw -} - -func (rw *ResultsWriter) SetSimpleJsonError(jsonErrors []formats.SimpleJsonError) *ResultsWriter { - rw.simpleJsonError = jsonErrors - return rw -} - -func (rw *ResultsWriter) SetIncludeVulnerabilities(includeVulnerabilities bool) *ResultsWriter { - rw.includeVulnerabilities = includeVulnerabilities - return rw -} - -func (rw *ResultsWriter) SetIncludeLicenses(licenses bool) *ResultsWriter { - rw.includeLicenses = licenses - return rw -} - -func (rw *ResultsWriter) SetIsMultipleRootProject(isMultipleRootProject bool) *ResultsWriter { - rw.isMultipleRoots = isMultipleRootProject - return rw -} - -func (rw *ResultsWriter) SetPrintExtendedTable(extendedTable bool) *ResultsWriter { - rw.printExtended = extendedTable - return rw -} - -func (rw *ResultsWriter) SetExtraMessages(messages []string) *ResultsWriter { - rw.messages = messages - return rw -} - -func (rw *ResultsWriter) SetSubScansPreformed(subScansPreformed []SubScanType) *ResultsWriter { - rw.subScansPreformed = subScansPreformed - return rw -} - -// PrintScanResults prints the scan results in the specified format. -// Note that errors are printed only with SimpleJson format. -func (rw *ResultsWriter) PrintScanResults() error { - switch rw.format { - case format.Table: - return rw.printScanResultsTables() - case format.SimpleJson: - jsonTable, err := rw.convertScanToSimpleJson() - if err != nil { - return err - } - return PrintJson(jsonTable) - case format.Json: - return PrintJson(rw.results.GetScaScansXrayResults()) - case format.Sarif: - return PrintSarif(rw.results, rw.isMultipleRoots, rw.includeLicenses, rw.subScansPreformed) - } - return nil -} - -func (rw *ResultsWriter) printScanResultsTables() (err error) { - printMessages(rw.messages) - violations, vulnerabilities, licenses := SplitScanResults(rw.results.ScaResults) - if rw.results.IsIssuesFound() { - var resultsPath string - if resultsPath, err = writeJsonResults(rw.results); err != nil { - return - } - printMessage(coreutils.PrintTitle("The full scan results are available here: ") + coreutils.PrintLink(resultsPath)) - } - log.Output() - if shouldScannerBeCalled(rw.subScansPreformed, ScaScan, rw.results.ResultType) { - if rw.hasViolationContext { - if err = PrintViolationsTable(violations, rw.results, rw.isMultipleRoots, rw.printExtended); err != nil { - return - } - } - if rw.includeVulnerabilities { - if err = PrintVulnerabilitiesTable(vulnerabilities, rw.results, rw.isMultipleRoots, rw.printExtended, rw.results.ResultType); err != nil { - return - } - } - if rw.includeLicenses { - if err = PrintLicensesTable(licenses, rw.printExtended, rw.results.ResultType); err != nil { - return - } - } - } - if shouldScannerBeCalled(rw.subScansPreformed, SecretsScan, rw.results.ResultType) { - if err = PrintSecretsTable(rw.results.ExtendedScanResults.SecretsScanResults, rw.results.ExtendedScanResults.EntitledForJas, rw.results.ExtendedScanResults.SecretValidation); err != nil { - return - } - } - if shouldScannerBeCalled(rw.subScansPreformed, IacScan, rw.results.ResultType) { - if err = PrintIacTable(rw.results.ExtendedScanResults.IacScanResults, rw.results.ExtendedScanResults.EntitledForJas); err != nil { - return - } - } - if !shouldScannerBeCalled(rw.subScansPreformed, SastScan, rw.results.ResultType) { - return nil - } - return PrintSastTable(rw.results.ExtendedScanResults.SastScanResults, rw.results.ExtendedScanResults.EntitledForJas) -} - -func shouldScannerBeCalled(requestedScans []SubScanType, subScan SubScanType, scanType CommandType) bool { - if scanType.IsTargetBinary() && (subScan == IacScan || subScan == SastScan) { - return false - } - return len(requestedScans) == 0 || slices.Contains(requestedScans, subScan) -} - -func printMessages(messages []string) { - if len(messages) > 0 { - log.Output() - } - for _, m := range messages { - printMessage(m) - } -} - -func printMessage(message string) { - log.Output("💬" + message) -} - -func filterAndPatchRunsIfRequired(requestedScans []SubScanType, subScan SubScanType, results *Results, scanResults []*sarif.Run) (filtered []*sarif.Run) { - if !shouldScannerBeCalled(requestedScans, subScan, results.ResultType) { - return - } - return patchRunsToPassIngestionRules(subScan, results, scanResults...) -} - -func GenerateSarifReportFromResults(results *Results, isMultipleRoots, includeLicenses bool, allowedLicenses []string, requestedScans []SubScanType) (report *sarif.Report, err error) { - report, err = sarifutils.NewReport() - if err != nil { - return - } - xrayRun, err := convertXrayResponsesToSarifRun(results, isMultipleRoots, includeLicenses, allowedLicenses) - if err != nil { - return - } - - report.Runs = append(report.Runs, filterAndPatchRunsIfRequired(requestedScans, ScaScan, results, []*sarif.Run{xrayRun})...) - report.Runs = append(report.Runs, filterAndPatchRunsIfRequired(requestedScans, IacScan, results, results.ExtendedScanResults.IacScanResults)...) - report.Runs = append(report.Runs, filterAndPatchRunsIfRequired(requestedScans, SecretsScan, results, results.ExtendedScanResults.SecretsScanResults)...) - report.Runs = append(report.Runs, filterAndPatchRunsIfRequired(requestedScans, SastScan, results, results.ExtendedScanResults.SastScanResults)...) - - return -} - -func convertXrayResponsesToSarifRun(results *Results, isMultipleRoots, includeLicenses bool, allowedLicenses []string) (run *sarif.Run, err error) { - xrayJson, err := ConvertXrayScanToSimpleJson(results, isMultipleRoots, includeLicenses, true, allowedLicenses) - if err != nil { - return - } - xrayRun := sarif.NewRunWithInformationURI("JFrog Xray Scanner", BaseDocumentationURL+"sca") - xrayRun.Tool.Driver.Version = &results.XrayVersion - if len(xrayJson.Vulnerabilities) > 0 || len(xrayJson.SecurityViolations) > 0 || len(xrayJson.LicensesViolations) > 0 { - if err = extractXrayIssuesToSarifRun(results, xrayRun, xrayJson); err != nil { - return - } - } - run = xrayRun - return -} - -func extractXrayIssuesToSarifRun(results *Results, run *sarif.Run, xrayJson formats.SimpleJsonResults) error { - for _, vulnerability := range xrayJson.Vulnerabilities { - if err := addXrayCveIssueToSarifRun(results, vulnerability, run); err != nil { - return err - } - } - for _, violation := range xrayJson.SecurityViolations { - if err := addXrayCveIssueToSarifRun(results, violation, run); err != nil { - return err - } - } - for _, license := range xrayJson.LicensesViolations { - if err := addXrayLicenseViolationToSarifRun(results, license, run); err != nil { - return err - } - } - return nil -} - -func addXrayCveIssueToSarifRun(results *Results, issue formats.VulnerabilityOrViolationRow, run *sarif.Run) (err error) { - maxCveScore, err := findMaxCVEScore(issue.Cves) - if err != nil { - return - } - location, err := getXrayIssueLocationIfValidExists(issue.Technology, run) - if err != nil { - return - } - formattedDirectDependencies, err := getDirectDependenciesFormatted(issue.Components) - if err != nil { - return - } - cveId := GetIssueIdentifier(issue.Cves, issue.IssueId) - markdownDescription := getSarifTableDescription(formattedDirectDependencies, maxCveScore, issue.Applicable, issue.FixedVersions) - addXrayIssueToSarifRun( - results.ResultType, - cveId, - issue.ImpactedDependencyName, - issue.ImpactedDependencyVersion, - severityutils.GetSeverity(issue.Severity), - maxCveScore, - issue.Summary, - getXrayIssueSarifHeadline(issue.ImpactedDependencyName, issue.ImpactedDependencyVersion, cveId), - markdownDescription, - issue.Components, - location, - run, - ) - return -} - -func addXrayLicenseViolationToSarifRun(results *Results, license formats.LicenseRow, run *sarif.Run) (err error) { - formattedDirectDependencies, err := getDirectDependenciesFormatted(license.Components) - if err != nil { - return - } - addXrayIssueToSarifRun( - results.ResultType, - license.LicenseKey, - license.ImpactedDependencyName, - license.ImpactedDependencyVersion, - severityutils.GetSeverity(license.Severity), - MissingCveScore, - getLicenseViolationSummary(license.ImpactedDependencyName, license.ImpactedDependencyVersion, license.LicenseKey), - getXrayLicenseSarifHeadline(license.ImpactedDependencyName, license.ImpactedDependencyVersion, license.LicenseKey), - getLicenseViolationMarkdown(license.ImpactedDependencyName, license.ImpactedDependencyVersion, license.LicenseKey, formattedDirectDependencies), - license.Components, - getXrayIssueLocation(""), - run, - ) - return -} - -func addXrayIssueToSarifRun(resultType CommandType, issueId, impactedDependencyName, impactedDependencyVersion string, severity severityutils.Severity, severityScore, summary, title, markdownDescription string, components []formats.ComponentRow, location *sarif.Location, run *sarif.Run) { - // Add rule if not exists - ruleId := getXrayIssueSarifRuleId(impactedDependencyName, impactedDependencyVersion, issueId) - if rule, _ := run.GetRuleById(ruleId); rule == nil { - addXrayRule(ruleId, title, severityScore, summary, markdownDescription, run) - } - // Add result for each component - for _, directDependency := range components { - msg := getXrayIssueSarifHeadline(directDependency.Name, directDependency.Version, issueId) - if result := run.CreateResultForRule(ruleId).WithMessage(sarif.NewTextMessage(msg)).WithLevel(severityutils.SeverityToSarifSeverityLevel(severity).String()); location != nil { - if resultType == DockerImage { - algorithm, layer := getLayerContentFromComponentId(directDependency.Name) - if layer != "" { - logicalLocation := sarifutils.NewLogicalLocation(layer, "layer") - if algorithm != "" { - logicalLocation.Properties = map[string]interface{}{"algorithm": algorithm} - } - location.LogicalLocations = append(location.LogicalLocations, logicalLocation) - } - } - result.AddLocation(location) - } - } -} - -func getDescriptorFullPath(tech techutils.Technology, run *sarif.Run) (string, error) { - descriptors := tech.GetPackageDescriptor() - if len(descriptors) == 1 { - // Generate the full path - return sarifutils.GetFullLocationFileName(strings.TrimSpace(descriptors[0]), run.Invocations), nil - } - for _, descriptor := range descriptors { - // If multiple options return first to match - absolutePath := sarifutils.GetFullLocationFileName(strings.TrimSpace(descriptor), run.Invocations) - if exists, err := fileutils.IsFileExists(absolutePath, false); err != nil { - return "", err - } else if exists { - return absolutePath, nil - } - } - return "", nil -} - -// Get the descriptor location with the Xray issues if exists. -func getXrayIssueLocationIfValidExists(tech techutils.Technology, run *sarif.Run) (location *sarif.Location, err error) { - descriptorPath, err := getDescriptorFullPath(tech, run) - if err != nil { - return - } - return getXrayIssueLocation(descriptorPath), nil -} - -func getXrayIssueLocation(filePath string) *sarif.Location { - if strings.TrimSpace(filePath) == "" { - filePath = "Package-Descriptor" - } - return sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + filePath))) -} - -func addXrayRule(ruleId, ruleDescription, maxCveScore, summary, markdownDescription string, run *sarif.Run) { - rule := run.AddRule(ruleId) - - if maxCveScore != MissingCveScore { - cveRuleProperties := sarif.NewPropertyBag() - cveRuleProperties.Add(severityutils.SarifSeverityRuleProperty, maxCveScore) - rule.WithProperties(cveRuleProperties.Properties) - } - - rule.WithDescription(ruleDescription) - rule.WithHelp(&sarif.MultiformatMessageString{ - Text: &summary, - Markdown: &markdownDescription, - }) -} - -func ConvertXrayScanToSimpleJson(results *Results, isMultipleRoots, includeLicenses, simplifiedOutput bool, allowedLicenses []string) (formats.SimpleJsonResults, error) { - violations, vulnerabilities, licenses := SplitScanResults(results.ScaResults) - jsonTable := formats.SimpleJsonResults{} - if len(vulnerabilities) > 0 { - vulJsonTable, err := PrepareVulnerabilities(vulnerabilities, results, isMultipleRoots, simplifiedOutput) - if err != nil { - return formats.SimpleJsonResults{}, err - } - jsonTable.Vulnerabilities = vulJsonTable - } - if includeLicenses || len(allowedLicenses) > 0 { - licJsonTable, err := PrepareLicenses(licenses) - if err != nil { - return formats.SimpleJsonResults{}, err - } - if includeLicenses { - jsonTable.Licenses = licJsonTable - } - jsonTable.LicensesViolations = GetViolatedLicenses(allowedLicenses, licJsonTable) - } - if len(violations) > 0 { - secViolationsJsonTable, licViolationsJsonTable, opRiskViolationsJsonTable, err := PrepareViolations(violations, results, isMultipleRoots, simplifiedOutput) - if err != nil { - return formats.SimpleJsonResults{}, err - } - jsonTable.SecurityViolations = secViolationsJsonTable - jsonTable.LicensesViolations = licViolationsJsonTable - jsonTable.OperationalRiskViolations = opRiskViolationsJsonTable - } - jsonTable.MultiScanId = results.MultiScanId - return jsonTable, nil -} - -func GetViolatedLicenses(allowedLicenses []string, licenses []formats.LicenseRow) (violatedLicenses []formats.LicenseRow) { - if len(allowedLicenses) == 0 { - return - } - for _, license := range licenses { - if !slices.Contains(allowedLicenses, license.LicenseKey) { - violatedLicenses = append(violatedLicenses, license) - } - } - return -} - -func (rw *ResultsWriter) convertScanToSimpleJson() (formats.SimpleJsonResults, error) { - jsonTable, err := ConvertXrayScanToSimpleJson(rw.results, rw.isMultipleRoots, rw.includeLicenses, false, nil) - if err != nil { - return formats.SimpleJsonResults{}, err - } - if len(rw.results.ExtendedScanResults.SecretsScanResults) > 0 { - jsonTable.Secrets = PrepareSecrets(rw.results.ExtendedScanResults.SecretsScanResults) - } - if len(rw.results.ExtendedScanResults.IacScanResults) > 0 { - jsonTable.Iacs = PrepareIacs(rw.results.ExtendedScanResults.IacScanResults) - } - if len(rw.results.ExtendedScanResults.SastScanResults) > 0 { - jsonTable.Sast = PrepareSast(rw.results.ExtendedScanResults.SastScanResults) - } - jsonTable.Errors = rw.simpleJsonError - - return jsonTable, nil -} - -func GetIssueIdentifier(cvesRow []formats.CveRow, issueId string) string { - var identifier string - if len(cvesRow) != 0 { - var cvesBuilder strings.Builder - for _, cve := range cvesRow { - cvesBuilder.WriteString(cve.Id + ", ") - } - identifier = strings.TrimSuffix(cvesBuilder.String(), ", ") - } - if identifier == "" { - identifier = issueId - } - - return identifier -} - -func getXrayIssueSarifRuleId(depName, version, key string) string { - return fmt.Sprintf("%s_%s_%s", key, depName, version) -} - -func getXrayIssueSarifHeadline(depName, version, key string) string { - return strings.TrimSpace(fmt.Sprintf("[%s] %s %s", key, depName, version)) -} - -func getXrayLicenseSarifHeadline(depName, version, key string) string { - return fmt.Sprintf("License violation [%s] %s %s", key, depName, version) -} - -func getLicenseViolationSummary(depName, version, key string) string { - return fmt.Sprintf("Dependency %s version %s is using a license (%s) that is not allowed.", depName, version, key) -} - -func getLicenseViolationMarkdown(depName, version, key, formattedDirectDependencies string) string { - return fmt.Sprintf("**The following direct dependencies are utilizing the `%s %s` dependency with `%s` license violation:**\n%s", depName, version, key, formattedDirectDependencies) -} - -func getDirectDependenciesFormatted(directDependencies []formats.ComponentRow) (string, error) { - var formattedDirectDependencies strings.Builder - for _, dependency := range directDependencies { - if _, err := formattedDirectDependencies.WriteString(fmt.Sprintf("`%s %s`
", dependency.Name, dependency.Version)); err != nil { - return "", err - } - } - return strings.TrimSuffix(formattedDirectDependencies.String(), "
"), nil -} - -func getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicable string, fixedVersions []string) string { - descriptionFixVersions := "No fix available" - if len(fixedVersions) > 0 { - descriptionFixVersions = strings.Join(fixedVersions, ", ") - } - if applicable == jasutils.NotScanned.String() { - return fmt.Sprintf("| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| %s | %s | %s |", - maxCveScore, formattedDirectDependencies, descriptionFixVersions) - } - return fmt.Sprintf("| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| %s | %s | %s | %s |", - maxCveScore, applicable, formattedDirectDependencies, descriptionFixVersions) -} - -func findMaxCVEScore(cves []formats.CveRow) (string, error) { - maxCve := 0.0 - for _, cve := range cves { - if cve.CvssV3 == "" { - continue - } - floatCve, err := strconv.ParseFloat(cve.CvssV3, 32) - if err != nil { - return "", err - } - if floatCve > maxCve { - maxCve = floatCve - } - // if found maximum possible cve score, no need to keep iterating - if maxCve == maxPossibleCve { - break - } - } - strCve := fmt.Sprintf("%.1f", maxCve) - - return strCve, nil -} - -func patchRules(subScanType SubScanType, cmdResults *Results, rules ...*sarif.ReportingDescriptor) (patched []*sarif.ReportingDescriptor) { - patched = []*sarif.ReportingDescriptor{} - for _, rule := range rules { - // Github code scanning ingestion rules rejects rules without help content. - // Patch by transferring the full description to the help field. - if rule.Help == nil && rule.FullDescription != nil { - rule.Help = rule.FullDescription - } - // SARIF1001 - if both 'id' and 'name' are present, they must be different. If they are identical, the tool must omit the 'name' property. - if rule.Name != nil && rule.ID == *rule.Name { - rule.Name = nil - } - if cmdResults.ResultType.IsTargetBinary() && subScanType == SecretsScan { - // Patch the rule name in case of binary scan - sarifutils.SetRuleShortDescriptionText(fmt.Sprintf("[Secret in Binary found] %s", sarifutils.GetRuleShortDescriptionText(rule)), rule) - } - patched = append(patched, rule) - } - return -} - -func patchResults(subScanType SubScanType, cmdResults *Results, run *sarif.Run, results ...*sarif.Result) (patched []*sarif.Result) { - patched = []*sarif.Result{} - for _, result := range results { - if len(result.Locations) == 0 { - // Github code scanning ingestion rules rejects results without locations. - // Patch by removing results without locations. - log.Debug(fmt.Sprintf("[%s] Removing result [ruleId=%s] without locations: %s", subScanType.String(), sarifutils.GetResultRuleId(result), sarifutils.GetResultMsgText(result))) - continue - } - if cmdResults.ResultType.IsTargetBinary() { - var markdown string - if subScanType == SecretsScan { - markdown = getSecretInBinaryMarkdownMsg(cmdResults, result) - } else { - markdown = getScaInBinaryMarkdownMsg(cmdResults, result) - } - sarifutils.SetResultMsgMarkdown(markdown, result) - // For Binary scans, override the physical location if applicable (after data already used for markdown) - convertBinaryPhysicalLocations(cmdResults, run, result) - // Calculate the fingerprints if not exists - if !sarifutils.IsFingerprintsExists(result) { - if err := calculateResultFingerprints(cmdResults.ResultType, run, result); err != nil { - log.Warn(fmt.Sprintf("Failed to calculate the fingerprint for result [ruleId=%s]: %s", sarifutils.GetResultRuleId(result), err.Error())) - } - } - } - patched = append(patched, result) - } - return patched -} - -func patchRunsToPassIngestionRules(subScanType SubScanType, cmdResults *Results, runs ...*sarif.Run) []*sarif.Run { - // Since we run in temp directories files should be relative - // Patch by converting the file paths to relative paths according to the invocations - convertPaths(cmdResults.ResultType, subScanType, runs...) - for _, run := range runs { - if cmdResults.ResultType.IsTargetBinary() && subScanType == SecretsScan { - // Patch the tool name in case of binary scan - sarifutils.SetRunToolName(patchedBinarySecretScannerToolName, run) - } - run.Tool.Driver.Rules = patchRules(subScanType, cmdResults, run.Tool.Driver.Rules...) - run.Results = patchResults(subScanType, cmdResults, run, run.Results...) - } - return runs -} - -func convertPaths(commandType CommandType, subScanType SubScanType, runs ...*sarif.Run) { - // Convert base on invocation for source code - sarifutils.ConvertRunsPathsToRelative(runs...) - if !(commandType == DockerImage && subScanType == SecretsScan) { - return - } - for _, run := range runs { - for _, result := range run.Results { - // For Docker secret scan, patch the logical location if not exists - patchDockerSecretLocations(result) - } - } -} - -// Patch the URI to be the file path from sha// -// Extract the layer from the location URI, adds it as a logical location kind "layer" -func patchDockerSecretLocations(result *sarif.Result) { - for _, location := range result.Locations { - algorithm, layerHash, relativePath := getLayerContentFromPath(sarifutils.GetLocationFileName(location)) - if layerHash != "" { - // Set Logical location kind "layer" with the layer hash - logicalLocation := sarifutils.NewLogicalLocation(layerHash, "layer") - if algorithm != "" { - logicalLocation.Properties = sarif.Properties(map[string]interface{}{"algorithm": algorithm}) - } - location.LogicalLocations = append(location.LogicalLocations, logicalLocation) - } - if relativePath != "" { - sarifutils.SetLocationFileName(location, relativePath) - } - } -} - -func convertBinaryPhysicalLocations(cmdResults *Results, run *sarif.Run, result *sarif.Result) { - if patchedLocation := getPatchedBinaryLocation(cmdResults, run); patchedLocation != "" { - for _, location := range result.Locations { - // Patch the location - Reset the uri and region - location.PhysicalLocation = sarifutils.NewPhysicalLocation(patchedLocation) - } - } -} - -func getPatchedBinaryLocation(cmdResults *Results, run *sarif.Run) (patchedLocation string) { - if cmdResults.ResultType == DockerImage { - if patchedLocation = getDockerfileLocationIfExists(run); patchedLocation != "" { - return - } - } - return getWorkflowFileLocationIfExists() -} - -func getDockerfileLocationIfExists(run *sarif.Run) string { - potentialLocations := []string{filepath.Clean("Dockerfile"), sarifutils.GetFullLocationFileName("Dockerfile", run.Invocations)} - for _, location := range potentialLocations { - if exists, err := fileutils.IsFileExists(location, false); err == nil && exists { - return location - } - } - if workspace := os.Getenv(CurrentWorkflowWorkspaceEnvVar); workspace != "" { - if exists, err := fileutils.IsFileExists(filepath.Join(workspace, "Dockerfile"), false); err == nil && exists { - return filepath.Join(workspace, "Dockerfile") - } - } - return "" -} - -func getGithubWorkflowsDirIfExists() string { - if exists, err := fileutils.IsDirExists(GithubBaseWorkflowDir, false); err == nil && exists { - return GithubBaseWorkflowDir - } - if workspace := os.Getenv(CurrentWorkflowWorkspaceEnvVar); workspace != "" { - if exists, err := fileutils.IsDirExists(filepath.Join(workspace, GithubBaseWorkflowDir), false); err == nil && exists { - return filepath.Join(workspace, GithubBaseWorkflowDir) - } - } - return "" -} - -func getWorkflowFileLocationIfExists() (location string) { - workflowName := os.Getenv(CurrentWorkflowNameEnvVar) - if workflowName == "" { - return - } - workflowsDir := getGithubWorkflowsDirIfExists() - if workflowsDir == "" { - return - } - currentWd, err := os.Getwd() - if err != nil { - log.Warn(fmt.Sprintf("Failed to get the current working directory to get workflow file location: %s", err.Error())) - return - } - // Check if exists in the .github/workflows directory as file name or in the content, return the file path or empty string - if files, err := fileutils.ListFiles(workflowsDir, false); err == nil && len(files) > 0 { - for _, file := range files { - if strings.Contains(file, workflowName) { - return strings.TrimPrefix(file, currentWd) - } - } - for _, file := range files { - if content, err := fileutils.ReadFile(file); err == nil && strings.Contains(string(content), workflowName) { - return strings.TrimPrefix(file, currentWd) - } - } - } - return -} - -func getSecretInBinaryMarkdownMsg(cmdResults *Results, result *sarif.Result) string { - if cmdResults.ResultType != Binary && cmdResults.ResultType != DockerImage { - return "" - } - content := "🔒 Found Secrets in Binary" - if cmdResults.ResultType == DockerImage { - content += " docker" - } - content += " scanning:" - return content + getBaseBinaryDescriptionMarkdown(SecretsScan, cmdResults, result) -} - -func getScaInBinaryMarkdownMsg(cmdResults *Results, result *sarif.Result) string { - return sarifutils.GetResultMsgText(result) + getBaseBinaryDescriptionMarkdown(ScaScan, cmdResults, result) -} - -func getBaseBinaryDescriptionMarkdown(subScanType SubScanType, cmdResults *Results, result *sarif.Result) (content string) { - // If in github action, add the workflow name and run number - if workflowLocation := getWorkflowFileLocationIfExists(); workflowLocation != "" { - content += fmt.Sprintf("\nGithub Actions Workflow: %s", workflowLocation) - } - if os.Getenv(CurrentWorkflowRunNumberEnvVar) != "" { - content += fmt.Sprintf("\nRun: %s", os.Getenv(CurrentWorkflowRunNumberEnvVar)) - } - // If is docker image, add the image tag - if cmdResults.ResultType == DockerImage { - if imageTag := getDockerImageTag(cmdResults); imageTag != "" { - content += fmt.Sprintf("\nImage: %s", imageTag) - } - } - var location *sarif.Location - if len(result.Locations) > 0 { - location = result.Locations[0] - } - return content + getBinaryLocationMarkdownString(cmdResults.ResultType, subScanType, location, result) -} - -func getDockerImageTag(cmdResults *Results) string { - if cmdResults.ResultType != DockerImage || len(cmdResults.ScaResults) == 0 { - return "" - } - for _, scaResults := range cmdResults.ScaResults { - if scaResults.Name != "" { - return scaResults.Name - } - } - return filepath.Base(cmdResults.ScaResults[0].Target) -} - -// If command is docker prepare the markdown string for the location: -// * Layer: -// * Filepath: -// * Evidence: -func getBinaryLocationMarkdownString(commandType CommandType, subScanType SubScanType, location *sarif.Location, result *sarif.Result) (content string) { - if location == nil { - return "" - } - if commandType == DockerImage { - if layer, algorithm := getDockerLayer(location); layer != "" { - if algorithm != "" { - content += fmt.Sprintf("\nLayer (%s): %s", algorithm, layer) - } else { - content += fmt.Sprintf("\nLayer: %s", layer) - } - } - } - if subScanType != SecretsScan { - return - } - if locationFilePath := sarifutils.GetLocationFileName(location); locationFilePath != "" { - content += fmt.Sprintf("\nFilepath: %s", locationFilePath) - } - if snippet := sarifutils.GetLocationSnippet(location); snippet != "" { - content += fmt.Sprintf("\nEvidence: %s", snippet) - } - if tokenValidation := GetResultPropertyTokenValidation(result); tokenValidation != "" { - content += fmt.Sprintf("\nToken Validation %s", tokenValidation) - } - return -} - -func getDockerLayer(location *sarif.Location) (layer, algorithm string) { - // If location has logical location with kind "layer" return it - if logicalLocation := sarifutils.GetLogicalLocation("layer", location); logicalLocation != nil && logicalLocation.Name != nil { - layer = *logicalLocation.Name - if algorithmValue, ok := logicalLocation.Properties["algorithm"].(string); ok { - algorithm = algorithmValue - } - return - } - return -} - -// Match: -// Extract algorithm, hash and relative path -func getLayerContentFromPath(content string) (algorithm string, layerHash string, relativePath string) { - matches := dockerJasLocationPathPattern.FindStringSubmatch(content) - if len(matches) == 0 { - return - } - algorithm = matches[dockerJasLocationPathPattern.SubexpIndex("algorithm")] - layerHash = matches[dockerJasLocationPathPattern.SubexpIndex("hash")] - relativePath = matches[dockerJasLocationPathPattern.SubexpIndex("relativePath")] - return -} - -// Match: ://:/ -// Extract algorithm and hash -func getLayerContentFromComponentId(componentId string) (algorithm string, layerHash string) { - matches := dockerScaComponentNamePattern.FindStringSubmatch(componentId) - if len(matches) == 0 { - return - } - algorithm = matches[dockerScaComponentNamePattern.SubexpIndex("algorithm")] - layerHash = matches[dockerScaComponentNamePattern.SubexpIndex("hash")] - return -} - -// According to the SARIF specification: -// To determine whether a result from a subsequent run is logically the same as a result from the baseline, -// there must be a way to use information contained in the result to construct a stable identifier for the result. We refer to this identifier as a fingerprint. -// A result management system SHOULD construct a fingerprint by using information contained in the SARIF file such as: -// The name of the tool that produced the result, the rule id, the file system path to the analysis target... -func calculateResultFingerprints(resultType CommandType, run *sarif.Run, result *sarif.Result) error { - if !resultType.IsTargetBinary() { - return nil - } - ids := []string{sarifutils.GetRunToolName(run), sarifutils.GetResultRuleId(result)} - for _, location := range sarifutils.GetResultFileLocations(result) { - ids = append(ids, strings.ReplaceAll(location, string(filepath.Separator), "/")) - } - ids = append(ids, sarifutils.GetResultLocationSnippets(result)...) - // Calculate the hash value and set the fingerprint to the result - hashValue, err := Md5Hash(ids...) - if err != nil { - return err - } - sarifutils.SetResultFingerprint(jfrogFingerprintAlgorithmName, hashValue, result) - return nil -} - -// Splits scan responses into aggregated lists of violations, vulnerabilities and licenses. -func SplitScanResults(results []*ScaScanResult) ([]services.Violation, []services.Vulnerability, []services.License) { - var violations []services.Violation - var vulnerabilities []services.Vulnerability - var licenses []services.License - for _, scan := range results { - for _, result := range scan.XrayResults { - violations = append(violations, result.Violations...) - vulnerabilities = append(vulnerabilities, result.Vulnerabilities...) - licenses = append(licenses, result.Licenses...) - } - } - return violations, vulnerabilities, licenses -} - -func writeJsonResults(results *Results) (resultsPath string, err error) { - out, err := fileutils.CreateTempFile() - if errorutils.CheckError(err) != nil { - return - } - defer func() { - e := out.Close() - if err == nil { - err = e - } - }() - bytesRes, err := JSONMarshalNotEscaped(&results) - if errorutils.CheckError(err) != nil { - return - } - var content bytes.Buffer - err = json.Indent(&content, bytesRes, "", " ") - if errorutils.CheckError(err) != nil { - return - } - _, err = out.Write(content.Bytes()) - if errorutils.CheckError(err) != nil { - return - } - resultsPath = out.Name() - return -} - -func WriteSarifResultsAsString(report *sarif.Report, escape bool) (sarifStr string, err error) { - var out []byte - if escape { - out, err = json.Marshal(report) - } else { - out, err = JSONMarshalNotEscaped(report) - } - if err != nil { - return "", errorutils.CheckError(err) - } - return clientUtils.IndentJson(out), nil -} - -func JSONMarshalNotEscaped(t interface{}) ([]byte, error) { - buffer := &bytes.Buffer{} - encoder := json.NewEncoder(buffer) - encoder.SetEscapeHTML(false) - err := encoder.Encode(t) - return buffer.Bytes(), err -} - -func PrintJson(output interface{}) error { - results, err := JSONMarshalNotEscaped(output) - if err != nil { - return errorutils.CheckError(err) - } - log.Output(clientUtils.IndentJson(results)) - return nil -} - -func PrintSarif(results *Results, isMultipleRoots, includeLicenses bool, subScans []SubScanType) error { - sarifReport, err := GenerateSarifReportFromResults(results, isMultipleRoots, includeLicenses, nil, subScans) - if err != nil { - return err - } - sarifFile, err := WriteSarifResultsAsString(sarifReport, false) - if err != nil { - return err - } - log.Output(sarifFile) - return nil -} - -func CheckIfFailBuild(results []services.ScanResponse) bool { - for _, result := range results { - for _, violation := range result.Violations { - if violation.FailBuild { - return true - } - } - } - return false -} - -func IsEmptyScanResponse(results []services.ScanResponse) bool { - for _, result := range results { - if len(result.Violations) > 0 || len(result.Vulnerabilities) > 0 || len(result.Licenses) > 0 { - return false - } - } - return true -} - -func NewFailBuildError() error { - return coreutils.CliError{ExitCode: coreutils.ExitCodeVulnerableBuild, ErrorMsg: "One or more of the violations found are set to fail builds that include them"} -} - -func ToSummary(cmdResult *Results, includeVulnerabilities, includeViolations bool) (summary formats.ResultsSummary) { - if len(cmdResult.ScaResults) <= 1 { - summary.Scans = GetScanSummaryByTargets(cmdResult, includeVulnerabilities, includeViolations) - return - } - for _, scaScan := range cmdResult.ScaResults { - summary.Scans = append(summary.Scans, GetScanSummaryByTargets(cmdResult, includeVulnerabilities, includeViolations, scaScan.Target)...) - } - return -} - -func GetScanSummaryByTargets(r *Results, includeVulnerabilities, includeViolations bool, targets ...string) (summaries []formats.ScanSummary) { - if len(targets) == 0 { - // No filter, one scan summary for all targets - summaries = append(summaries, getScanSummary(includeVulnerabilities, includeViolations, r.ExtendedScanResults, r.ScaResults...)) - return - } - for _, target := range targets { - // Get target sca results - targetScaResults := []*ScaScanResult{} - if targetScaResult := r.getScaScanResultByTarget(target); targetScaResult != nil { - targetScaResults = append(targetScaResults, targetScaResult) - } - // Get target extended results - targetExtendedResults := r.ExtendedScanResults - if targetExtendedResults != nil { - targetExtendedResults = targetExtendedResults.GetResultsForTarget(target) - } - summaries = append(summaries, getScanSummary(includeVulnerabilities, includeViolations, targetExtendedResults, targetScaResults...)) - } - return -} - -func getScanSummary(includeVulnerabilities, includeViolations bool, extendedScanResults *ExtendedScanResults, scaResults ...*ScaScanResult) (summary formats.ScanSummary) { - if len(scaResults) == 1 { - summary.Target = scaResults[0].Target - } - if includeViolations { - summary.Violations = getScanViolationsSummary(extendedScanResults, scaResults...) - } - if includeVulnerabilities { - summary.Vulnerabilities = getScanSecurityVulnerabilitiesSummary(extendedScanResults, scaResults...) - } - return -} - -func getScanViolationsSummary(extendedScanResults *ExtendedScanResults, scaResults ...*ScaScanResult) (violations *formats.ScanViolationsSummary) { - watches := datastructures.MakeSet[string]() - parsed := datastructures.MakeSet[string]() - failBuild := false - scanIds := []string{} - moreInfoUrls := []string{} - violationsUniqueFindings := map[ViolationIssueType]formats.ResultSummary{} - // Parse unique findings - for _, scaResult := range scaResults { - for _, xrayResult := range scaResult.XrayResults { - if xrayResult.ScanId != "" { - scanIds = append(scanIds, xrayResult.ScanId) - } - if xrayResult.XrayDataUrl != "" { - moreInfoUrls = append(moreInfoUrls, xrayResult.XrayDataUrl) - } - for _, violation := range xrayResult.Violations { - watches.Add(violation.WatchName) - failBuild = failBuild || violation.FailBuild - key := violation.IssueId + violation.WatchName - if parsed.Exists(key) { - continue - } - parsed.Add(key) - severity := severityutils.GetSeverity(violation.Severity).String() - violationType := ViolationIssueType(violation.ViolationType) - if _, ok := violationsUniqueFindings[violationType]; !ok { - violationsUniqueFindings[violationType] = formats.ResultSummary{} - } - if _, ok := violationsUniqueFindings[violationType][severity]; !ok { - violationsUniqueFindings[violationType][severity] = map[string]int{} - } - if violationType == ViolationTypeSecurity { - applicableRuns := []*sarif.Run{} - if extendedScanResults != nil { - applicableRuns = append(applicableRuns, extendedScanResults.ApplicabilityScanResults...) - } - violationsUniqueFindings[violationType][severity] = mergeMaps(violationsUniqueFindings[violationType][severity], getSecuritySummaryFindings(violation.Cves, violation.IssueId, violation.Components, applicableRuns...)) - } else { - // License, Operational Risk - violationsUniqueFindings[violationType][severity][formats.NoStatus] += 1 - } - } - } - } - violations = &formats.ScanViolationsSummary{ - Watches: watches.ToSlice(), - FailBuild: failBuild, - ScanResultSummary: formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ - ScanIds: scanIds, - MoreInfoUrls: moreInfoUrls, - Security: violationsUniqueFindings[ViolationTypeSecurity], - License: violationsUniqueFindings[ViolationTypeLicense], - OperationalRisk: violationsUniqueFindings[ViolationTypeOperationalRisk], - }, - }} - return -} - -func getScanSecurityVulnerabilitiesSummary(extendedScanResults *ExtendedScanResults, scaResults ...*ScaScanResult) (vulnerabilities *formats.ScanResultSummary) { - vulnerabilities = &formats.ScanResultSummary{} - parsed := datastructures.MakeSet[string]() - for _, scaResult := range scaResults { - for _, xrayResult := range scaResult.XrayResults { - if vulnerabilities.ScaResults == nil { - vulnerabilities.ScaResults = &formats.ScaScanResultSummary{Security: formats.ResultSummary{}} - } - if xrayResult.ScanId != "" { - vulnerabilities.ScaResults.ScanIds = append(vulnerabilities.ScaResults.ScanIds, xrayResult.ScanId) - } - if xrayResult.XrayDataUrl != "" { - vulnerabilities.ScaResults.MoreInfoUrls = append(vulnerabilities.ScaResults.MoreInfoUrls, xrayResult.XrayDataUrl) - } - for _, vulnerability := range xrayResult.Vulnerabilities { - if parsed.Exists(vulnerability.IssueId) { - continue - } - parsed.Add(vulnerability.IssueId) - severity := severityutils.GetSeverity(vulnerability.Severity).String() - applicableRuns := []*sarif.Run{} - if extendedScanResults != nil { - applicableRuns = append(applicableRuns, extendedScanResults.ApplicabilityScanResults...) - } - vulnerabilities.ScaResults.Security[severity] = mergeMaps(vulnerabilities.ScaResults.Security[severity], getSecuritySummaryFindings(vulnerability.Cves, vulnerability.IssueId, vulnerability.Components, applicableRuns...)) - } - } - } - if extendedScanResults == nil { - return - } - vulnerabilities.IacResults = getJasSummaryFindings(extendedScanResults.IacScanResults...) - vulnerabilities.SecretsResults = getJasSummaryFindings(extendedScanResults.SecretsScanResults...) - vulnerabilities.SastResults = getJasSummaryFindings(extendedScanResults.SastScanResults...) - return -} - -func getSecuritySummaryFindings(cves []services.Cve, issueId string, components map[string]services.Component, applicableRuns ...*sarif.Run) map[string]int { - uniqueFindings := map[string]int{} - for _, cve := range cves { - applicableStatus := jasutils.NotScanned - if applicableInfo := getCveApplicabilityField(getCveId(cve, issueId), applicableRuns, components); applicableInfo != nil { - applicableStatus = jasutils.ConvertToApplicabilityStatus(applicableInfo.Status) - } - uniqueFindings[applicableStatus.String()] += 1 - } - if len(cves) == 0 { - // XRAY-ID, no scanners for them - status := jasutils.NotScanned - if len(applicableRuns) > 0 { - status = jasutils.NotCovered - } - uniqueFindings[status.String()] += 1 - } - return uniqueFindings -} - -func getCveId(cve services.Cve, defaultIssueId string) string { - if cve.Id == "" { - return defaultIssueId - } - return cve.Id -} - -func mergeMaps(m1, m2 map[string]int) map[string]int { - if m1 == nil { - return m2 - } - for k, v := range m2 { - m1[k] += v - } - return m1 -} - -func getJasSummaryFindings(runs ...*sarif.Run) *formats.ResultSummary { - if len(runs) == 0 { - return nil - } - summary := formats.ResultSummary{} - for _, run := range runs { - for _, result := range run.Results { - resultLevel := sarifutils.GetResultLevel(result) - severity, err := severityutils.ParseSeverity(resultLevel, true) - if err != nil { - log.Warn(fmt.Sprintf("Failed to parse Sarif level %s. %s", resultLevel, err.Error())) - severity = severityutils.Unknown - } - if _, ok := summary[severity.String()]; !ok { - summary[severity.String()] = map[string]int{} - } - summary[severity.String()][formats.NoStatus] += len(result.Locations) - } - } - return &summary -} diff --git a/utils/resultwriter_test.go b/utils/resultwriter_test.go deleted file mode 100644 index 1b948f9c..00000000 --- a/utils/resultwriter_test.go +++ /dev/null @@ -1,864 +0,0 @@ -package utils - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "testing" - - "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" - "github.com/jfrog/jfrog-cli-security/utils/jasutils" - "github.com/jfrog/jfrog-cli-security/utils/techutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - clientTests "github.com/jfrog/jfrog-client-go/utils/tests" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/owenrumney/go-sarif/v2/sarif" - "github.com/stretchr/testify/assert" -) - -func TestGetVulnerabilityOrViolationSarifHeadline(t *testing.T) { - assert.Equal(t, "[CVE-2022-1234] loadsh 1.4.1", getXrayIssueSarifHeadline("loadsh", "1.4.1", "CVE-2022-1234")) - assert.NotEqual(t, "[CVE-2022-1234] loadsh 1.4.1", getXrayIssueSarifHeadline("loadsh", "1.2.1", "CVE-2022-1234")) -} - -func TestGetIssueIdentifier(t *testing.T) { - issueId := "XRAY-123456" - cvesRow := []formats.CveRow{{Id: "CVE-2022-1234"}} - assert.Equal(t, "CVE-2022-1234", GetIssueIdentifier(cvesRow, issueId)) - cvesRow = append(cvesRow, formats.CveRow{Id: "CVE-2019-1234"}) - assert.Equal(t, "CVE-2022-1234, CVE-2019-1234", GetIssueIdentifier(cvesRow, issueId)) - assert.Equal(t, issueId, GetIssueIdentifier(nil, issueId)) -} - -func TestGetDirectDependenciesFormatted(t *testing.T) { - testCases := []struct { - name string - directDeps []formats.ComponentRow - expectedOutput string - }{ - { - name: "Single direct dependency", - directDeps: []formats.ComponentRow{ - {Name: "example-package", Version: "1.0.0"}, - }, - expectedOutput: "`example-package 1.0.0`", - }, - { - name: "Multiple direct dependencies", - directDeps: []formats.ComponentRow{ - {Name: "dependency1", Version: "1.0.0"}, - {Name: "dependency2", Version: "2.0.0"}, - }, - expectedOutput: "`dependency1 1.0.0`
`dependency2 2.0.0`", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output, err := getDirectDependenciesFormatted(tc.directDeps) - assert.NoError(t, err) - assert.Equal(t, tc.expectedOutput, output) - }) - } -} - -func TestGetSarifTableDescription(t *testing.T) { - testCases := []struct { - name string - formattedDeps string - maxCveScore string - status jasutils.ApplicabilityStatus - fixedVersions []string - expectedDescription string - }{ - { - name: "Applicable vulnerability", - formattedDeps: "`example-package 1.0.0`", - maxCveScore: "7.5", - status: "Applicable", - fixedVersions: []string{"1.0.1", "1.0.2"}, - expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.5 | Applicable | `example-package 1.0.0` | 1.0.1, 1.0.2 |", - }, - { - name: "Not-scanned vulnerability", - formattedDeps: "`example-package 2.0.0`", - maxCveScore: "6.2", - status: "", - fixedVersions: []string{"2.0.1"}, - expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 6.2 | `example-package 2.0.0` | 2.0.1 |", - }, - { - name: "No fixed versions", - formattedDeps: "`example-package 3.0.0`", - maxCveScore: "3.0", - status: "", - fixedVersions: []string{}, - expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 3.0 | `example-package 3.0.0` | No fix available |", - }, - { - name: "Not-covered vulnerability", - formattedDeps: "`example-package 3.0.0`", - maxCveScore: "3.0", - status: "Not covered", - fixedVersions: []string{"3.0.1"}, - expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Not covered | `example-package 3.0.0` | 3.0.1 |", - }, - { - name: "Undetermined vulnerability", - formattedDeps: "`example-package 3.0.0`", - maxCveScore: "3.0", - status: "Undetermined", - fixedVersions: []string{"3.0.1"}, - expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Undetermined | `example-package 3.0.0` | 3.0.1 |", - }, - { - name: "Not-status vulnerability", - formattedDeps: "`example-package 3.0.0`", - maxCveScore: "3.0", - status: "Not status", - fixedVersions: []string{"3.0.1"}, - expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Not status | `example-package 3.0.0` | 3.0.1 |", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output := getSarifTableDescription(tc.formattedDeps, tc.maxCveScore, tc.status.String(), tc.fixedVersions) - assert.Equal(t, tc.expectedDescription, output) - }) - } -} - -func TestFindMaxCVEScore(t *testing.T) { - testCases := []struct { - name string - cves []formats.CveRow - expectedOutput string - expectedError bool - }{ - { - name: "CVEScore with valid float values", - cves: []formats.CveRow{ - {Id: "CVE-2021-1234", CvssV3: "7.5"}, - {Id: "CVE-2021-5678", CvssV3: "9.2"}, - }, - expectedOutput: "9.2", - }, - { - name: "CVEScore with invalid float value", - cves: []formats.CveRow{ - {Id: "CVE-2022-4321", CvssV3: "invalid"}, - }, - expectedOutput: "", - expectedError: true, - }, - { - name: "CVEScore without values", - cves: []formats.CveRow{}, - expectedOutput: "0.0", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output, err := findMaxCVEScore(tc.cves) - assert.False(t, tc.expectedError && err == nil) - assert.Equal(t, tc.expectedOutput, output) - }) - } -} - -func TestGetXrayIssueLocationIfValidExists(t *testing.T) { - testDir, cleanup := tests.CreateTempDirWithCallbackAndAssert(t) - defer cleanup() - invocation := sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(testDir)) - file, err := os.Create(filepath.Join(testDir, "go.mod")) - assert.NoError(t, err) - assert.NotNil(t, file) - defer func() { assert.NoError(t, file.Close()) }() - file2, err := os.Create(filepath.Join(testDir, "build.gradle.kts")) - assert.NoError(t, err) - assert.NotNil(t, file2) - defer func() { assert.NoError(t, file2.Close()) }() - - testCases := []struct { - name string - tech techutils.Technology - run *sarif.Run - expectedOutput *sarif.Location - }{ - { - name: "No descriptor information", - tech: techutils.Pip, - run: sarifutils.CreateRunWithDummyResults().WithInvocations([]*sarif.Invocation{invocation}), - expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://Package-Descriptor"))), - }, - { - name: "One descriptor information", - tech: techutils.Go, - run: sarifutils.CreateRunWithDummyResults().WithInvocations([]*sarif.Invocation{invocation}), - expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + filepath.Join(testDir, "go.mod")))), - }, - { - name: "One descriptor information - no invocation", - tech: techutils.Go, - run: sarifutils.CreateRunWithDummyResults(), - expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://go.mod"))), - }, - { - name: "Multiple descriptor information", - tech: techutils.Gradle, - run: sarifutils.CreateRunWithDummyResults().WithInvocations([]*sarif.Invocation{invocation}), - expectedOutput: sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri("file://" + filepath.Join(testDir, "build.gradle.kts")))), - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output, err := getXrayIssueLocationIfValidExists(tc.tech, tc.run) - if assert.NoError(t, err) { - assert.Equal(t, tc.expectedOutput, output) - } - }) - } -} - -func TestConvertXrayScanToSimpleJson(t *testing.T) { - vulnerabilities := []services.Vulnerability{ - { - IssueId: "XRAY-1", - Summary: "summary-1", - Severity: "high", - Components: map[string]services.Component{"component-A": {}, "component-B": {}}, - }, - { - IssueId: "XRAY-2", - Summary: "summary-2", - Severity: "low", - Components: map[string]services.Component{"component-B": {}}, - }, - } - expectedVulnerabilities := []formats.VulnerabilityOrViolationRow{ - { - Summary: "summary-1", - IssueId: "XRAY-1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, - ImpactedDependencyName: "component-A", - }, - }, - { - Summary: "summary-1", - IssueId: "XRAY-1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, - ImpactedDependencyName: "component-B", - }, - }, - { - Summary: "summary-2", - IssueId: "XRAY-2", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 10}, - ImpactedDependencyName: "component-B", - }, - }, - } - - violations := []services.Violation{ - { - IssueId: "XRAY-1", - Summary: "summary-1", - Severity: "high", - WatchName: "watch-1", - ViolationType: "security", - Components: map[string]services.Component{"component-A": {}, "component-B": {}}, - }, - { - IssueId: "XRAY-2", - Summary: "summary-2", - Severity: "low", - WatchName: "watch-1", - ViolationType: "license", - LicenseKey: "license-1", - Components: map[string]services.Component{"component-B": {}}, - }, - } - expectedSecViolations := []formats.VulnerabilityOrViolationRow{ - { - Summary: "summary-1", - IssueId: "XRAY-1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, - ImpactedDependencyName: "component-A", - }, - }, - { - Summary: "summary-1", - IssueId: "XRAY-1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 18}, - ImpactedDependencyName: "component-B", - }, - }, - } - expectedLicViolations := []formats.LicenseRow{ - { - LicenseKey: "license-1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 10}, - ImpactedDependencyName: "component-B", - }, - }, - } - - licenses := []services.License{ - { - Key: "license-1", - Name: "license-1-name", - Components: map[string]services.Component{"component-A": {}, "component-B": {}}, - }, - { - Key: "license-2", - Name: "license-2-name", - Components: map[string]services.Component{"component-B": {}}, - }, - } - expectedLicenses := []formats.LicenseRow{ - { - LicenseKey: "license-1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-A"}, - }, - { - LicenseKey: "license-1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-B"}, - }, - { - LicenseKey: "license-2", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-B"}, - }, - } - - testCases := []struct { - name string - result services.ScanResponse - includeLicenses bool - allowedLicenses []string - expectedOutput formats.SimpleJsonResults - }{ - { - name: "Vulnerabilities only", - includeLicenses: false, - allowedLicenses: nil, - result: services.ScanResponse{Vulnerabilities: vulnerabilities, Licenses: licenses}, - expectedOutput: formats.SimpleJsonResults{Vulnerabilities: expectedVulnerabilities}, - }, - { - name: "Vulnerabilities with licenses", - includeLicenses: true, - allowedLicenses: nil, - result: services.ScanResponse{Vulnerabilities: vulnerabilities, Licenses: licenses}, - expectedOutput: formats.SimpleJsonResults{Vulnerabilities: expectedVulnerabilities, Licenses: expectedLicenses}, - }, - { - name: "Vulnerabilities only - with allowed licenses", - includeLicenses: false, - allowedLicenses: []string{"license-1"}, - result: services.ScanResponse{Vulnerabilities: vulnerabilities, Licenses: licenses}, - expectedOutput: formats.SimpleJsonResults{ - Vulnerabilities: expectedVulnerabilities, - LicensesViolations: []formats.LicenseRow{ - { - LicenseKey: "license-2", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "component-B"}, - }, - }, - }, - }, - { - name: "Violations only", - includeLicenses: false, - allowedLicenses: nil, - result: services.ScanResponse{Violations: violations, Licenses: licenses}, - expectedOutput: formats.SimpleJsonResults{SecurityViolations: expectedSecViolations, LicensesViolations: expectedLicViolations}, - }, - { - name: "Violations - override allowed licenses", - includeLicenses: false, - allowedLicenses: []string{"license-1"}, - result: services.ScanResponse{Violations: violations, Licenses: licenses}, - expectedOutput: formats.SimpleJsonResults{SecurityViolations: expectedSecViolations, LicensesViolations: expectedLicViolations}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - results := NewAuditResults(SourceCode) - scaScanResult := ScaScanResult{XrayResults: []services.ScanResponse{tc.result}} - results.ScaResults = append(results.ScaResults, &scaScanResult) - output, err := ConvertXrayScanToSimpleJson(results, false, tc.includeLicenses, true, tc.allowedLicenses) - if assert.NoError(t, err) { - assert.ElementsMatch(t, tc.expectedOutput.Vulnerabilities, output.Vulnerabilities) - assert.ElementsMatch(t, tc.expectedOutput.SecurityViolations, output.SecurityViolations) - assert.ElementsMatch(t, tc.expectedOutput.LicensesViolations, output.LicensesViolations) - assert.ElementsMatch(t, tc.expectedOutput.Licenses, output.Licenses) - assert.ElementsMatch(t, tc.expectedOutput.OperationalRiskViolations, output.OperationalRiskViolations) - } - }) - } -} - -func TestJSONMarshall(t *testing.T) { - testCases := []struct { - testName string - resultString string - expectedResult string - }{ - { - testName: "Regular URL", - resultString: "http://my-artifactory.jfrog.io/", - expectedResult: "\"http://my-artifactory.jfrog.io/\"\n", - }, - { - testName: "Regular CVE", - resultString: "CVE-2021-4104", - expectedResult: "\"CVE-2021-4104\"\n", - }, - { - testName: "URL with escape characters ignore rules", - resultString: "https://my-artifactory.jfrog.com/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=1babb2d0-42c0-4389-7770-18a6cab8d9a7\u0026issue_id=XRAY-590941\u0026on_demand_scanning=true\u0026show_popup=true\u0026type=security\u0026watch_name=my-watch", - expectedResult: "\"https://my-artifactory.jfrog.com/ui/admin/xray/policiesGovernance/ignore-rules?graph_scan_id=1babb2d0-42c0-4389-7770-18a6cab8d9a7&issue_id=XRAY-590941&on_demand_scanning=true&show_popup=true&type=security&watch_name=my-watch\"\n", - }, - { - testName: "URL with escape characters build scan data", - resultString: "https://my-artifactory.jfrog.com/ui/scans-list/builds-scans/dort1/scan-descendants/1?version=1\u0026package_id=build%3A%2F%2Fdort1\u0026build_repository=artifactory-build-info\u0026component_id=build%3A%2F%2Fshweta1%3A1\u0026page_type=security-vulnerabilities\u0026exposure_status=to_fix", - expectedResult: "\"https://my-artifactory.jfrog.com/ui/scans-list/builds-scans/dort1/scan-descendants/1?version=1&package_id=build%3A%2F%2Fdort1&build_repository=artifactory-build-info&component_id=build%3A%2F%2Fshweta1%3A1&page_type=security-vulnerabilities&exposure_status=to_fix\"\n", - }, - } - - for _, tc := range testCases { - t.Run(tc.testName, func(t *testing.T) { - printedString, err := JSONMarshalNotEscaped(tc.resultString) - assert.NoError(t, err) - assert.Equal(t, tc.expectedResult, string(printedString)) - }) - } -} - -func TestGetSummary(t *testing.T) { - dummyExtendedScanResults := &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyPassingResult("applic_CVE-2")).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), - }), - }, - SecretsScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target1/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), - }), - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target2/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target2")), - }), - }, - SastScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target1/file2", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), - }), - }, - } - - expectedVulnerabilities := &formats.ScanResultSummary{ - ScaResults: &formats.ScaScanResultSummary{ - ScanIds: []string{TestScaScanId}, - MoreInfoUrls: []string{TestMoreInfoUrl}, - Security: formats.ResultSummary{ - "Critical": map[string]int{jasutils.ApplicabilityUndetermined.String(): 1}, - "High": map[string]int{jasutils.NotApplicable.String(): 1}, - }, - }, - SecretsResults: &formats.ResultSummary{"Low": map[string]int{jasutils.NotScanned.String(): 2}}, - SastResults: &formats.ResultSummary{"Low": map[string]int{jasutils.NotScanned.String(): 1}}, - } - expectedViolations := &formats.ScanViolationsSummary{ - Watches: []string{"test-watch-name", "test-watch-name2"}, - FailBuild: true, - ScanResultSummary: formats.ScanResultSummary{ - ScaResults: &formats.ScaScanResultSummary{ - ScanIds: []string{TestScaScanId}, - MoreInfoUrls: []string{TestMoreInfoUrl}, - Security: formats.ResultSummary{ - "Critical": map[string]int{jasutils.ApplicabilityUndetermined.String(): 1}, - "High": map[string]int{jasutils.NotApplicable.String(): 1}, - }, - License: formats.ResultSummary{"High": map[string]int{jasutils.NotScanned.String(): 1}}, - }, - }, - } - - testCases := []struct { - name string - results *Results - includeVulnerabilities bool - includeViolations bool - expected formats.ResultsSummary - }{ - { - name: "Vulnerabilities only", - includeVulnerabilities: true, - results: &Results{ - ScaResults: []*ScaScanResult{{ - Target: "target1", - XrayResults: getDummyScaTestResults(true, true), - }}, - ExtendedScanResults: dummyExtendedScanResults, - }, - expected: formats.ResultsSummary{ - Scans: []formats.ScanSummary{{ - Target: "target1", - Vulnerabilities: expectedVulnerabilities, - }}, - }, - }, - { - name: "Violations only", - includeViolations: true, - results: &Results{ - ScaResults: []*ScaScanResult{{ - Target: "target1", - XrayResults: getDummyScaTestResults(true, true), - }}, - ExtendedScanResults: dummyExtendedScanResults, - }, - expected: formats.ResultsSummary{ - Scans: []formats.ScanSummary{{ - Target: "target1", - Violations: expectedViolations, - }}, - }, - }, - { - name: "Vulnerabilities and Violations", - includeVulnerabilities: true, - includeViolations: true, - results: &Results{ - ScaResults: []*ScaScanResult{{ - Target: "target1", - XrayResults: getDummyScaTestResults(true, true), - }}, - ExtendedScanResults: dummyExtendedScanResults, - }, - expected: formats.ResultsSummary{ - Scans: []formats.ScanSummary{{ - Target: "target1", - Violations: expectedViolations, - Vulnerabilities: expectedVulnerabilities, - }}, - }, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - summary := ToSummary(testCase.results, testCase.includeVulnerabilities, testCase.includeViolations) - for _, scan := range summary.Scans { - if scan.Vulnerabilities != nil { - sort.Strings(scan.Vulnerabilities.ScaResults.ScanIds) - sort.Strings(scan.Vulnerabilities.ScaResults.MoreInfoUrls) - } - if scan.Violations != nil { - sort.Strings(scan.Violations.Watches) - sort.Strings(scan.Violations.ScanResultSummary.ScaResults.ScanIds) - sort.Strings(scan.Violations.ScanResultSummary.ScaResults.MoreInfoUrls) - } - } - assert.Equal(t, testCase.expected, summary) - }) - } -} - -func TestGetLayerContentFromComponentId(t *testing.T) { - testCases := []struct { - name string - path string - expectedAlgorithm string - expectedLayerHash string - }{ - { - name: "Valid path", - path: "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", - expectedAlgorithm: "sha256", - expectedLayerHash: "cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", - }, - { - name: "Invalid path - not hex", - path: "sha256__NOT_HEX.tar", - }, - { - name: "Invalid path - no algorithm", - path: "_cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e.tar", - }, - { - name: "Invalid path - no suffix", - path: "sha256__cedb364ef937c7e51179d8e514bdd98644bac5fdc82a45d784ef91afe4bc647e", - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - algorithm, layerHash := getLayerContentFromComponentId(tc.path) - assert.Equal(t, tc.expectedAlgorithm, algorithm) - assert.Equal(t, tc.expectedLayerHash, layerHash) - }) - } -} - -func preparePatchTestEnv(t *testing.T) (string, string, func()) { - currentWd, err := os.Getwd() - assert.NoError(t, err) - wd, cleanUpTempDir := tests.CreateTempDirWithCallbackAndAssert(t) - cleanUpWd := clientTests.ChangeDirWithCallback(t, currentWd, wd) - dockerfileDir := filepath.Join(wd, "DockerfileDir") - err = fileutils.CreateDirIfNotExist(dockerfileDir) - // Prepare env content - assert.NoError(t, err) - createDummyDockerfile(t, dockerfileDir) - createDummyGithubWorkflow(t, dockerfileDir) - createDummyGithubWorkflow(t, wd) - return wd, dockerfileDir, func() { - cleanUpWd() - cleanUpTempDir() - } -} - -func createDummyGithubWorkflow(t *testing.T, baseDir string) { - assert.NoError(t, fileutils.CreateDirIfNotExist(filepath.Join(baseDir, GithubBaseWorkflowDir))) - assert.NoError(t, os.WriteFile(filepath.Join(baseDir, GithubBaseWorkflowDir, "workflowFile.yml"), []byte("workflow name"), 0644)) -} - -func createDummyDockerfile(t *testing.T, baseDir string) { - assert.NoError(t, os.WriteFile(filepath.Join(baseDir, "Dockerfile"), []byte("Dockerfile data"), 0644)) -} - -func TestPatchRunsToPassIngestionRules(t *testing.T) { - wd, dockerfileDir, cleanUp := preparePatchTestEnv(t) - defer cleanUp() - - testCases := []struct { - name string - cmdResult *Results - subScan SubScanType - withEnvVars bool - withDockerfile bool - input []*sarif.Run - expectedResults []*sarif.Run - }{ - { - name: "No runs", - cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, - subScan: SecretsScan, - input: []*sarif.Run{}, - expectedResults: []*sarif.Run{}, - }, - { - name: "Build scan - SCA", - cmdResult: &Results{ResultType: Build, ScaResults: []*ScaScanResult{{Name: "buildName (buildNumber)"}}}, - subScan: ScaScan, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file")))), - }, - expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file"))), - }, - }, - { - name: "Docker image scan - SCA", - cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, - subScan: ScaScan, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), []string{"applicability"}, []string{"applicable"}). - WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), - }, - ), - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), - ), - }, - expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties(sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", - sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), - ), []string{"applicability"}, []string{"applicable"}, - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), - }), - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", - sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), - ), - ), - }, - }, - { - name: "Docker image scan - with env vars", - cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, - subScan: ScaScan, - withEnvVars: true, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), - // No location, should be removed in the output - sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), - ), - }, - expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "eda26ae830c578197aeda65a82d7f093", - sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( - sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation(filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml"))), - ), - ), - ), - }, - }, - { - name: "Docker image scan - with Dockerfile in wd", - cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, - subScan: ScaScan, - withEnvVars: true, - withDockerfile: true, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(dockerfileDir, - sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), - ), - }, - expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(dockerfileDir, - sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "8cbd7268a4d20f2358ba2667ebd18956", - sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( - sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation("Dockerfile")), - ), - ), - ), - }, - }, - { - name: "Docker image scan - Secrets", - cmdResult: &Results{ResultType: DockerImage, ScaResults: []*ScaScanResult{{Name: "dockerImage:imageVersion"}}}, - subScan: SecretsScan, - input: []*sarif.Run{ - sarifutils.CreateRunNameWithResults("some tool name", - sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "unpacked", "filesystem", "blobs", "sha1", "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "usr", "src", "app", "server", "index.js"))), - ).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), - }), - }, - expectedResults: []*sarif.Run{ - { - Tool: sarif.Tool{ - Driver: sarifutils.CreateDummyDriver(patchedBinarySecretScannerToolName, "", &sarif.ReportingDescriptor{ - ID: "rule", - ShortDescription: sarif.NewMultiformatMessageString("[Secret in Binary found] "), - }), - }, - Invocations: []*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}, - Results: []*sarif.Result{ - sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("🔒 Found Secrets in Binary docker scanning:\nImage: dockerImage:imageVersion\nLayer (sha1): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: %s\nEvidence: snippet", filepath.Join("usr", "src", "app", "server", "index.js")), "", jfrogFingerprintAlgorithmName, "dee156c9fd75a4237102dc8fb29277a2", - sarifutils.CreateDummyLocationWithPathAndLogicalLocation(filepath.Join("usr", "src", "app", "server", "index.js"), "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "layer", "algorithm", "sha1"), - ), - }, - }, - }, - }, - { - name: "Binary scan - SCA", - cmdResult: &Results{ResultType: Binary, ScaResults: []*ScaScanResult{{Target: filepath.Join(wd, "dir", "binary")}}}, - subScan: ScaScan, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "binary"))), - ), - }, - expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultWithFingerprint("", "", jfrogFingerprintAlgorithmName, "e72a936dc73acbc4283a93230ff9b6e8", sarifutils.CreateDummyLocationInPath(filepath.Join("dir", "binary"))), - ), - }, - }, - { - name: "Audit scan - SCA", - cmdResult: &Results{ResultType: SourceCode, ScaResults: []*ScaScanResult{{Target: wd}}}, - subScan: ScaScan, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultInPath(filepath.Join(wd, "Package-Descriptor")), - // No location, should be removed in the output - sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), - ), - }, - expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultInPath("Package-Descriptor"), - ), - }, - }, - { - name: "Audit scan - Secrets", - cmdResult: &Results{ResultType: SourceCode, ScaResults: []*ScaScanResult{{Target: wd}}}, - subScan: SecretsScan, - input: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file"))), - // No location, should be removed in the output - sarifutils.CreateDummyResult("some-markdown", "some-other-msg", "rule", "level"), - ), - }, - expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file")), - ), - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.withEnvVars { - cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "workflow name") - defer cleanFileEnv() - cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "123") - defer cleanRunNumEnv() - } else { - // Since the the env are provided by the - cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "") - defer cleanFileEnv() - cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "") - defer cleanRunNumEnv() - } - if tc.withDockerfile { - revertWd := clientTests.ChangeDirWithCallback(t, wd, dockerfileDir) - defer revertWd() - } - patchRunsToPassIngestionRules(tc.subScan, tc.cmdResult, tc.input...) - assert.ElementsMatch(t, tc.expectedResults, tc.input) - }) - } -} - -func getDummyScaTestResults(vulnerability, violation bool) (responses []services.ScanResponse) { - response := services.ScanResponse{} - if vulnerability { - response.Vulnerabilities = []services.Vulnerability{ - {IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - {IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - } - } - if violation { - response.Violations = []services.Violation{ - {ViolationType: ViolationTypeSecurity.String(), WatchName: "test-watch-name", IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - {ViolationType: ViolationTypeSecurity.String(), FailBuild: true, WatchName: "test-watch-name", IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - {ViolationType: ViolationTypeLicense.String(), WatchName: "test-watch-name2", IssueId: "MIT", Severity: "High", LicenseKey: "MIT", Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - } - } - response.ScanId = TestScaScanId - response.XrayDataUrl = TestMoreInfoUrl - responses = append(responses, response) - return -} diff --git a/utils/severityutils/severity.go b/utils/severityutils/severity.go index f33bf31c..e612fccd 100644 --- a/utils/severityutils/severity.go +++ b/utils/severityutils/severity.go @@ -7,7 +7,7 @@ import ( "github.com/gookit/color" "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/formats" + "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "golang.org/x/text/cases" @@ -15,8 +15,8 @@ import ( ) const ( - MinCveScore = 0.0 - MaxCveScore = 10.0 + MinCveScore float32 = 0.0 + MaxCveScore float32 = 10.0 // When parsing Sarif level to severity, // If the level is not provided, the value is defaulted to be 'Medium' SeverityDefaultValue = Medium @@ -228,14 +228,6 @@ func ParseForDetails(severity string, sarifSeverity bool, applicabilityStatus ja return } -func ParseToSeverityDetails(severity string, sarifSeverity, pretty bool, applicabilityStatus jasutils.ApplicabilityStatus) (out formats.SeverityDetails, err error) { - parsed, err := ParseSeverity(severity, sarifSeverity) - if err != nil { - return - } - return GetSeverityDetails(parsed, applicabilityStatus).ToDetails(parsed, pretty), nil -} - // -- Getters functions (With default values) -- func GetAsDetails(severity Severity, applicabilityStatus jasutils.ApplicabilityStatus, pretty bool) formats.SeverityDetails { @@ -247,6 +239,9 @@ func GetAsDetails(severity Severity, applicabilityStatus jasutils.ApplicabilityS } func GetSeverityDetails(severity Severity, applicabilityStatus jasutils.ApplicabilityStatus) *SeverityDetails { + if applicabilityStatus == jasutils.NotScanned { + applicabilityStatus = jasutils.Applicable + } details, err := ParseForDetails(severity.String(), false, applicabilityStatus) if err != nil { return &SeverityDetails{Priority: 0, Score: 0} diff --git a/utils/techutils/techutils.go b/utils/techutils/techutils.go index 4710aa76..8aa8a89b 100644 --- a/utils/techutils/techutils.go +++ b/utils/techutils/techutils.go @@ -69,9 +69,26 @@ var TechToProjectType = map[Technology]project.ProjectType{ Dotnet: project.Dotnet, } +var packageTypes = map[string]string{ + "gav": "Maven", + "docker": "Docker", + "rpm": "RPM", + "deb": "Debian", + "nuget": "NuGet", + "generic": "Generic", + "npm": "npm", + "pip": "Python", + "pypi": "Python", + "composer": "Composer", + "go": "Go", + "alpine": "Alpine", +} + type TechData struct { // The name of the package type used in this technology. packageType string + // The package type ID used in Xray. + packageTypeId string // Suffixes of file/directory names that indicate if a project uses this technology. // The name of at least one of the files/directories in the project's directory must end with one of these suffixes. indicators []string @@ -118,6 +135,7 @@ var technologiesData = map[Technology]TechData{ exclude: []string{".yarnrc.yml", "yarn.lock", ".yarn"}, packageDescriptors: []string{"package.json"}, packageVersionOperator: "@", + packageTypeId: "npm://", packageInstallationCommand: "update", }, Yarn: { @@ -245,6 +263,13 @@ func (tech Technology) GetPackageType() string { return technologiesData[tech].packageType } +func (tech Technology) GetPackageTypeId() string { + if technologiesData[tech].packageTypeId == "" { + return fmt.Sprintf("%s://", tech.GetPackageType()) + } + return technologiesData[tech].packageTypeId +} + func (tech Technology) GetPackageDescriptor() []string { return technologiesData[tech].packageDescriptors } @@ -538,3 +563,68 @@ func GetAllTechnologiesList() (technologies []Technology) { } return } + +// SplitComponentId splits a Xray component ID to the component name, version and package type. +// In case componentId doesn't contain a version, the returned version will be an empty string. +// In case componentId's format is invalid, it will be returned as the component name +// and empty strings will be returned instead of the version and the package type. +// Examples: +// 1. componentId: "gav://antparent:ant:1.6.5" +// Returned values: +// Component name: "antparent:ant" +// Component version: "1.6.5" +// Package type: "Maven" +// 2. componentId: "generic://sha256:244fd47e07d1004f0aed9c156aa09083c82bf8944eceb67c946ff7430510a77b/foo.jar" +// Returned values: +// Component name: "foo.jar" +// Component version: "" +// Package type: "Generic" +// 3. componentId: "invalid-comp-id" +// Returned values: +// Component name: "invalid-comp-id" +// Component version: "" +// Package type: "" +func SplitComponentId(componentId string) (string, string, string) { + compIdParts := strings.Split(componentId, "://") + // Invalid component ID + if len(compIdParts) != 2 { + return componentId, "", "" + } + + packageType := compIdParts[0] + packageId := compIdParts[1] + + // Generic identifier structure: generic://sha256:/name + if packageType == "generic" { + lastSlashIndex := strings.LastIndex(packageId, "/") + return packageId[lastSlashIndex+1:], "", packageTypes[packageType] + } + + var compName, compVersion string + switch packageType { + case "rpm": + // RPM identifier structure: rpm://os-version:package:epoch-version:version + // os-version is optional. + splitCompId := strings.Split(packageId, ":") + if len(splitCompId) >= 3 { + compName = splitCompId[len(splitCompId)-3] + compVersion = fmt.Sprintf("%s:%s", splitCompId[len(splitCompId)-2], splitCompId[len(splitCompId)-1]) + } + default: + // All other identifiers look like this: package-type://package-name:version. + // Sometimes there's a namespace or a group before the package name, separated by a '/' or a ':'. + lastColonIndex := strings.LastIndex(packageId, ":") + + if lastColonIndex != -1 { + compName = packageId[:lastColonIndex] + compVersion = packageId[lastColonIndex+1:] + } + } + + // If there's an error while parsing the component ID + if compName == "" { + compName = packageId + } + + return compName, compVersion, packageTypes[packageType] +} diff --git a/utils/utils.go b/utils/utils.go index c429f632..4832daed 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,19 +1,31 @@ package utils import ( + "bytes" "crypto" "encoding/hex" + "encoding/json" "fmt" - "github.com/jfrog/jfrog-client-go/utils/log" "os" "path/filepath" "strings" + + "github.com/jfrog/jfrog-client-go/utils/log" + "golang.org/x/exp/slices" + "time" + + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" ) const ( - NodeModulesPattern = "**/*node_modules*/**" - JfMsiEnvVariable = "JF_MSI" + NodeModulesPattern = "**/*node_modules*/**" + JfMsiEnvVariable = "JF_MSI" + + BaseDocumentationURL = "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/" + JasInfoURL = "https://jfrog.com/xray/" EntitlementsMinVersion = "3.66.5" ) @@ -67,6 +79,89 @@ func GetAllSupportedScans() []SubScanType { return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan, SecretTokenValidationScan} } +// IsScanRequested returns true if the scan is requested, otherwise false. If requestedScans is empty, all scans are considered requested. +func IsScanRequested(cmdType CommandType, subScan SubScanType, requestedScans ...SubScanType) bool { + if cmdType.IsTargetBinary() && (subScan == IacScan || subScan == SastScan) { + return false + } + return len(requestedScans) == 0 || slices.Contains(requestedScans, subScan) +} + +func IsCI() bool { + return strings.ToLower(os.Getenv(coreutils.CI)) == "true" +} + +// UniqueIntersection returns a new slice of strings that contains elements from both input slices without duplicates +func UniqueIntersection[T comparable](arr []T, others ...T) []T { + uniqueSet := datastructures.MakeSetFromElements(arr...) + uniqueIntersection := datastructures.MakeSet[T]() + for _, other := range others { + if exist := uniqueSet.Exists(other); exist { + uniqueIntersection.Add(other) + } + } + return uniqueIntersection.ToSlice() +} + +// UniqueUnion returns a new slice of strings that contains elements from the input slice and the elements provided without duplicates +func UniqueUnion[T comparable](arr []T, elements ...T) []T { + uniqueSet := datastructures.MakeSetFromElements(arr...) + uniqueSet.AddElements(elements...) + return uniqueSet.ToSlice() +} + +func GetAsJsonBytes(output interface{}, escapeValues, indent bool) (results []byte, err error) { + if escapeValues { + if results, err = json.Marshal(output); errorutils.CheckError(err) != nil { + return + } + } else { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + if err = encoder.Encode(output); err != nil { + return + } + results = buffer.Bytes() + } + if indent { + return doIndent(results) + } + return +} + +func doIndent(bytesRes []byte) ([]byte, error) { + var content bytes.Buffer + if err := json.Indent(&content, bytesRes, "", " "); errorutils.CheckError(err) != nil { + return content.Bytes(), err + } + return content.Bytes(), nil +} + +func GetAsJsonString(output interface{}, escapeValues, indent bool) (string, error) { + results, err := GetAsJsonBytes(output, escapeValues, indent) + if err != nil { + return "", err + } + return string(results), nil +} + +func NewBoolPtr(v bool) *bool { + return &v +} + +func NewIntPtr(v int) *int { + return &v +} + +func NewInt64Ptr(v int64) *int64 { + return &v +} + +func NewFloat64Ptr(v float64) *float64 { + return &v +} + func Md5Hash(values ...string) (string, error) { return toHash(crypto.MD5, values...) } diff --git a/utils/utils_test.go b/utils/utils_test.go index a7e0c1b6..7629efe1 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -6,6 +6,72 @@ import ( "github.com/stretchr/testify/assert" ) +func TestUniqueIntersection(t *testing.T) { + testCases := []struct { + name string + slice1 []string + slice2 []string + expected []string + }{ + { + name: "Empty", + slice1: []string{}, + slice2: []string{}, + expected: []string{}, + }, + { + name: "One element", + slice1: []string{"element1"}, + slice2: []string{"element1"}, + expected: []string{"element1"}, + }, + { + name: "Two elements", + slice1: []string{"element1", "element2"}, + slice2: []string{"element2", "element3"}, + expected: []string{"element2"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.ElementsMatch(t, tc.expected, UniqueIntersection(tc.slice1, tc.slice2...)) + }) + } +} + +func TestUniqueUnion(t *testing.T) { + testCases := []struct { + name string + slice []string + elements []string + expected []string + }{ + { + name: "Empty", + slice: []string{}, + elements: []string{}, + expected: []string{}, + }, + { + name: "One element", + slice: []string{"element1"}, + elements: []string{"element1"}, + expected: []string{"element1"}, + }, + { + name: "Two elements", + slice: []string{"element1", "element2"}, + elements: []string{"element2", "element3"}, + expected: []string{"element1", "element2", "element3"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.ElementsMatch(t, tc.expected, UniqueUnion(tc.slice, tc.elements...)) + }) + } +} + func TestToCommandEnvVars(t *testing.T) { testCases := []struct { name string diff --git a/utils/test_mocks.go b/utils/validations/test_mocks.go similarity index 91% rename from utils/test_mocks.go rename to utils/validations/test_mocks.go index 0588b284..04b66ad4 100644 --- a/utils/test_mocks.go +++ b/utils/validations/test_mocks.go @@ -1,4 +1,4 @@ -package utils +package validations import ( "fmt" @@ -14,11 +14,18 @@ import ( ) const ( - TestMsi = "27e175b8-e525-11ee-842b-7aa2c69b8f1f" - TestScaScanId = "3d90ec4b-cf33-4846-6831-4bf9576f2235" - TestMoreInfoUrl = "https://www.jfrog.com" + TestMsi = "27e175b8-e525-11ee-842b-7aa2c69b8f1f" + TestScaScanId = "3d90ec4b-cf33-4846-6831-4bf9576f2235" + + // TestMoreInfoUrl = "https://www.jfrog.com" + TestPlatformUrl = "https://test-platform-url.jfrog.io/" + TestMoreInfoUrl = "https://test-more-info-url.jfrog.io/" + TestConfigProfileName = "default-profile" - versionApiUrl = "/%s/api/v1/system/version" +) + +var ( + versionApiUrl = "/%s/api/v1/system/version" ) type restsTestHandler func(w http.ResponseWriter, r *http.Request) diff --git a/utils/validations/test_validate_sarif.go b/utils/validations/test_validate_sarif.go new file mode 100644 index 00000000..8c90481d --- /dev/null +++ b/utils/validations/test_validate_sarif.go @@ -0,0 +1,273 @@ +package validations + +import ( + "fmt" + "strings" + "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-cli-security/utils/results/conversion/sarifparser" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/stretchr/testify/assert" +) + +const ( + SastToolName = "USAF" + IacToolName = "JFrog Terraform scanner" + // #nosec G101 -- Not credentials. + SecretsToolName = "JFrog Secrets scanner" +) + +// Validate sarif report according to the expected values and issue counts in the validation params. +// Value/Actual content should be a *sarif.Report in the validation params +// If ExactResultsMatch is true, the validation will check exact values and not only the 'equal or grater' counts / existence of expected attributes. +// For Integration tests with JFrog API, ExactResultsMatch should be set to false. +func ValidateCommandSarifOutput(t *testing.T, params ValidationParams) { + results, ok := params.Actual.(*sarif.Report) + if assert.True(t, ok, "Actual content is not a *sarif.Report") { + ValidateSarifIssuesCount(t, params, results) + if params.Expected != nil { + expectedResults, ok := params.Expected.(*sarif.Report) + if assert.True(t, ok, "Expected content is not a *sarif.Report") { + ValidateSarifReport(t, params.ExactResultsMatch, expectedResults, results) + } + } + } +} + +// Validate sarif report according to the expected counts in the validation params. +// Actual content should be a *sarif.Report in the validation params. +// If Expected is provided, the validation will check if the Actual content matches the expected results. +// If ExactResultsMatch is true, the validation will check exact values and not only the 'equal or grater' counts / existence of expected attributes. (For Integration tests with JFrog API, ExactResultsMatch should be set to false) +func ValidateSarifIssuesCount(t *testing.T, params ValidationParams, report *sarif.Report) { + var vulnerabilities, securityViolations, licenseViolations, applicableResults, undeterminedResults, notCoveredResults, notApplicableResults, missingContextResults, inactiveResults int + + iac := sarifutils.GetResultsLocationCount(sarifutils.GetRunsByToolName(report, IacToolName)...) + vulnerabilities += iac + secrets := sarifutils.GetResultsLocationCount(sarifutils.GetRunsByToolName(report, SecretsToolName)...) + secrets += sarifutils.GetResultsLocationCount(sarifutils.GetRunsByToolName(report, sarifparser.BinarySecretScannerToolName)...) + vulnerabilities += secrets + sast := sarifutils.GetResultsLocationCount(sarifutils.GetRunsByToolName(report, SastToolName)...) + vulnerabilities += sast + + scaRuns := sarifutils.GetRunsByToolName(report, sarifparser.ScaScannerToolName) + for _, run := range scaRuns { + for _, result := range run.Results { + // If watch property exists, add to security violations or license violations else add to vulnerabilities + if _, ok := result.Properties[sarifparser.WatchSarifPropertyKey]; ok { + if isSecurityIssue(result) { + securityViolations++ + } else { + licenseViolations++ + } + continue + } + vulnerabilities++ + // Get the applicability status in the result properties (convert to string) and add count to the appropriate category + applicabilityProperty := result.Properties[jasutils.ApplicabilitySarifPropertyKey] + if applicability, ok := applicabilityProperty.(string); ok { + switch applicability { + case jasutils.Applicable.String(): + applicableResults++ + case jasutils.NotApplicable.String(): + notApplicableResults++ + case jasutils.ApplicabilityUndetermined.String(): + undeterminedResults++ + case jasutils.NotCovered.String(): + notCoveredResults++ + case jasutils.MissingContext.String(): + missingContextResults++ + } + } + if tokenStatus := results.GetResultPropertyTokenValidation(result); tokenStatus == jasutils.Inactive.ToString() { + inactiveResults++ + } + } + } + + ValidateContent(t, params.ExactResultsMatch, + CountValidation[int]{Expected: params.Sast, Actual: sast, Msg: GetValidationCountErrMsg("sast", "sarif report", params.ExactResultsMatch, params.Sast, sast)}, + CountValidation[int]{Expected: params.Iac, Actual: iac, Msg: GetValidationCountErrMsg("Iac", "sarif report", params.ExactResultsMatch, params.Iac, iac)}, + CountValidation[int]{Expected: params.Secrets, Actual: secrets, Msg: GetValidationCountErrMsg("secrets", "sarif report", params.ExactResultsMatch, params.Secrets, secrets)}, + CountValidation[int]{Expected: params.Inactive, Actual: inactiveResults, Msg: GetValidationCountErrMsg("inactive secret results", "sarif report", params.ExactResultsMatch, params.Inactive, inactiveResults)}, + CountValidation[int]{Expected: params.Applicable, Actual: applicableResults, Msg: GetValidationCountErrMsg("applicable results", "sarif report", params.ExactResultsMatch, params.Applicable, applicableResults)}, + CountValidation[int]{Expected: params.Undetermined, Actual: undeterminedResults, Msg: GetValidationCountErrMsg("undetermined results", "sarif report", params.ExactResultsMatch, params.Undetermined, undeterminedResults)}, + CountValidation[int]{Expected: params.NotCovered, Actual: notCoveredResults, Msg: GetValidationCountErrMsg("not covered results", "sarif report", params.ExactResultsMatch, params.NotCovered, notCoveredResults)}, + CountValidation[int]{Expected: params.NotApplicable, Actual: notApplicableResults, Msg: GetValidationCountErrMsg("not applicable results", "sarif report", params.ExactResultsMatch, params.NotApplicable, notApplicableResults)}, + CountValidation[int]{Expected: params.MissingContext, Actual: missingContextResults, Msg: GetValidationCountErrMsg("missing context results", "sarif report", params.ExactResultsMatch, params.MissingContext, missingContextResults)}, + CountValidation[int]{Expected: params.SecurityViolations, Actual: securityViolations, Msg: GetValidationCountErrMsg("security violations", "sarif report", params.ExactResultsMatch, params.SecurityViolations, securityViolations)}, + CountValidation[int]{Expected: params.LicenseViolations, Actual: licenseViolations, Msg: GetValidationCountErrMsg("license violations", "sarif report", params.ExactResultsMatch, params.LicenseViolations, licenseViolations)}, + CountValidation[int]{Expected: params.Vulnerabilities, Actual: vulnerabilities, Msg: GetValidationCountErrMsg("vulnerabilities", "sarif report", params.ExactResultsMatch, params.Vulnerabilities, vulnerabilities)}, + ) +} + +func isSecurityIssue(result *sarif.Result) bool { + // If the rule id starts with CVE or XRAY, it is a security issue + if result.RuleID == nil { + return false + } + ruleID := *result.RuleID + + if strings.HasPrefix(ruleID, "CVE") || strings.HasPrefix(ruleID, "XRAY") { + return true + } + return false +} + +func ValidateSarifReport(t *testing.T, exactMatch bool, expected, actual *sarif.Report) { + ValidateContent(t, exactMatch, StringValidation{Expected: expected.Version, Actual: actual.Version, Msg: "Sarif version mismatch"}) + for _, run := range expected.Runs { + // expect Invocation + if !assert.Len(t, run.Invocations, 1, "Expected exactly one invocation for run with tool name %s", run.Tool.Driver.Name) { + continue + } + actualRun := getRunByInvocationTargetAndToolName(sarifutils.GetInvocationWorkingDirectory(run.Invocations[0]), run.Tool.Driver.Name, actual.Runs) + if !assert.NotNil(t, actualRun, "Expected run with tool name %s and working directory %s not found", run.Tool.Driver.Name, sarifutils.GetInvocationWorkingDirectory(run.Invocations[0])) { + continue + } + validateSarifRun(t, exactMatch, run, actualRun) + } +} + +func getRunByInvocationTargetAndToolName(target, toolName string, content []*sarif.Run) *sarif.Run { + potentialRuns := sarifutils.GetRunsByWorkingDirectory(target, content...) + for _, run := range potentialRuns { + if run.Tool.Driver != nil && run.Tool.Driver.Name == toolName { + return run + } + } + return nil +} + +func validateSarifRun(t *testing.T, exactMatch bool, expected, actual *sarif.Run) { + ValidateContent(t, exactMatch, + PointerValidation[string]{Expected: expected.Tool.Driver.InformationURI, Actual: actual.Tool.Driver.InformationURI, Msg: fmt.Sprintf("Run tool information URI mismatch for tool %s", expected.Tool.Driver.Name)}, + PointerValidation[string]{Expected: expected.Tool.Driver.Version, Actual: actual.Tool.Driver.Version, Msg: fmt.Sprintf("Run tool version mismatch for tool %s", expected.Tool.Driver.Name)}, + ) + validateSarifProperties(t, exactMatch, expected.Properties, actual.Properties, expected.Tool.Driver.Name, "run") + // validate rules + for _, expectedRule := range expected.Tool.Driver.Rules { + rule, err := actual.GetRuleById(expectedRule.ID) + if !(assert.NoError(t, err, fmt.Sprintf("Run tool %s: Expected rule with ID %s not found", expected.Tool.Driver.Name, expectedRule.ID)) || + assert.NotNil(t, rule, fmt.Sprintf("Run tool %s: Expected rule with ID %s not found", expected.Tool.Driver.Name, expectedRule.ID))) { + continue + } + validateSarifRule(t, exactMatch, expected.Tool.Driver.Name, expectedRule, rule) + } + // validate results + for _, expectedResult := range expected.Results { + result := getResultByResultId(expectedResult, actual.Results) + if !assert.NotNil(t, result, fmt.Sprintf("Run tool %s: Expected result with rule ID %s not found in %v", expected.Tool.Driver.Name, sarifutils.GetResultRuleId(expectedResult), getResultsRuleIds(actual.Results))) { + continue + } + validateSarifResult(t, exactMatch, expected.Tool.Driver.Name, expectedResult, result) + } +} + +func getResultsRuleIds(results []*sarif.Result) []string { + var ruleIds []string + for _, result := range results { + ruleIds = append(ruleIds, sarifutils.GetResultRuleId(result)) + } + return ruleIds +} + +func validateSarifRule(t *testing.T, exactMatch bool, toolName string, expected, actual *sarif.ReportingDescriptor) { + ValidateContent(t, exactMatch, + StringValidation{Expected: sarifutils.GetRuleFullDescription(expected), Actual: sarifutils.GetRuleFullDescription(actual), Msg: fmt.Sprintf("Run tool %s: Rule full description mismatch for rule %s", toolName, expected.ID)}, + StringValidation{Expected: sarifutils.GetRuleFullDescriptionMarkdown(expected), Actual: sarifutils.GetRuleFullDescriptionMarkdown(actual), Msg: fmt.Sprintf("Run tool %s: Rule full description markdown mismatch for rule %s", toolName, expected.ID)}, + StringValidation{Expected: sarifutils.GetRuleShortDescription(expected), Actual: sarifutils.GetRuleShortDescription(actual), Msg: fmt.Sprintf("Run tool %s: Rule short description mismatch for rule %s", toolName, expected.ID)}, + StringValidation{Expected: sarifutils.GetRuleHelp(expected), Actual: sarifutils.GetRuleHelp(actual), Msg: fmt.Sprintf("Run tool %s: Rule help mismatch for rule %s", toolName, expected.ID)}, + StringValidation{Expected: sarifutils.GetRuleHelpMarkdown(expected), Actual: sarifutils.GetRuleHelpMarkdown(actual), Msg: fmt.Sprintf("Run tool %s: Rule help markdown mismatch for rule %s", toolName, expected.ID)}, + ) + // validate properties + validateSarifProperties(t, exactMatch, expected.Properties, actual.Properties, toolName, fmt.Sprintf("rule %s", expected.ID)) +} + +func getResultByResultId(expected *sarif.Result, actual []*sarif.Result) *sarif.Result { + log.Output("====================================") + log.Output(fmt.Sprintf(":: Actual results with expected results: %s", getResultId(expected))) + for _, result := range actual { + + log.Output(fmt.Sprintf("Compare actual result (isPotential=%t, hasSameLocations=%t) with expected result: %s", isPotentialSimilarResults(expected, result), hasSameLocations(expected, result), getResultId(result))) + if isPotentialSimilarResults(expected, result) && hasSameLocations(expected, result) { + return result + } + } + log.Output("====================================") + return nil +} + +func isPotentialSimilarResults(expected, actual *sarif.Result) bool { + return sarifutils.GetResultRuleId(actual) == sarifutils.GetResultRuleId(expected) && sarifutils.GetResultMsgText(actual) == sarifutils.GetResultMsgText(expected) && sarifutils.GetResultProperty(sarifparser.WatchSarifPropertyKey, actual) == sarifutils.GetResultProperty(sarifparser.WatchSarifPropertyKey, expected) +} + +func getResultId(result *sarif.Result) string { + return fmt.Sprintf("%s-%s-%s-%s", sarifutils.GetResultRuleId(result), sarifutils.GetResultMsgText(result), sarifutils.GetResultProperty(sarifparser.WatchSarifPropertyKey, result), getLocationsId(result.Locations)) +} + +func getLocationsId(locations []*sarif.Location) string { + var locationsId string + for _, location := range locations { + locationsId += sarifutils.GetLocationId(location) + } + return locationsId +} + +func hasSameLocations(expected, actual *sarif.Result) bool { + if len(expected.Locations) != len(actual.Locations) { + return false + } + for _, expectedLocation := range expected.Locations { + location := getLocationById(expectedLocation, actual.Locations) + if location == nil { + return false + } + } + return true +} + +func validateSarifResult(t *testing.T, exactMatch bool, toolName string, expected, actual *sarif.Result) { + ValidateContent(t, exactMatch, + StringValidation{Expected: sarifutils.GetResultLevel(expected), Actual: sarifutils.GetResultLevel(actual), Msg: fmt.Sprintf("Run tool %s: Result level mismatch for rule %s", toolName, sarifutils.GetResultRuleId(expected))}, + ) + // validate properties + validateSarifProperties(t, exactMatch, expected.Properties, actual.Properties, toolName, fmt.Sprintf("result rule %s", sarifutils.GetResultRuleId(expected))) + // validate locations + for _, expectedLocation := range expected.Locations { + location := getLocationById(expectedLocation, actual.Locations) + if !assert.NotNil(t, location, "Expected location with physical location %s not found", expectedLocation.PhysicalLocation) { + continue + } + } +} + +func getLocationById(expected *sarif.Location, actual []*sarif.Location) *sarif.Location { + for _, location := range actual { + if sarifutils.GetLocationId(location) == sarifutils.GetLocationId(expected) { + return location + } + } + return nil +} + +func validateSarifProperties(t *testing.T, exactMatch bool, expected, actual map[string]interface{}, toolName, id string) { + for key, expectedValue := range expected { + actualValue, ok := actual[key] + if !assert.True(t, ok, fmt.Sprintf("Run tool %s: Expected property with key %s not found for %s", toolName, key, id)) { + continue + } + // If the property is a string, compare the string values + if expectedStr, ok := expectedValue.(string); ok { + actualStr, ok := actualValue.(string) + if assert.True(t, ok, fmt.Sprintf("Run tool %s: Expected property with key %s is not a string for %s", toolName, key, id)) { + ValidateContent(t, exactMatch, StringValidation{Expected: expectedStr, Actual: actualStr, Msg: fmt.Sprintf("Run tool %s: Rule property mismatch for rule %s", toolName, id)}) + continue + } + assert.Fail(t, fmt.Sprintf("Run tool %s: Expected property with key %s is a string for %s", toolName, key, id)) + } + } +} diff --git a/utils/validations/test_validate_sca.go b/utils/validations/test_validate_sca.go new file mode 100644 index 00000000..23591ef8 --- /dev/null +++ b/utils/validations/test_validate_sca.go @@ -0,0 +1,83 @@ +package validations + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/stretchr/testify/assert" +) + +// Validate SCA content only (No JAS in this content) according to the expected values and issue counts in the validation params. +// Content/Expected should be a []services.ScanResponse in the validation params. +// If Expected is provided, the validation will check if the Actual content matches the expected results. +// If ExactResultsMatch is true, the validation will check exact values and not only the 'equal or grater' counts / existence of expected attributes. (For Integration tests with JFrog API, ExactResultsMatch should be set to false) +func VerifyJsonResults(t *testing.T, content string, params ValidationParams) { + var results []services.ScanResponse + err := json.Unmarshal([]byte(content), &results) + assert.NoError(t, err) + params.Actual = results + ValidateCommandJsonOutput(t, params) +} + +// Validation on SCA content only (No JAS in this content) +// Actual (and optional Expected) content should be a slice of services.ScanResponse in the validation params +func ValidateCommandJsonOutput(t *testing.T, params ValidationParams) { + results, ok := params.Actual.([]services.ScanResponse) + if assert.True(t, ok, "Actual content is not a slice of services.ScanResponse") { + ValidateScanResponseIssuesCount(t, params, results...) + if params.Expected != nil { + expectedResults, ok := params.Expected.([]services.ScanResponse) + if assert.True(t, ok, "Expected content is not a slice of services.ScanResponse") { + ValidateScanResponses(t, params.ExactResultsMatch, expectedResults, results) + } + } + } +} + +func ValidateScanResponseIssuesCount(t *testing.T, params ValidationParams, content ...services.ScanResponse) { + var vulnerabilities, licenses, securityViolations, licenseViolations, operationalViolations int + + for _, result := range content { + vulnerabilities += len(result.Vulnerabilities) + licenses += len(result.Licenses) + for _, violation := range result.Violations { + switch violation.ViolationType { + case utils.ViolationTypeSecurity.String(): + securityViolations += 1 + case utils.ViolationTypeLicense.String(): + licenseViolations += 1 + case utils.ViolationTypeOperationalRisk.String(): + operationalViolations += 1 + } + } + } + + ValidateContent(t, params.ExactResultsMatch, + CountValidation[int]{Expected: params.Vulnerabilities, Actual: vulnerabilities, Msg: GetValidationCountErrMsg("vulnerabilities", "scan responses", params.ExactResultsMatch, params.Vulnerabilities, vulnerabilities)}, + CountValidation[int]{Expected: params.Licenses, Actual: licenses, Msg: GetValidationCountErrMsg("licenses", "scan responses", params.ExactResultsMatch, params.Licenses, licenses)}, + CountValidation[int]{Expected: params.SecurityViolations, Actual: securityViolations, Msg: GetValidationCountErrMsg("security violations", "scan responses", params.ExactResultsMatch, params.SecurityViolations, securityViolations)}, + CountValidation[int]{Expected: params.LicenseViolations, Actual: licenseViolations, Msg: GetValidationCountErrMsg("license violations", "scan responses", params.ExactResultsMatch, params.LicenseViolations, licenseViolations)}, + CountValidation[int]{Expected: params.OperationalViolations, Actual: operationalViolations, Msg: GetValidationCountErrMsg("operational risk violations", "scan responses", params.ExactResultsMatch, params.OperationalViolations, operationalViolations)}, + ) +} + +func ValidateScanResponses(t *testing.T, exactMatch bool, expected, actual []services.ScanResponse) { + for _, expectedResponse := range expected { + actualResponse := getScanResponseByScanId(expectedResponse.ScanId, actual) + if !assert.NotNil(t, actualResponse, fmt.Sprintf("ScanId %s not found in the scan responses", expectedResponse.ScanId)) { + return + } + } +} + +func getScanResponseByScanId(scanId string, content []services.ScanResponse) *services.ScanResponse { + for _, result := range content { + if result.ScanId == scanId { + return &result + } + } + return nil +} diff --git a/utils/validations/test_validate_simple_json.go b/utils/validations/test_validate_simple_json.go new file mode 100644 index 00000000..ed0d5622 --- /dev/null +++ b/utils/validations/test_validate_simple_json.go @@ -0,0 +1,247 @@ +package validations + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/stretchr/testify/assert" +) + +// Validate simple-json report results according to the expected values and issue counts in the validation params. +// Content/Expected should be a formats.SimpleJsonResults in the validation params. +// If Expected is provided, the validation will check if the Actual content matches the expected results. +// If ExactResultsMatch is true, the validation will check exact values and not only the 'equal or grater' counts / existence of expected attributes. (For Integration tests with JFrog API, ExactResultsMatch should be set to false) +func VerifySimpleJsonResults(t *testing.T, content string, params ValidationParams) { + var results formats.SimpleJsonResults + err := json.Unmarshal([]byte(content), &results) + assert.NoError(t, err, "Failed to unmarshal content to formats.SimpleJsonResults") + params.Actual = results + ValidateCommandSimpleJsonOutput(t, params) +} + +// Validate simple-json report results according to the expected values and issue counts in the validation params. +// Actual/Expected content should be a formats.SimpleJsonResults in the validation params. +// If Expected is provided, the validation will check if the Actual content matches the expected results. +// If ExactResultsMatch is true, the validation will check exact values and not only the 'equal or grater' counts / existence of expected attributes. (For Integration tests with JFrog API, ExactResultsMatch should be set to false) +func ValidateCommandSimpleJsonOutput(t *testing.T, params ValidationParams) { + results, ok := params.Actual.(formats.SimpleJsonResults) + if assert.True(t, ok, "Actual content is not of type formats.SimpleJsonResults") { + ValidateSimpleJsonIssuesCount(t, params, results) + if params.Expected != nil { + expectedResults, ok := params.Expected.(formats.SimpleJsonResults) + if assert.True(t, ok, "Expected content is not of type formats.SimpleJsonResults") { + ValidateSimpleJsonResults(t, params.ExactResultsMatch, expectedResults, results) + } + } + } +} + +// Validate simple-json report results according to the expected counts in the validation params. +// Actual content should be a formats.SimpleJsonResults in the validation params. +// If Expected is provided, the validation will check if the Actual content matches the expected results. +// If ExactResultsMatch is true, the validation will check exact values and not only the 'equal or grater' counts / existence of expected attributes. (For Integration tests with JFrog API, ExactResultsMatch should be set to false) +func ValidateSimpleJsonIssuesCount(t *testing.T, params ValidationParams, results formats.SimpleJsonResults) { + var applicableResults, undeterminedResults, notCoveredResults, notApplicableResults, missingContextResults, inactiveResults int + for _, vuln := range results.Vulnerabilities { + switch vuln.Applicable { + case jasutils.NotApplicable.String(): + notApplicableResults++ + case jasutils.Applicable.String(): + applicableResults++ + case jasutils.NotCovered.String(): + notCoveredResults++ + case jasutils.ApplicabilityUndetermined.String(): + undeterminedResults++ + case jasutils.MissingContext.String(): + missingContextResults++ + } + } + for _, result := range results.Secrets { + if result.Applicability != nil { + if result.Applicability.Status == jasutils.Inactive.String() { + inactiveResults += 1 + } + } + } + vulnerabilitiesCount := len(results.Vulnerabilities) + len(results.Secrets) + len(results.Sast) + len(results.Iacs) + + ValidateContent(t, params.ExactResultsMatch, + CountValidation[int]{Expected: params.Vulnerabilities, Actual: vulnerabilitiesCount, Msg: GetValidationCountErrMsg("vulnerabilities", "simple-json", params.ExactResultsMatch, params.Vulnerabilities, vulnerabilitiesCount)}, + CountValidation[int]{Expected: params.Sast, Actual: len(results.Sast), Msg: GetValidationCountErrMsg("sast", "simple-json", params.ExactResultsMatch, params.Sast, len(results.Sast))}, + CountValidation[int]{Expected: params.Iac, Actual: len(results.Iacs), Msg: GetValidationCountErrMsg("IaC", "simple-json", params.ExactResultsMatch, params.Iac, len(results.Iacs))}, + CountValidation[int]{Expected: params.Secrets, Actual: len(results.Secrets), Msg: GetValidationCountErrMsg("secrets", "simple-json", params.ExactResultsMatch, params.Secrets, len(results.Secrets))}, + CountValidation[int]{Expected: params.Inactive, Actual: inactiveResults, Msg: GetValidationCountErrMsg("inactive secrets", "simple-json", params.ExactResultsMatch, params.Inactive, inactiveResults)}, + + CountValidation[int]{Expected: params.Applicable, Actual: applicableResults, Msg: GetValidationCountErrMsg("applicable vulnerabilities", "simple-json", params.ExactResultsMatch, params.Applicable, applicableResults)}, + CountValidation[int]{Expected: params.Undetermined, Actual: undeterminedResults, Msg: GetValidationCountErrMsg("undetermined vulnerabilities", "simple-json", params.ExactResultsMatch, params.Undetermined, undeterminedResults)}, + CountValidation[int]{Expected: params.NotCovered, Actual: notCoveredResults, Msg: GetValidationCountErrMsg("not covered vulnerabilities", "simple-json", params.ExactResultsMatch, params.NotCovered, notCoveredResults)}, + CountValidation[int]{Expected: params.NotApplicable, Actual: notApplicableResults, Msg: GetValidationCountErrMsg("not applicable vulnerabilities", "simple-json", params.ExactResultsMatch, params.NotApplicable, notApplicableResults)}, + CountValidation[int]{Expected: params.MissingContext, Actual: missingContextResults, Msg: GetValidationCountErrMsg("missing context vulnerabilities", "simple-json", params.ExactResultsMatch, params.MissingContext, missingContextResults)}, + + CountValidation[int]{Expected: params.SecurityViolations, Actual: len(results.SecurityViolations), Msg: GetValidationCountErrMsg("security violations", "simple-json", params.ExactResultsMatch, params.SecurityViolations, len(results.SecurityViolations))}, + CountValidation[int]{Expected: params.LicenseViolations, Actual: len(results.LicensesViolations), Msg: GetValidationCountErrMsg("license violations", "simple-json", params.ExactResultsMatch, params.LicenseViolations, len(results.LicensesViolations))}, + CountValidation[int]{Expected: params.OperationalViolations, Actual: len(results.OperationalRiskViolations), Msg: GetValidationCountErrMsg("operational risk violations", "simple-json", params.ExactResultsMatch, params.OperationalViolations, len(results.OperationalRiskViolations))}, + CountValidation[int]{Expected: params.Licenses, Actual: len(results.Licenses), Msg: GetValidationCountErrMsg("Licenses", "simple-json", params.ExactResultsMatch, params.Licenses, len(results.Licenses))}, + ) +} + +func ValidateSimpleJsonResults(t *testing.T, exactMatch bool, expected, actual formats.SimpleJsonResults) { + ValidateContent(t, exactMatch, StringValidation{Expected: expected.MultiScanId, Actual: actual.MultiScanId, Msg: "MultiScanId mismatch"}) + ValidateContent(t, false, NumberValidation[int]{Expected: len(expected.Errors), Actual: len(actual.Errors), Msg: "Errors count mismatch"}) + // Validate vulnerabilities + for _, expectedVulnerability := range expected.Vulnerabilities { + vulnerability := getVulnerabilityOrViolationByIssueId(expectedVulnerability.IssueId, expectedVulnerability.ImpactedDependencyName, expectedVulnerability.ImpactedDependencyVersion, actual.Vulnerabilities) + if !assert.NotNil(t, vulnerability, fmt.Sprintf("IssueId %s not found in the vulnerabilities", expectedVulnerability.IssueId)) { + return + } + validateVulnerabilityOrViolationRow(t, exactMatch, expectedVulnerability, *vulnerability) + } + // Validate securityViolations + for _, expectedViolation := range expected.SecurityViolations { + violation := getVulnerabilityOrViolationByIssueId(expectedViolation.IssueId, expectedViolation.ImpactedDependencyName, expectedViolation.ImpactedDependencyVersion, actual.SecurityViolations) + if !assert.NotNil(t, violation, fmt.Sprintf("IssueId %s not found in the securityViolations", expectedViolation.IssueId)) { + return + } + validateVulnerabilityOrViolationRow(t, exactMatch, expectedViolation, *violation) + } +} + +func getVulnerabilityOrViolationByIssueId(issueId, impactedDependencyName, impactedDependencyVersion string, content []formats.VulnerabilityOrViolationRow) *formats.VulnerabilityOrViolationRow { + for _, result := range content { + if result.IssueId == issueId && result.ImpactedDependencyName == impactedDependencyName && result.ImpactedDependencyVersion == impactedDependencyVersion { + return &result + } + } + return nil +} + +func validateVulnerabilityOrViolationRow(t *testing.T, exactMatch bool, expected, actual formats.VulnerabilityOrViolationRow) { + ValidateContent(t, exactMatch, + StringValidation{Expected: expected.Summary, Actual: actual.Summary, Msg: fmt.Sprintf("IssueId %s: Summary mismatch", expected.IssueId)}, + StringValidation{Expected: expected.Severity, Actual: actual.Severity, Msg: fmt.Sprintf("IssueId %s: Severity mismatch", expected.IssueId)}, + StringValidation{Expected: expected.Applicable, Actual: actual.Applicable, Msg: fmt.Sprintf("IssueId %s: Applicable mismatch", expected.IssueId)}, + StringValidation{Expected: expected.Technology.String(), Actual: actual.Technology.String(), Msg: fmt.Sprintf("IssueId %s: Technology mismatch", expected.IssueId)}, + ListValidation[string]{Expected: expected.References, Actual: actual.References, Msg: fmt.Sprintf("IssueId %s: References mismatch", expected.IssueId)}, + + StringValidation{Expected: expected.ImpactedDependencyType, Actual: actual.ImpactedDependencyType, Msg: fmt.Sprintf("IssueId %s: ImpactedDependencyType mismatch", expected.IssueId)}, + + ListValidation[string]{Expected: expected.FixedVersions, Actual: actual.FixedVersions, Msg: fmt.Sprintf("IssueId %s: FixedVersions mismatch", expected.IssueId)}, + ) + if ValidatePointersAndNotNil(t, exactMatch, PointerValidation[formats.JfrogResearchInformation]{Expected: expected.JfrogResearchInformation, Actual: actual.JfrogResearchInformation, Msg: fmt.Sprintf("IssueId %s: JfrogResearchInformation mismatch", expected.IssueId)}) { + ValidateContent(t, exactMatch, + StringValidation{Expected: expected.JfrogResearchInformation.Summary, Actual: actual.JfrogResearchInformation.Summary, Msg: fmt.Sprintf("IssueId %s: JfrogResearchInformation.Summary mismatch", expected.IssueId)}, + StringValidation{Expected: expected.JfrogResearchInformation.Severity, Actual: actual.JfrogResearchInformation.Severity, Msg: fmt.Sprintf("IssueId %s: JfrogResearchInformation.Severity mismatch", expected.IssueId)}, + StringValidation{Expected: expected.JfrogResearchInformation.Remediation, Actual: actual.JfrogResearchInformation.Remediation, Msg: fmt.Sprintf("IssueId %s: JfrogResearchInformation.Remediation mismatch", expected.IssueId)}, + ListValidation[formats.JfrogResearchSeverityReason]{Expected: expected.JfrogResearchInformation.SeverityReasons, Actual: actual.JfrogResearchInformation.SeverityReasons, Msg: fmt.Sprintf("IssueId %s: JfrogResearchInformation.SeverityReasons mismatch", expected.IssueId)}, + ) + } + validateComponentRows(t, expected.IssueId, exactMatch, expected.Components, actual.Components) + validateCveRows(t, expected.IssueId, exactMatch, expected.Cves, actual.Cves) + validateImpactPaths(t, expected.IssueId, exactMatch, expected.ImpactPaths, actual.ImpactPaths) +} + +func validateImpactPaths(t *testing.T, issueId string, exactMatch bool, expected, actual [][]formats.ComponentRow) { + assert.Len(t, actual, len(expected), fmt.Sprintf("IssueId %s: ImpactPaths count mismatch", issueId)) + if !exactMatch { + return + } + for _, expectedPath := range expected { + impactPath := getImpactPath(expectedPath, actual) + if !assert.NotNil(t, impactPath, fmt.Sprintf("IssueId %s: expected ImpactPath not found in the impactPaths", issueId)) { + return + } + } +} + +func getImpactPath(path []formats.ComponentRow, content [][]formats.ComponentRow) *[]formats.ComponentRow { + for _, result := range content { + if len(result) != len(path) { + continue + } + found := true + for i, component := range result { + if component.Name != path[i].Name || component.Version != path[i].Version { + found = false + break + } + } + if found { + return &result + } + } + return nil +} + +func validateComponentRows(t *testing.T, issueId string, exactMatch bool, expected, actual []formats.ComponentRow) { + assert.Len(t, actual, len(expected), fmt.Sprintf("IssueId %s: Components count mismatch", issueId)) + if !exactMatch { + return + } + for _, expectedComponent := range expected { + component := getComponent(expectedComponent.Name, expectedComponent.Version, actual) + if !assert.NotNil(t, component, fmt.Sprintf("IssueId %s: Component %s: not found in the components", issueId, expectedComponent.Name)) { + return + } + validateComponentRow(t, issueId, exactMatch, expectedComponent, *component) + } +} + +func validateComponentRow(t *testing.T, issueId string, exactMatch bool, expected, actual formats.ComponentRow) { + ValidateContent(t, exactMatch, + PointerValidation[formats.Location]{Expected: expected.Location, Actual: actual.Location, Msg: fmt.Sprintf("IssueId %s: Component %s:%s Location mismatch", issueId, expected.Name, expected.Version)}, + ) + if expected.Location != nil { + ValidateContent(t, exactMatch, StringValidation{Expected: expected.Location.File, Actual: actual.Location.File, Msg: fmt.Sprintf("IssueId %s: Component %s:%s Location.File mismatch", issueId, expected.Name, expected.Version)}) + } +} + +func getComponent(name, version string, content []formats.ComponentRow) *formats.ComponentRow { + for _, result := range content { + if result.Name == name && result.Version == version { + return &result + } + } + return nil +} + +func validateCveRows(t *testing.T, issueId string, exactMatch bool, expected, actual []formats.CveRow) { + assert.Len(t, actual, len(expected), fmt.Sprintf("IssueId %s: CVEs count mismatch", issueId)) + if !exactMatch { + return + } + for _, expectedCve := range expected { + cve := getCve(expectedCve.Id, actual) + if !assert.NotNil(t, cve, fmt.Sprintf("IssueId %s: CVE %s not found in the CVEs", issueId, expectedCve.Id)) { + return + } + validateCveRow(t, issueId, exactMatch, expectedCve, *cve) + } +} + +func validateCveRow(t *testing.T, issueId string, exactMatch bool, expected, actual formats.CveRow) { + if !ValidateContent(t, exactMatch, + StringValidation{Expected: expected.CvssV2, Actual: actual.CvssV2, Msg: fmt.Sprintf("IssueId %s: Cve %s: CvssV2 mismatch", issueId, expected.Id)}, + StringValidation{Expected: expected.CvssV3, Actual: actual.CvssV3, Msg: fmt.Sprintf("IssueId %s: Cve %s: CvssV3 mismatch", issueId, expected.Id)}, + ) { + return + } + if ValidatePointersAndNotNil(t, exactMatch, PointerValidation[formats.Applicability]{Expected: expected.Applicability, Actual: actual.Applicability, Msg: fmt.Sprintf("IssueId %s: Cve %s: Applicability mismatch", issueId, expected.Id)}) { + ValidateContent(t, exactMatch, + StringValidation{Expected: expected.Applicability.Status, Actual: actual.Applicability.Status, Msg: fmt.Sprintf("IssueId %s: Cve %s: Applicability.Status mismatch", issueId, expected.Id)}, + StringValidation{Expected: expected.Applicability.ScannerDescription, Actual: actual.Applicability.ScannerDescription, Msg: fmt.Sprintf("IssueId %s: Cve %s: Applicability.ScannerDescription mismatch", issueId, expected.Id)}, + ListValidation[formats.Evidence]{Expected: expected.Applicability.Evidence, Actual: actual.Applicability.Evidence, Msg: fmt.Sprintf("IssueId %s: Cve %s: Applicability.Evidence mismatch", issueId, expected.Id)}, + ) + } +} + +func getCve(cve string, content []formats.CveRow) *formats.CveRow { + for _, result := range content { + if result.Id == cve { + return &result + } + } + return nil +} diff --git a/utils/validations/test_validate_summary.go b/utils/validations/test_validate_summary.go new file mode 100644 index 00000000..5eedf12e --- /dev/null +++ b/utils/validations/test_validate_summary.go @@ -0,0 +1,72 @@ +package validations + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" + "github.com/stretchr/testify/assert" +) + +// Validate summary results according to the expected values and issue counts in the validation params. +// Content/Expected should be a formats.ResultsSummary in the validation params. +// If Expected is provided, the validation will check if the Actual content matches the expected results. +// If ExactResultsMatch is true, the validation will check exact values and not only the 'equal or grater' counts / existence of expected attributes. (For Integration tests with JFrog API, ExactResultsMatch should be set to false) +func ValidateCommandSummaryOutput(t *testing.T, params ValidationParams) { + results, ok := params.Actual.(formats.ResultsSummary) + if assert.True(t, ok, "Actual content is not a formats.ResultsSummary") { + ValidateSummaryIssuesCount(t, params, results) + } +} + +func ValidateSummaryIssuesCount(t *testing.T, params ValidationParams, results formats.ResultsSummary) { + var vulnerabilities, securityViolations, licenseViolations, opRiskViolations, applicableResults, undeterminedResults, notCoveredResults, notApplicableResults, missingContextResults, sast, iac, secrets int + + vulnerabilities = results.GetTotalVulnerabilities() + + securityViolations = results.GetTotalViolations(formats.ScaSecurityResult) + licenseViolations = results.GetTotalViolations(formats.ScaLicenseResult) + opRiskViolations = results.GetTotalViolations(formats.ScaOperationalResult) + // Jas Results only available as vulnerabilities + sast = results.GetTotalVulnerabilities(formats.SastResult) + secrets = results.GetTotalVulnerabilities(formats.SecretsResult) + iac = results.GetTotalVulnerabilities(formats.IacResult) + // Get applicability status counts + for _, scan := range results.Scans { + if scan.Vulnerabilities != nil { + if scan.Vulnerabilities.ScaResults != nil { + for _, counts := range scan.Vulnerabilities.ScaResults.Security { + for status, count := range counts { + switch status { + case jasutils.Applicable.String(): + applicableResults += count + case jasutils.ApplicabilityUndetermined.String(): + undeterminedResults += count + case jasutils.NotCovered.String(): + notCoveredResults += count + case jasutils.NotApplicable.String(): + notApplicableResults += count + case jasutils.MissingContext.String(): + missingContextResults += count + } + } + } + } + } + } + + ValidateContent(t, params.ExactResultsMatch, + CountValidation[int]{Expected: params.Vulnerabilities, Actual: vulnerabilities, Msg: GetValidationCountErrMsg("vulnerabilities", "summary", params.ExactResultsMatch, params.Vulnerabilities, vulnerabilities)}, + CountValidation[int]{Expected: params.Sast, Actual: sast, Msg: GetValidationCountErrMsg("sast", "summary", params.ExactResultsMatch, params.Sast, sast)}, + CountValidation[int]{Expected: params.Secrets, Actual: secrets, Msg: GetValidationCountErrMsg("secrets", "summary", params.ExactResultsMatch, params.Secrets, secrets)}, + CountValidation[int]{Expected: params.Iac, Actual: iac, Msg: GetValidationCountErrMsg("IaC", "summary", params.ExactResultsMatch, params.Iac, iac)}, + CountValidation[int]{Expected: params.Applicable, Actual: applicableResults, Msg: GetValidationCountErrMsg("applicable vulnerabilities", "summary", params.ExactResultsMatch, params.Applicable, applicableResults)}, + CountValidation[int]{Expected: params.Undetermined, Actual: undeterminedResults, Msg: GetValidationCountErrMsg("undetermined vulnerabilities", "summary", params.ExactResultsMatch, params.Undetermined, undeterminedResults)}, + CountValidation[int]{Expected: params.NotCovered, Actual: notCoveredResults, Msg: GetValidationCountErrMsg("not covered vulnerabilities", "summary", params.ExactResultsMatch, params.NotCovered, notCoveredResults)}, + CountValidation[int]{Expected: params.NotApplicable, Actual: notApplicableResults, Msg: GetValidationCountErrMsg("not applicable vulnerabilities", "summary", params.ExactResultsMatch, params.NotApplicable, notApplicableResults)}, + CountValidation[int]{Expected: params.MissingContext, Actual: missingContextResults, Msg: GetValidationCountErrMsg("missing context vulnerabilities", "summary", params.ExactResultsMatch, params.MissingContext, missingContextResults)}, + CountValidation[int]{Expected: params.SecurityViolations, Actual: securityViolations, Msg: GetValidationCountErrMsg("security violations", "summary", params.ExactResultsMatch, params.SecurityViolations, securityViolations)}, + CountValidation[int]{Expected: params.LicenseViolations, Actual: licenseViolations, Msg: GetValidationCountErrMsg("license violations", "summary", params.ExactResultsMatch, params.LicenseViolations, licenseViolations)}, + CountValidation[int]{Expected: params.OperationalViolations, Actual: opRiskViolations, Msg: GetValidationCountErrMsg("operational risk violations", "summary", params.ExactResultsMatch, params.OperationalViolations, opRiskViolations)}, + ) +} diff --git a/utils/validations/test_validation.go b/utils/validations/test_validation.go new file mode 100644 index 00000000..21d86bfd --- /dev/null +++ b/utils/validations/test_validation.go @@ -0,0 +1,213 @@ +package validations + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/jfrog/jfrog-cli-security/utils" +) + +const ( + ErrCountFormat = "Expected%s %d %s in %s, but got %d %s." +) + +func GetValidationCountErrMsg(what, where string, exactMatch bool, expectedCount, actualCount int) string { + if exactMatch { + return fmt.Sprintf(ErrCountFormat, "", expectedCount, what, where, actualCount, what) + } + return fmt.Sprintf(ErrCountFormat, " at least", expectedCount, what, where, actualCount, what) +} + +// ValidationParams holds validation/assertion parameters for tests. +type ValidationParams struct { + // The actual content to verify. + Actual interface{} + // If provided, the test will check if the content matches the expected results. + Expected interface{} + // If provided, the test will check exact values and not only the minimum values / existence. + ExactResultsMatch bool + // Expected issues for each type to check if the content has the correct amount of issues. + Vulnerabilities int + Licenses int + SecurityViolations int + LicenseViolations int + OperationalViolations int + Applicable int + Undetermined int + NotCovered int + NotApplicable int + MissingContext int + Inactive int + Sast int + Iac int + Secrets int +} + +// Validation allows to validate/assert a content with expected values. +// Using the Validation interfaces implementations allows you to assert content for exact value or not exact match (changes base on the implementation). +type Validation interface { + Validate(t *testing.T, exactMatch bool) bool + ErrMsgs(t *testing.T) []string +} + +// Validate a string content. +// Not ExactMatch: The actual content must not be empty if the expected content is not empty. +type StringValidation struct { + Expected string + Actual string + Msg string +} + +func (sv StringValidation) Validate(t *testing.T, exactMatch bool) bool { + return validateStrContent(t, sv.Expected, sv.Actual, exactMatch, sv.ErrMsgs(t)) +} + +func validateStrContent(t *testing.T, expected, actual string, actualValue bool, msgAndArgs ...interface{}) bool { + if actualValue { + return assert.Equal(t, expected, actual, msgAndArgs...) + } + if expected != "" { + return assert.NotEmpty(t, actual, msgAndArgs...) + } else { + return assert.Empty(t, actual, msgAndArgs...) + } +} + +func (sv StringValidation) ErrMsgs(_ *testing.T) []string { + return []string{sv.Msg} +} + +// CountValidation validates the content of the given numbers. +// Not ExactMatch: The actual content must be greater or equal to the expected content. +type CountValidation[T any] struct { + Expected int + Actual int + Msg string +} + +func (cv CountValidation[T]) Validate(t *testing.T, exactMatch bool) bool { + return validateNumberCount(t, cv.Expected, cv.Actual, exactMatch, cv.ErrMsgs(t)) +} + +func validateNumberCount(t *testing.T, expected, actual interface{}, actualValue bool, msgAndArgs ...interface{}) bool { + if actualValue { + return assert.Equal(t, expected, actual, msgAndArgs...) + } + return assert.GreaterOrEqual(t, actual, expected, msgAndArgs...) +} + +func (cv CountValidation[T]) ErrMsgs(_ *testing.T) []string { + return []string{cv.Msg} +} + +// NumberValidation validates the content of the given numbers. +// Not ExactMatch: The actual content must not be zero if the expected content is not zero. +type NumberValidation[T any] struct { + Expected T + Actual T + Msg string +} + +func (nvp NumberValidation[T]) Validate(t *testing.T, exactMatch bool) bool { + return validateNumberContent(t, nvp.Expected, nvp.Actual, exactMatch, nvp.ErrMsgs(t)) +} + +func validateNumberContent(t *testing.T, expected, actual interface{}, actualValue bool, msgAndArgs ...interface{}) bool { + if actualValue { + return assert.Equal(t, expected, actual, msgAndArgs...) + } + if expected != 0 { + return assert.NotZero(t, actual, msgAndArgs...) + } else { + return assert.Zero(t, actual, msgAndArgs...) + } +} + +func (nvp NumberValidation[T]) ErrMsgs(_ *testing.T) []string { + return []string{nvp.Msg} +} + +// PointerValidation validates the content of the given pointers. +// Not ExactMatch: The actual content must not be nil if the expected content is not nil. +type PointerValidation[T any] struct { + Expected *T + Actual *T + Msg string +} + +func (pvp PointerValidation[T]) Validate(t *testing.T, exactMatch bool) bool { + return validatePointers(t, pvp.Expected, pvp.Actual, exactMatch, pvp.ErrMsgs(t)) +} + +func ValidatePointersAndNotNil[T any](t *testing.T, exactMatch bool, pair PointerValidation[T]) bool { + return validatePointers(t, pair.Expected, pair.Actual, exactMatch, pair.Msg) && pair.Expected != nil +} + +func validatePointers(t *testing.T, expected, actual interface{}, actualValue bool, msgAndArgs ...interface{}) bool { + if actualValue { + return assert.Equal(t, expected, actual, msgAndArgs...) + } + if expected != nil { + return assert.NotNil(t, actual, msgAndArgs...) + } + return assert.Nil(t, actual, msgAndArgs...) +} + +func (pvp PointerValidation[T]) ErrMsgs(t *testing.T) []string { + return jsonErrMsg(t, pvp.Expected, pvp.Actual, pvp.Msg) +} + +// ListValidation validates the content of the given lists. +// Not ExactMatch: The expected content must be subset of the actual content. +type ListValidation[T any] struct { + Expected []T + Actual []T + Msg string +} + +func (lvp ListValidation[T]) Validate(t *testing.T, exactMatch bool) bool { + return validateLists(t, lvp.Expected, lvp.Actual, exactMatch, lvp.ErrMsgs(t)) +} + +func validateLists(t *testing.T, expected, actual interface{}, exactMatch bool, msgAndArgs ...interface{}) bool { + if exactMatch { + return assert.ElementsMatch(t, expected, actual, msgAndArgs...) + } + return assert.Subset(t, actual, expected, msgAndArgs...) +} + +func (lvp ListValidation[T]) ErrMsgs(t *testing.T) []string { + return jsonErrMsg(t, lvp.Expected, lvp.Actual, lvp.Msg) +} + +func jsonErrMsg(t *testing.T, expected, actual any, msg string) []string { + var expectedStr, actualStr string + var err error + if expected != nil { + expectedStr, err = utils.GetAsJsonString(expected, false, true) + assert.NoError(t, err) + } + if actual != nil { + actualStr, err = utils.GetAsJsonString(actual, false, true) + assert.NoError(t, err) + } + return errMsg(expectedStr, actualStr, msg) +} + +func errMsg(expected, actual string, msg string) []string { + return []string{msg, fmt.Sprintf("\n* Expected:\n'%s'\n\n* Actual:\n%s\n", expected, actual)} +} + +// ValidateContent validates the content of the given Validations. +// If exactMatch is true, the content must match exactly. +// If at least one validation fails, the function returns false and stops validating the rest of the pairs. +func ValidateContent(t *testing.T, exactMatch bool, validations ...Validation) bool { + for _, validation := range validations { + if !validation.Validate(t, exactMatch) { + return false + } + } + return true +} diff --git a/utils/xsc/analyticsmetrics.go b/utils/xsc/analyticsmetrics.go index c8430c36..3fb4d795 100644 --- a/utils/xsc/analyticsmetrics.go +++ b/utils/xsc/analyticsmetrics.go @@ -9,7 +9,8 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/usage" "github.com/jfrog/jfrog-cli-security/jas" - "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xsc" @@ -157,16 +158,26 @@ func (ams *AnalyticsMetricsService) GetGeneralEvent(msi string) (*xscservices.Xs return event, err } -func (ams *AnalyticsMetricsService) CreateXscAnalyticsGeneralEventFinalizeFromAuditResults(auditResults *utils.Results) *xscservices.XscAnalyticsGeneralEventFinalize { +func (ams *AnalyticsMetricsService) CreateXscAnalyticsGeneralEventFinalizeFromAuditResults(auditResults *results.SecurityCommandResults) *xscservices.XscAnalyticsGeneralEventFinalize { totalDuration := time.Since(ams.GetStartTime()) eventStatus := xscservices.Completed - if auditResults.ScansErr != nil { + if auditResults.GetErrors() != nil { eventStatus = xscservices.Failed } - + summary, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: true}).ConvertToSummary(auditResults) + if err != nil { + log.Warn(fmt.Sprintf("Failed to convert audit results to summary. %s", err.Error())) + } + var totalFindings int + if summary.HasViolations() { + totalFindings = summary.GetTotalViolations() + } else { + totalFindings = summary.GetTotalVulnerabilities() + } + // return summary.GetTotalVulnerabilities() basicEvent := xscservices.XscAnalyticsBasicGeneralEvent{ EventStatus: eventStatus, - TotalFindings: auditResults.CountScanResultsFindings(true, true), + TotalFindings: totalFindings, TotalScanDuration: totalDuration.String(), } return &xscservices.XscAnalyticsGeneralEventFinalize{ diff --git a/utils/xsc/analyticsmetrics_test.go b/utils/xsc/analyticsmetrics_test.go index 9145b526..eb723a4a 100644 --- a/utils/xsc/analyticsmetrics_test.go +++ b/utils/xsc/analyticsmetrics_test.go @@ -7,8 +7,10 @@ import ( "time" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/validations" "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/jfrog/jfrog-client-go/xray/services" xscservices "github.com/jfrog/jfrog-client-go/xsc/services" @@ -29,19 +31,19 @@ func TestCalcShouldReportEvents(t *testing.T) { defer reportUsageCallback() // Minimum Xsc version. - mockServer, serverDetails := utils.XscServer(t, xscservices.AnalyticsMetricsMinXscVersion) + mockServer, serverDetails := validations.XscServer(t, xscservices.AnalyticsMetricsMinXscVersion) defer mockServer.Close() am := NewAnalyticsMetricsService(serverDetails) assert.True(t, am.calcShouldReportEvents()) // Lower Xsc version. - mockServerLowerVersion, serverDetails := utils.XscServer(t, lowerAnalyticsMetricsMinXscVersion) + mockServerLowerVersion, serverDetails := validations.XscServer(t, lowerAnalyticsMetricsMinXscVersion) defer mockServerLowerVersion.Close() am = NewAnalyticsMetricsService(serverDetails) assert.False(t, am.calcShouldReportEvents()) // Higher Xsc version. - mockServerHigherVersion, serverDetails := utils.XscServer(t, higherAnalyticsMetricsMinXscVersion) + mockServerHigherVersion, serverDetails := validations.XscServer(t, higherAnalyticsMetricsMinXscVersion) defer mockServerHigherVersion.Close() am = NewAnalyticsMetricsService(serverDetails) assert.True(t, am.calcShouldReportEvents()) @@ -60,11 +62,11 @@ func TestAddGeneralEvent(t *testing.T) { usageCallback := tests.SetEnvWithCallbackAndAssert(t, coreutils.ReportUsage, "true") defer usageCallback() // Successful flow. - mockServer, serverDetails := utils.XscServer(t, xscservices.AnalyticsMetricsMinXscVersion) + mockServer, serverDetails := validations.XscServer(t, xscservices.AnalyticsMetricsMinXscVersion) defer mockServer.Close() am := NewAnalyticsMetricsService(serverDetails) am.AddGeneralEvent(am.CreateGeneralEvent(xscservices.CliProduct, xscservices.CliEventType)) - assert.Equal(t, utils.TestMsi, am.GetMsi()) + assert.Equal(t, validations.TestMsi, am.GetMsi()) // In case cli should not report analytics, verify that request won't be sent. am.shouldReportEvents = false @@ -76,37 +78,18 @@ func TestAddGeneralEvent(t *testing.T) { func TestAnalyticsMetricsService_createAuditResultsFromXscAnalyticsBasicGeneralEvent(t *testing.T) { usageCallback := tests.SetEnvWithCallbackAndAssert(t, coreutils.ReportUsage, "true") defer usageCallback() - vulnerabilities := []services.Vulnerability{{IssueId: "XRAY-ID", Cves: []services.Cve{{Id: "CVE-123"}}, Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}} - scaResults := []*utils.ScaScanResult{{XrayResults: []services.ScanResponse{{Vulnerabilities: vulnerabilities}}}} - auditResults := utils.Results{ - ScaResults: scaResults, - ExtendedScanResults: &utils.ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyPassingResult("applic_CVE-123"))}, - SecretsScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 0, 0, 0, 0, ""))), - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 1, 1, 1, 1, ""))), - }, - IacScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 0, 0, 0, 0, ""))), - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("", 1, 1, 1, 1, ""))), - }, - 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, ""))), - }, - }, - } + testStruct := []struct { name string - auditResults *utils.Results + auditResults *results.SecurityCommandResults want xscservices.XscAnalyticsBasicGeneralEvent }{ - {name: "No audit results", auditResults: &utils.Results{}, want: xscservices.XscAnalyticsBasicGeneralEvent{EventStatus: xscservices.Completed}}, - {name: "Valid audit result", auditResults: &auditResults, want: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 7, EventStatus: xscservices.Completed}}, - {name: "Scan failed because jas errors.", auditResults: &utils.Results{ScansErr: errors.New("jas error"), ScaResults: scaResults}, want: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 1, EventStatus: xscservices.Failed}}, - {name: "Scan failed because sca errors.", auditResults: &utils.Results{ScansErr: errors.New("sca error")}, want: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 0, EventStatus: xscservices.Failed}}, + {name: "No audit results", auditResults: &results.SecurityCommandResults{}, want: xscservices.XscAnalyticsBasicGeneralEvent{EventStatus: xscservices.Completed}}, + {name: "Valid audit result", auditResults: getDummyContentForGeneralEvent(true, false), want: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 7, EventStatus: xscservices.Completed}}, + {name: "Scan failed with findings.", auditResults: getDummyContentForGeneralEvent(false, true), want: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 1, EventStatus: xscservices.Failed}}, + {name: "Scan failed no findings.", auditResults: &results.SecurityCommandResults{Targets: []*results.TargetResults{{Errors: []error{errors.New("an error")}}}}, want: xscservices.XscAnalyticsBasicGeneralEvent{TotalFindings: 0, EventStatus: xscservices.Failed}}, } - mockServer, serverDetails := utils.XscServer(t, xscservices.AnalyticsMetricsMinXscVersion) + mockServer, serverDetails := validations.XscServer(t, xscservices.AnalyticsMetricsMinXscVersion) defer mockServer.Close() am := NewAnalyticsMetricsService(serverDetails) am.SetStartTime() @@ -122,3 +105,36 @@ func TestAnalyticsMetricsService_createAuditResultsFromXscAnalyticsBasicGeneralE }) } } + +// Create a dummy content for general event. 1 SCA scan with 1 vulnerability +// withJas - Add 2 JAS results for each scan type. +// withErr - Add an error to the results. +func getDummyContentForGeneralEvent(withJas, withErr bool) *results.SecurityCommandResults { + vulnerabilities := []services.Vulnerability{{IssueId: "XRAY-ID", Severity: "medium", Cves: []services.Cve{{Id: "CVE-123"}}, Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}} + + cmdResults := results.NewCommandResults(utils.SourceCode, "", true, true) + scanResults := cmdResults.NewScanResults(results.ScanTarget{Target: "target"}) + scanResults.NewScaScanResults(services.ScanResponse{Vulnerabilities: vulnerabilities}) + + if withJas { + scanResults.JasResults.ApplicabilityScanResults = []*sarif.Run{sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyPassingResult("applic_CVE-123"))} + scanResults.JasResults.SecretsScanResults = []*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.IacScanResults = []*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.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, ""))), + } + } + + if withErr { + scanResults.Errors = []error{errors.New("an error")} + } + + return cmdResults +} diff --git a/utils/xsc/configprofile_test.go b/utils/xsc/configprofile_test.go index 86afca0a..7d8c8962 100644 --- a/utils/xsc/configprofile_test.go +++ b/utils/xsc/configprofile_test.go @@ -2,18 +2,19 @@ package xsc import ( "encoding/json" - "github.com/jfrog/jfrog-cli-security/utils" - "github.com/jfrog/jfrog-client-go/xsc/services" - "github.com/stretchr/testify/assert" "os" "testing" + + "github.com/jfrog/jfrog-cli-security/utils/validations" + "github.com/jfrog/jfrog-client-go/xsc/services" + "github.com/stretchr/testify/assert" ) func TestGetConfigProfile_ValidRequest_SuccessExpected(t *testing.T) { - mockServer, serverDetails := utils.XscServer(t, services.ConfigProfileMinXscVersion) + mockServer, serverDetails := validations.XscServer(t, services.ConfigProfileMinXscVersion) defer mockServer.Close() - configProfile, err := GetConfigProfile(serverDetails, utils.TestConfigProfileName) + configProfile, err := GetConfigProfile(serverDetails, validations.TestConfigProfileName) assert.NoError(t, err) profileFileContent, err := os.ReadFile("../../tests/testdata/other/configProfile/configProfileExample.json") @@ -27,10 +28,10 @@ func TestGetConfigProfile_ValidRequest_SuccessExpected(t *testing.T) { } func TestGetConfigProfile_TooLowXscVersion_FailureExpected(t *testing.T) { - mockServer, serverDetails := utils.XscServer(t, "1.0.0") + mockServer, serverDetails := validations.XscServer(t, "1.0.0") defer mockServer.Close() - configProfile, err := GetConfigProfile(serverDetails, utils.TestConfigProfileName) + configProfile, err := GetConfigProfile(serverDetails, validations.TestConfigProfileName) assert.Error(t, err) assert.Nil(t, configProfile) } diff --git a/utils/xsc/errorreport_test.go b/utils/xsc/errorreport_test.go index 11b839d5..2fc77db8 100644 --- a/utils/xsc/errorreport_test.go +++ b/utils/xsc/errorreport_test.go @@ -7,7 +7,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/validations" clienttestutils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" ) @@ -27,7 +27,7 @@ func TestReportLogErrorEventPossible(t *testing.T) { }{ { serverCreationFunc: func() (*httptest.Server, *config.ServerDetails) { - serverMock, serverDetails, _ := utils.CreateXscRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { + serverMock, serverDetails, _ := validations.CreateXscRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.RequestURI == "/xsc/api/v1/system/version" { w.WriteHeader(http.StatusNotFound) _, innerError := w.Write([]byte("Xsc service is not enabled")) @@ -41,18 +41,18 @@ func TestReportLogErrorEventPossible(t *testing.T) { expectedResponse: false, }, { - serverCreationFunc: func() (*httptest.Server, *config.ServerDetails) { return utils.XscServer(t, "") }, + serverCreationFunc: func() (*httptest.Server, *config.ServerDetails) { return validations.XscServer(t, "") }, expectedResponse: false, }, { serverCreationFunc: func() (*httptest.Server, *config.ServerDetails) { - return utils.XscServer(t, unsupportedXscVersionForErrorLogs) + return validations.XscServer(t, unsupportedXscVersionForErrorLogs) }, expectedResponse: false, }, { serverCreationFunc: func() (*httptest.Server, *config.ServerDetails) { - return utils.XscServer(t, supportedXscVersionForErrorLogs) + return validations.XscServer(t, supportedXscVersionForErrorLogs) }, expectedResponse: true, }, diff --git a/xsc_test.go b/xsc_test.go index c9da0e4c..28787968 100644 --- a/xsc_test.go +++ b/xsc_test.go @@ -12,7 +12,8 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-security/formats" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/validations" "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" "github.com/jfrog/jfrog-cli-security/utils/xsc" @@ -57,28 +58,41 @@ func TestXscAuditNpmJsonWithWatch(t *testing.T) { restoreFunc := initXscTest(t) defer restoreFunc() output := testAuditNpm(t, string(format.Json), false) - securityTestUtils.VerifyJsonScanResults(t, output, 1, 0, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + SecurityViolations: 1, + Licenses: 1, + }) } func TestXscAuditNpmSimpleJsonWithWatch(t *testing.T) { restoreFunc := initXscTest(t) defer restoreFunc() output := testAuditNpm(t, string(format.SimpleJson), true) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 1, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + SecurityViolations: 1, + Vulnerabilities: 1, + Licenses: 1, + }) } func TestXscAuditMavenJson(t *testing.T) { restoreFunc := initXscTest(t) defer restoreFunc() output := testXscAuditMaven(t, string(format.Json)) - securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) + validations.VerifyJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func TestXscAuditMavenSimpleJson(t *testing.T) { restoreFunc := initXscTest(t) defer restoreFunc() output := testXscAuditMaven(t, string(format.SimpleJson)) - securityTestUtils.VerifySimpleJsonScanResults(t, output, 0, 1, 1) + validations.VerifySimpleJsonResults(t, output, validations.ValidationParams{ + Vulnerabilities: 1, + Licenses: 1, + }) } func TestXscAnalyticsForAudit(t *testing.T) {