From be9cf8ef26c7989f76731c4d90efbf5b6087cae0 Mon Sep 17 00:00:00 2001 From: Bar Vered <161704690+barv-jfrog@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:41:54 +0300 Subject: [PATCH] Secret Token Validation feature (#144) --- audit_test.go | 39 ++++++---- cli/docs/flags.go | 6 +- cli/scancommands.go | 10 ++- commands/audit/audit.go | 5 +- commands/audit/audit_test.go | 4 +- commands/scan/scan.go | 17 ++-- formats/conversion.go | 18 ++++- formats/sarifutils/sarifutils.go | 11 +++ formats/sarifutils/test_sarifutils.go | 14 ++++ formats/simplejsonapi.go | 9 ++- formats/table.go | 10 ++- jas/analyzermanager.go | 2 + jas/common.go | 23 +++++- jas/common_test.go | 34 +++++--- jas/runner/jasrunner_test.go | 6 +- scans_test.go | 28 +++++-- tests/config.go | 4 +- tests/testdata/other/enrich/enrich.json | 2 +- tests/testdata/other/enrich/enrich.xml | 2 +- .../jas/jas/secrets/api_secrets/tokens | 6 ++ tests/utils/test_validation.go | 11 ++- utils/jasutils/jasutils.go | 40 ++++++++++ utils/results.go | 1 + utils/resultstable.go | 32 +++++++- utils/resultstable_test.go | 78 ++++++++++++++++++- utils/resultwriter.go | 9 ++- utils/utils.go | 14 ++-- 27 files changed, 354 insertions(+), 81 deletions(-) create mode 100644 tests/testdata/projects/jas/jas/secrets/api_secrets/tokens diff --git a/audit_test.go b/audit_test.go index 53916424..cd8ed215 100644 --- a/audit_test.go +++ b/audit_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "github.com/jfrog/jfrog-cli-security/utils/jasutils" "os" "os/exec" "path/filepath" @@ -340,7 +341,7 @@ func TestXrayAuditMultiProjects(t *testing.T) { 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, 24, 2, 1) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 6, 3, 0, 24, 2, 1, 0) } func TestXrayAuditPipJson(t *testing.T) { @@ -445,11 +446,11 @@ func addDummyPackageDescriptor(t *testing.T, hasPackageJson bool) { func TestXrayAuditNotEntitledForJas(t *testing.T) { cliToRun, cleanUp := securityTestUtils.InitTestWithMockCommandOrParams(t, getNoJasAuditMockCommand) defer cleanUp() - output := testXrayAuditJas(t, cliToRun, filepath.Join("jas", "jas"), "3") + 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) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, 0) } func getNoJasAuditMockCommand() components.Command { @@ -469,29 +470,35 @@ func getNoJasAuditMockCommand() components.Command { } func TestXrayAuditJasSimpleJson(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "3") + 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) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 6, 3, 1, 1, 2, 0, 0) +} + +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) } func TestXrayAuditJasSimpleJsonWithOneThread(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas"), "1") + 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) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 6, 3, 1, 1, 2, 0, 0) } func TestXrayAuditJasSimpleJsonWithConfig(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("jas", "jas-config"), "3") - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 1, 3, 1, 1, 2, 0) + 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) } func TestXrayAuditJasNoViolationsSimpleJson(t *testing.T) { - output := testXrayAuditJas(t, securityTests.PlatformCli, filepath.Join("package-managers", "npm", "npm"), "3") + 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) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 1, 0, 0) } -func testXrayAuditJas(t *testing.T, testCli *coreTests.JfrogCli, project string, threads string) string { +func testXrayAuditJas(t *testing.T, testCli *coreTests.JfrogCli, project string, threads string, validateSecrets bool) string { securityTestUtils.InitSecurityTest(t, scangraph.GraphScanMinXrayVersion) tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) defer createTempDirCallback() @@ -505,7 +512,11 @@ func testXrayAuditJas(t *testing.T, testCli *coreTests.JfrogCli, project string, assert.NoError(t, err) chdirCallback := clientTests.ChangeDirWithCallback(t, baseWd, tempDirPath) defer chdirCallback() - return testCli.WithoutCredentials().RunCliCmdWithOutput(t, "audit", "--format="+string(format.SimpleJson), "--threads="+threads) + args := []string{"audit", "--format=" + string(format.SimpleJson), "--threads=" + threads} + if validateSecrets { + args = append(args, "--secrets", "--validate-secrets") + } + return testCli.WithoutCredentials().RunCliCmdWithOutput(t, args...) } func TestXrayAuditDetectTech(t *testing.T) { @@ -577,5 +588,5 @@ 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) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, 0) } diff --git a/cli/docs/flags.go b/cli/docs/flags.go index e1caf48b..bf9643e7 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -106,6 +106,7 @@ const ( buildPrefix = "build-" BuildVuln = buildPrefix + Vuln ScanVuln = scanPrefix + Vuln + SecretValidation = "validate-secrets" // Unique audit flags auditPrefix = "audit-" @@ -147,13 +148,13 @@ var commandFlags = map[string][]string{ url, user, password, accessToken, ServerId, Project, BuildVuln, OutputFormat, Fail, ExtendedTable, Rescan, }, DockerScan: { - ServerId, Project, Watches, RepoPath, Licenses, OutputFormat, Fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, ScanVuln, + ServerId, Project, Watches, RepoPath, Licenses, OutputFormat, Fail, ExtendedTable, BypassArchiveLimits, MinSeverity, FixableOnly, ScanVuln, SecretValidation, }, Audit: { 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, OutputDir, + Sca, Iac, Sast, Secrets, WithoutCA, ScanVuln, SecretValidation, OutputDir, }, CurationAudit: { CurationOutput, WorkingDirs, Threads, RequirementsFile, @@ -258,6 +259,7 @@ var flagsMap = map[string]components.Flag{ Sast: components.NewBoolFlag(Sast, fmt.Sprintf("Selective scanners mode: Execute SAST sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Secrets, Iac)), Secrets: components.NewBoolFlag(Secrets, fmt.Sprintf("Selective scanners mode: Execute Secrets sub-scan. Can be combined with --%s, --%s and --%s.", Sca, Sast, Iac)), WithoutCA: components.NewBoolFlag(WithoutCA, fmt.Sprintf("Selective scanners mode: Disable Contextual Analysis scanner after SCA. Relevant only with --%s flag.", Sca)), + SecretValidation: components.NewBoolFlag(SecretValidation, fmt.Sprintf("Selective scanners mode: Execute Token validation sub-scan on secrets. Relevant only with --%s flag.", Secrets)), // Git flags InputFile: components.NewStringFlag(InputFile, "Path to an input file in YAML format contains multiple git providers. With this option, all other scm flags will be ignored and only git servers mentioned in the file will be examined.."), diff --git a/cli/scancommands.go b/cli/scancommands.go index aaac4152..fcd620e3 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -396,6 +396,11 @@ func AuditCmd(c *components.Context) error { return pluginsCommon.PrintHelpAndReturnError(fmt.Sprintf("flag '--%s' cannot be used without '--%s'", flags.WithoutCA, flags.Sca), c) } + if c.GetBoolFlagValue(flags.SecretValidation) && !c.GetBoolFlagValue(flags.Secrets) { + // No secrets flag but secret validation is provided, error + return pluginsCommon.PrintHelpAndReturnError(fmt.Sprintf("flag '--%s' cannot be used without '--%s'", flags.SecretValidation, flags.Secrets), c) + } + allSubScans := utils.GetAllSupportedScans() subScans := []utils.SubScanType{} for _, subScan := range allSubScans { @@ -420,7 +425,7 @@ func AuditCmd(c *components.Context) error { func shouldAddSubScan(subScan utils.SubScanType, c *components.Context) bool { return c.GetBoolFlagValue(subScan.String()) || - (subScan == utils.ContextualAnalysisScan && c.GetBoolFlagValue(flags.Sca) && !c.GetBoolFlagValue(flags.WithoutCA)) + (subScan == utils.ContextualAnalysisScan && c.GetBoolFlagValue(flags.Sca) && !c.GetBoolFlagValue(flags.WithoutCA)) || (subScan == utils.SecretTokenValidationScan && c.GetBoolFlagValue(flags.Secrets) && c.GetBoolFlagValue(flags.SecretValidation)) } func reportErrorIfExists(err error, auditCmd *audit.AuditCommand) { @@ -724,7 +729,8 @@ func DockerScan(c *components.Context, image string) error { SetFixableOnly(c.GetBoolFlagValue(flags.FixableOnly)). SetMinSeverityFilter(minSeverity). SetThreads(threads). - SetAnalyticsMetricsService(xsc.NewAnalyticsMetricsService(serverDetails)) + SetAnalyticsMetricsService(xsc.NewAnalyticsMetricsService(serverDetails)). + SetSecretValidation(c.GetBoolFlagValue(flags.SecretValidation)) if c.GetStringFlagValue(flags.Watches) != "" { containerScanCommand.SetWatches(splitByCommaAndTrim(c.GetStringFlagValue(flags.Watches))) } diff --git a/commands/audit/audit.go b/commands/audit/audit.go index bd9ad907..8f93a52a 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -13,6 +13,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils" "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" @@ -185,8 +186,8 @@ func RunAudit(auditParams *AuditParams) (results *utils.Results, err error) { 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) @@ -251,7 +252,7 @@ func downloadAnalyzerManagerAndRunScanners(auditParallelRunner *utils.SecurityPa if err = jas.DownloadAnalyzerManagerIfNeeded(threadId); err != nil { return fmt.Errorf("%s failed to download analyzer manager: %s", clientutils.GetLogMsgPrefix(threadId, false), err.Error()) } - scanner, err = jas.CreateJasScanner(scanner, jfrogAppsConfig, serverDetails, jas.GetAnalyzerManagerXscEnvVars(auditParams.commonGraphScanParams.MultiScanId, scanResults.GetScaScannedTechnologies()...), auditParams.Exclusions()...) + scanner, err = jas.CreateJasScanner(scanner, jfrogAppsConfig, serverDetails, jas.GetAnalyzerManagerXscEnvVars(auditParams.commonGraphScanParams.MultiScanId, scanResults.ExtendedScanResults.SecretValidation, scanResults.GetScaScannedTechnologies()...), auditParams.Exclusions()...) if err != nil { return fmt.Errorf("failed to create jas scanner: %s", err.Error()) } diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 0ef8f40e..915e64a0 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -43,7 +43,7 @@ func TestAuditWithConfigProfile(t *testing.T) { IsDefault: false, }, expectedSastIssues: 0, - expectedSecretsIssues: 7, + expectedSecretsIssues: 16, }, { name: "Enable only sast scanner", @@ -87,7 +87,7 @@ func TestAuditWithConfigProfile(t *testing.T) { IsDefault: false, }, expectedSastIssues: 1, - expectedSecretsIssues: 7, + expectedSecretsIssues: 16, }, } diff --git a/commands/scan/scan.go b/commands/scan/scan.go index c6f6d83b..1d34dbef 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -71,6 +71,7 @@ type ScanCommand struct { includeLicenses bool fail bool printExtendedTable bool + validateSecrets bool bypassArchiveLimits bool fixableOnly bool progress ioUtils.ProgressMgr @@ -83,6 +84,11 @@ func (scanCmd *ScanCommand) SetMinSeverityFilter(minSeverityFilter severityutils return scanCmd } +func (scanCmd *ScanCommand) SetSecretValidation(validateSecrets bool) *ScanCommand { + scanCmd.validateSecrets = validateSecrets + return scanCmd +} + func (scanCmd *ScanCommand) SetFixableOnly(fixable bool) *ScanCommand { scanCmd.fixableOnly = fixable return scanCmd @@ -230,6 +236,7 @@ func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recor } 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 { // Download (if needed) the analyzer manager in a background routine. @@ -288,7 +295,7 @@ func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recor jasScanProducerErrors := make([][]formats.SimpleJsonError, 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, resultsArr, fileProducerErrors, indexedFileProducerErrors, jasScanProducerErrors, fileCollectingErrorsQueue, xrayVersion) + scanCmd.prepareScanTasks(fileProducerConsumer, indexedFileProducerConsumer, &JasScanProducerConsumer, scanResults.ExtendedScanResults.EntitledForJas, scanResults.ExtendedScanResults.SecretValidation, resultsArr, fileProducerErrors, indexedFileProducerErrors, jasScanProducerErrors, fileCollectingErrorsQueue, xrayVersion) scanCmd.performScanTasks(fileProducerConsumer, indexedFileProducerConsumer, &JasScanProducerConsumer) // Handle results @@ -356,14 +363,14 @@ func (scanCmd *ScanCommand) CommandName() string { return "xr_scan" } -func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer parallel.Runner, jasFileProducerConsumer *utils.SecurityParallelRunner, entitledForJas bool, resultsArr [][]*ScanInfo, fileErrors, indexedFileErrors, jasErrors [][]formats.SimpleJsonError, fileCollectingErrorsQueue *clientutils.ErrorsQueue, xrayVersion string) { +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) { 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, indexedFileProducer, jasFileProducerConsumer, resultsArr, fileErrors, indexedFileErrors, jasErrors, xrayVersion) + artifactHandlerFunc := scanCmd.createIndexerHandlerFunc(&specFiles[i], entitledForJas, validateSecrets, indexedFileProducer, jasFileProducerConsumer, resultsArr, fileErrors, indexedFileErrors, jasErrors, xrayVersion) taskHandler := getAddTaskToProducerFunc(fileProducer, artifactHandlerFunc) err := collectFilesForIndexing(specFiles[i], taskHandler) @@ -375,7 +382,7 @@ func (scanCmd *ScanCommand) prepareScanTasks(fileProducer, indexedFileProducer p }() } -func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, entitledForJas bool, indexedFileProducer parallel.Runner, jasFileProducerConsumer *utils.SecurityParallelRunner, resultsArr [][]*ScanInfo, fileErrors, indexedFileErrors, jasErrors [][]formats.SimpleJsonError, xrayVersion string) FileContext { +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 { return func(filePath string) parallel.TaskFunc { return func(threadId int) (err error) { logMsgPrefix := clientutils.GetLogMsgPrefix(threadId, false) @@ -445,7 +452,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, entitledFo indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) } scanner := &jas.JasScanner{} - scanner, err = jas.CreateJasScanner(scanner, jfrogAppsConfig, scanCmd.serverDetails, jas.GetAnalyzerManagerXscEnvVars(scanResults.MultiScanId, techutils.Technology(graphScanResults.ScannedPackageType))) + scanner, err = jas.CreateJasScanner(scanner, jfrogAppsConfig, scanCmd.serverDetails, 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()}) diff --git a/formats/conversion.go b/formats/conversion.go index 8a602869..49c97e8e 100644 --- a/formats/conversion.go +++ b/formats/conversion.go @@ -1,6 +1,7 @@ package formats import ( + "github.com/jfrog/jfrog-cli-security/utils/jasutils" "strconv" "strings" ) @@ -143,12 +144,21 @@ func ConvertToOperationalRiskViolationScanTableRow(rows []OperationalRiskViolati func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow) { for i := range rows { + var status string + var info string + if rows[i].Applicability != nil { + status = rows[i].Applicability.Status + info = rows[i].Applicability.ScannerDescription + } tableRows = append(tableRows, secretsTableRow{ - severity: rows[i].Severity, - file: rows[i].File, - lineColumn: strconv.Itoa(rows[i].StartLine) + ":" + strconv.Itoa(rows[i].StartColumn), - secret: rows[i].Snippet, + severity: rows[i].Severity, + file: rows[i].File, + lineColumn: strconv.Itoa(rows[i].StartLine) + ":" + strconv.Itoa(rows[i].StartColumn), + secret: rows[i].Snippet, + tokenValidation: jasutils.TokenValidationStatus(status).ToString(), + tokenInfo: info, }) + } return } diff --git a/formats/sarifutils/sarifutils.go b/formats/sarifutils/sarifutils.go index 32a6f142..3a5abe96 100644 --- a/formats/sarifutils/sarifutils.go +++ b/formats/sarifutils/sarifutils.go @@ -88,6 +88,17 @@ 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 { diff --git a/formats/sarifutils/test_sarifutils.go b/formats/sarifutils/test_sarifutils.go index 4d557fb8..d044de78 100644 --- a/formats/sarifutils/test_sarifutils.go +++ b/formats/sarifutils/test_sarifutils.go @@ -63,6 +63,20 @@ func CreateDummyResult(markdown, msg, ruleId, level string) *sarif.Result { } } +func CreateResultWithProperties(msg, ruleId, level string, properties map[string]string, locations ...*sarif.Location) *sarif.Result { + result := &sarif.Result{ + Message: *sarif.NewTextMessage(msg), + Level: &level, + RuleID: &ruleId, + Locations: locations, + } + result.Properties = map[string]interface{}{} + for key, val := range properties { + result.Properties[key] = val + } + return result +} + func CreateResultWithDummyLocationAmdProperty(fileName, property, value string) *sarif.Result { resultWithLocation := CreateDummyResultInPath(fileName) resultWithLocation.Properties = map[string]interface{}{property: value} diff --git a/formats/simplejsonapi.go b/formats/simplejsonapi.go index 56aae14e..1da5b1f8 100644 --- a/formats/simplejsonapi.go +++ b/formats/simplejsonapi.go @@ -67,10 +67,11 @@ type OperationalRiskViolationRow struct { type SourceCodeRow struct { SeverityDetails Location - Finding string `json:"finding,omitempty"` - Fingerprint string `json:"fingerprint,omitempty"` - ScannerDescription string `json:"scannerDescription,omitempty"` - CodeFlow [][]Location `json:"codeFlow,omitempty"` + Finding string `json:"finding,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Applicability *Applicability `json:"applicability,omitempty"` + ScannerDescription string `json:"scannerDescription,omitempty"` + CodeFlow [][]Location `json:"codeFlow,omitempty"` } type Location struct { diff --git a/formats/table.go b/formats/table.go index fc9486d9..1734463e 100644 --- a/formats/table.go +++ b/formats/table.go @@ -124,10 +124,12 @@ type cveTableRow struct { } type secretsTableRow struct { - severity string `col-name:"Severity"` - file string `col-name:"File"` - lineColumn string `col-name:"Line:Column"` - secret string `col-name:"Secret"` + severity string `col-name:"Severity"` + file string `col-name:"File"` + lineColumn string `col-name:"Line:Column"` + secret string `col-name:"Secret"` + tokenValidation string `col-name:"Token Validation" omitempty:"true"` + tokenInfo string `col-name:"Token Info" omitempty:"true"` } type iacOrSastTableRow struct { diff --git a/jas/analyzermanager.go b/jas/analyzermanager.go index 1a61320c..4222119c 100644 --- a/jas/analyzermanager.go +++ b/jas/analyzermanager.go @@ -41,6 +41,8 @@ const ( jfrogCliAnalyzerManagerVersionEnvVariable = "JFROG_CLI_ANALYZER_MANAGER_VERSION" JfPackageManagerEnvVariable = "AM_PACKAGE_MANAGER" JfLanguageEnvVariable = "AM_LANGUAGE" + // #nosec G101 -- Not credentials. + JfSecretValidationEnvVariable = "JF_VALIDATE_SECRETS" ) var exitCodeErrorsMap = map[int]string{ diff --git a/jas/common.go b/jas/common.go index bad296ca..dd547435 100644 --- a/jas/common.go +++ b/jas/common.go @@ -216,7 +216,7 @@ func InitJasTest(t *testing.T, workingDirs ...string) (*JasScanner, func()) { jfrogAppsConfigForTest, err := CreateJFrogAppsConfig(workingDirs) assert.NoError(t, err) scanner := &JasScanner{} - scanner, err = CreateJasScanner(scanner, jfrogAppsConfigForTest, &FakeServerDetails, GetAnalyzerManagerXscEnvVars("")) + scanner, err = CreateJasScanner(scanner, jfrogAppsConfigForTest, &FakeServerDetails, GetAnalyzerManagerXscEnvVars("", false)) assert.NoError(t, err) return scanner, func() { assert.NoError(t, scanner.ScannerDirCleanupFunc()) @@ -273,8 +273,27 @@ func convertToFilesExcludePatterns(excludePatterns []string) []string { return patterns } -func GetAnalyzerManagerXscEnvVars(msi string, technologies ...techutils.Technology) map[string]string { +func CheckForSecretValidation(xrayManager *xray.XrayServicesManager, xrayVersion string, validateSecrets bool) bool { + dynamicTokenVersionMismatchErr := goclientutils.ValidateMinimumVersion(goclientutils.Xray, xrayVersion, jasutils.DynamicTokenValidationMinXrayVersion) + if dynamicTokenVersionMismatchErr != nil { + if validateSecrets { + log.Info(fmt.Sprintf("Token validation (--validate-secrets flag) is not supported in your xray version, your xray version is %s and the minimum is %s", xrayVersion, jasutils.DynamicTokenValidationMinXrayVersion)) + } + return false + } + // Ordered By importance + // first check for flag and second check for env var + if validateSecrets || strings.ToLower(os.Getenv(JfSecretValidationEnvVariable)) == "true" { + return true + } + // third check for platform api + isEnabled, err := xrayManager.IsTokenValidationEnabled() + return err == nil && isEnabled +} + +func GetAnalyzerManagerXscEnvVars(msi string, validateSecrets bool, 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 50ef1a44..15d7cef2 100644 --- a/jas/common_test.go +++ b/jas/common_test.go @@ -149,19 +149,21 @@ func TestGetAnalyzerManagerEnvVariables(t *testing.T) { func TestGetAnalyzerManagerXscEnvVars(t *testing.T) { tests := []struct { - name string - msi string - technologies []techutils.Technology - expectedOutput map[string]string + name string + msi string + validateSecrets bool + 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), - utils.JfMsiEnvVariable: "msi", + JfPackageManagerEnvVariable: string(techutils.Maven), + JfLanguageEnvVariable: string(techutils.Java), + JfSecretValidationEnvVariable: "false", + utils.JfMsiEnvVariable: "msi", }, }, { @@ -169,7 +171,8 @@ func TestGetAnalyzerManagerXscEnvVars(t *testing.T) { msi: "msi", technologies: []techutils.Technology{techutils.Maven, techutils.Npm}, expectedOutput: map[string]string{ - utils.JfMsiEnvVariable: "msi", + JfSecretValidationEnvVariable: "false", + utils.JfMsiEnvVariable: "msi", }, }, { @@ -177,13 +180,24 @@ func TestGetAnalyzerManagerXscEnvVars(t *testing.T) { msi: "msi", technologies: []techutils.Technology{}, expectedOutput: map[string]string{ - utils.JfMsiEnvVariable: "msi", + 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", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expectedOutput, GetAnalyzerManagerXscEnvVars(test.msi, test.technologies...)) + assert.Equal(t, test.expectedOutput, GetAnalyzerManagerXscEnvVars(test.msi, test.validateSecrets, test.technologies...)) }) } } diff --git a/jas/runner/jasrunner_test.go b/jas/runner/jasrunner_test.go index 72ca13d5..878ce1e8 100644 --- a/jas/runner/jasrunner_test.go +++ b/jas/runner/jasrunner_test.go @@ -27,7 +27,7 @@ func TestGetExtendedScanResults_AnalyzerManagerDoesntExist(t *testing.T) { assert.NoError(t, os.Unsetenv(coreutils.HomeDir)) }() scanner := &jas.JasScanner{} - _, err = jas.CreateJasScanner(scanner, nil, &jas.FakeServerDetails, jas.GetAnalyzerManagerXscEnvVars("")) + _, err = jas.CreateJasScanner(scanner, nil, &jas.FakeServerDetails, jas.GetAnalyzerManagerXscEnvVars("", false)) assert.Error(t, err) assert.ErrorContains(t, err, "unable to locate the analyzer manager package. Advanced security scans cannot be performed without this package") } @@ -37,7 +37,7 @@ func TestGetExtendedScanResults_ServerNotValid(t *testing.T) { scanResults := &utils.Results{ScaResults: []*utils.ScaScanResult{{Technology: techutils.Pip, XrayResults: jas.FakeBasicXrayResults}}, ExtendedScanResults: &utils.ExtendedScanResults{}} scanner := &jas.JasScanner{} - jasScanner, err := jas.CreateJasScanner(scanner, nil, &jas.FakeServerDetails, jas.GetAnalyzerManagerXscEnvVars("", scanResults.GetScaScannedTechnologies()...)) + jasScanner, err := jas.CreateJasScanner(scanner, nil, &jas.FakeServerDetails, jas.GetAnalyzerManagerXscEnvVars("", false, scanResults.GetScaScannedTechnologies()...)) assert.NoError(t, err) err = AddJasScannersTasks(securityParallelRunnerForTest, scanResults, &[]string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, nil, false, jasScanner, applicability.ApplicabilityScannerType, secrets.SecretsScannerType, securityParallelRunnerForTest.AddErrorToChan, utils.GetAllSupportedScans(), nil, "") assert.NoError(t, err) @@ -48,7 +48,7 @@ func TestGetExtendedScanResults_AnalyzerManagerReturnsError(t *testing.T) { jfrogAppsConfigForTest, _ := jas.CreateJFrogAppsConfig(nil) scanner := &jas.JasScanner{} - scanner, _ = jas.CreateJasScanner(scanner, nil, &jas.FakeServerDetails, jas.GetAnalyzerManagerXscEnvVars("")) + scanner, _ = jas.CreateJasScanner(scanner, 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) diff --git a/scans_test.go b/scans_test.go index ba6271d6..1cba58c1 100644 --- a/scans_test.go +++ b/scans_test.go @@ -103,6 +103,15 @@ func TestDockerScanWithProgressBar(t *testing.T) { TestDockerScan(t) } +func TestDockerScanWithTokenValidation(t *testing.T) { + securityTestUtils.InitSecurityTest(t, jasutils.DynamicTokenValidationMinXrayVersion) + testCli, cleanup := initNativeDockerWithXrayTest(t) + defer cleanup() + // #nosec G101 -- Image with dummy token for tests + tokensImageToScan := "srmishj/inactive_tokens:latest" + runDockerScan(t, testCli, tokensImageToScan, "", 0, 0, 0, 5, true) +} + func TestDockerScan(t *testing.T) { testCli, cleanup := initNativeDockerWithXrayTest(t) defer cleanup() @@ -118,7 +127,7 @@ func TestDockerScan(t *testing.T) { "redhat/ubi8-micro:8.4", } for _, imageName := range imagesToScan { - runDockerScan(t, testCli, imageName, watchName, 3, 3, 3) + runDockerScan(t, testCli, imageName, watchName, 3, 3, 3, 0, false) } // On Xray 3.40.3 there is a bug whereby xray fails to scan docker image with 0 vulnerabilities, @@ -126,7 +135,7 @@ func TestDockerScan(t *testing.T) { securityTestUtils.ValidateXrayVersion(t, "3.41.0") // Image with 0 vulnerabilities - runDockerScan(t, testCli, "busybox:1.35", "", 0, 0, 0) + runDockerScan(t, testCli, "busybox:1.35", "", 0, 0, 0, 0, false) } func initNativeDockerWithXrayTest(t *testing.T) (mockCli *coreTests.JfrogCli, cleanUp func()) { @@ -136,7 +145,7 @@ func initNativeDockerWithXrayTest(t *testing.T) (mockCli *coreTests.JfrogCli, cl return securityTestUtils.InitTestWithMockCommandOrParams(t, cli.DockerScanMockCommand) } -func runDockerScan(t *testing.T, testCli *coreTests.JfrogCli, imageName, watchName string, minViolations, minVulnerabilities, minLicenses int) { +func runDockerScan(t *testing.T, testCli *coreTests.JfrogCli, imageName, watchName string, minViolations, minVulnerabilities, minLicenses int, minInactives int, validateSecrets bool) { // Pull image from docker repo imageTag := path.Join(*securityTests.ContainerRegistry, securityTests.DockerVirtualRepo, imageName) dockerPullCommand := container.NewPullCommand(containerUtils.DockerClient) @@ -144,10 +153,19 @@ func runDockerScan(t *testing.T, testCli *coreTests.JfrogCli, imageName, watchNa if assert.NoError(t, dockerPullCommand.Run()) { defer commonTests.DeleteTestImage(t, imageTag, containerUtils.DockerClient) // Run docker scan on image - cmdArgs := []string{"docker", "scan", imageTag, "--server-id=default", "--licenses", "--format=json", "--fail=false", "--min-severity=low", "--fixable-only"} + cmdArgs := []string{"docker", "scan", imageTag, "--server-id=default", "--licenses", "--fail=false", "--min-severity=low", "--fixable-only"} + if validateSecrets { + cmdArgs = append(cmdArgs, "--validate-secrets", "--format=simple-json") + } else { + cmdArgs = append(cmdArgs, "--format=json") + } output := testCli.WithoutCredentials().RunCliCmdWithOutput(t, cmdArgs...) if assert.NotEmpty(t, output) { - securityTestUtils.VerifyJsonScanResults(t, output, 0, minVulnerabilities, minLicenses) + if validateSecrets { + securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 0, 0, minInactives) + } else { + securityTestUtils.VerifyJsonScanResults(t, output, 0, minVulnerabilities, minLicenses) + } } // Run docker scan on image with watch if watchName == "" { diff --git a/tests/config.go b/tests/config.go index 743ab542..5f6e8e3f 100644 --- a/tests/config.go +++ b/tests/config.go @@ -53,14 +53,14 @@ func init() { TestSecurity = flag.Bool("test.security", true, "Test Security") TestDockerScan = flag.Bool("test.dockerScan", false, "Test Docker scan") - JfrogUrl = flag.String("jfrog.url", "http://localhost:8081/", "JFrog platform url") + JfrogUrl = flag.String("jfrog.url", "http://localhost:8083/", "JFrog platform url") JfrogUser = flag.String("jfrog.user", "admin", "JFrog platform username") JfrogPassword = flag.String("jfrog.password", "password", "JFrog platform password") JfrogSshKeyPath = flag.String("jfrog.sshKeyPath", "", "Ssh key file path") JfrogSshPassphrase = flag.String("jfrog.sshPassphrase", "", "Ssh key passphrase") JfrogAccessToken = flag.String("jfrog.adminToken", "", "JFrog platform admin token") - ContainerRegistry = flag.String("test.containerRegistry", "localhost:8082", "Container registry") + ContainerRegistry = flag.String("test.containerRegistry", "localhost:8083", "Container registry") HideUnitTestLog = flag.Bool("test.hideUnitTestLog", false, "Hide unit tests logs and print it in a file") SkipUnitTests = flag.Bool("test.skipUnitTests", false, "Skip unit tests") diff --git a/tests/testdata/other/enrich/enrich.json b/tests/testdata/other/enrich/enrich.json index 40ba81ec..8f8da420 100755 --- a/tests/testdata/other/enrich/enrich.json +++ b/tests/testdata/other/enrich/enrich.json @@ -6,7 +6,7 @@ "metadata": { "timestamp": "2024-05-22T08:20:45Z", "component": { - "bom-ref": "pypi://services/web@main", + "bom-ref": "pypggi://services/web@main", "type": "library", "name": "pypi://services/web@main", "version": "main", diff --git a/tests/testdata/other/enrich/enrich.xml b/tests/testdata/other/enrich/enrich.xml index 5368b18e..95430da0 100644 --- a/tests/testdata/other/enrich/enrich.xml +++ b/tests/testdata/other/enrich/enrich.xml @@ -11,7 +11,7 @@ jfrog/jfrog-cli-v2-jf - 2.58.2 + 2.ss58.2 diff --git a/tests/testdata/projects/jas/jas/secrets/api_secrets/tokens b/tests/testdata/projects/jas/jas/secrets/api_secrets/tokens new file mode 100644 index 00000000..feb5fe73 --- /dev/null +++ b/tests/testdata/projects/jas/jas/secrets/api_secrets/tokens @@ -0,0 +1,6 @@ +API_KEY=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 +API_KEY_TWO=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 +API_KEY_THREE=ghp_SerULubAEgmUvNss9EJMJLVYZ2Iprm29Mdlg +API_KEY_FOUR=sl.BxYhe_DcMZdu0TiwIqjSxxWOG02QQ2aPk17N94bsnyPCNoZ1IbD5as-LUOFRF-t7qKJaoUUz_XpFxs1WFe9nZZXMkUxcXuyoV6Caje3E8Lyx55w0oTEo29Fpolfg8d_QEBIvqpJUiB9C +API_KEY_FIVE=sl.BxPWLN25rQ2VVsFIy0AvjHY-Tiy1Pjuu9VZ-62fobHruF_5ANABMm48aG4Tjx6YohNF7mZia2zZVLbvqTlklsAQe41zzz0lqOp1dyCf-FFY-GFC9gHZ-gECgytQL8WW26j77W3jcS4ee +API_KEY_SIX=glpat-QXnCVeWcFaUMRZM9sAUZ \ No newline at end of file diff --git a/tests/utils/test_validation.go b/tests/utils/test_validation.go index afd70be6..dcb46463 100644 --- a/tests/utils/test_validation.go +++ b/tests/utils/test_validation.go @@ -66,7 +66,7 @@ func VerifySimpleJsonScanResults(t *testing.T, content string, minViolations, mi } func VerifySimpleJsonJasResults(t *testing.T, content string, minSastViolations, minIacViolations, minSecrets, - minApplicable, minUndetermined, minNotCovered, minNotApplicable, minMissingContext int) { + minApplicable, minUndetermined, minNotCovered, minNotApplicable, minMissingContext, minInactives int) { var results formats.SimpleJsonResults err := json.Unmarshal([]byte(content), &results) if assert.NoError(t, err) { @@ -88,6 +88,15 @@ func VerifySimpleJsonJasResults(t *testing.T, content string, minSastViolations, 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") diff --git a/utils/jasutils/jasutils.go b/utils/jasutils/jasutils.go index 80659b61..a889864d 100644 --- a/utils/jasutils/jasutils.go +++ b/utils/jasutils/jasutils.go @@ -10,6 +10,14 @@ const ( ApplicabilityRuleIdPrefix = "applic_" ) +const ( + DynamicTokenValidationMinXrayVersion = "3.101.0" +) + +const ( + TokenValidationStatusForNonTokens = "Not a token" +) + const ( Applicability JasScanType = "Applicability" Secrets JasScanType = "Secrets" @@ -17,12 +25,35 @@ const ( Sast JasScanType = "Sast" ) +const ( + Active TokenValidationStatus = "Active" + Inactive TokenValidationStatus = "Inactive" + Unsupported TokenValidationStatus = "Unsupported" + Unavailable TokenValidationStatus = "Unavailable" + NotAToken TokenValidationStatus = TokenValidationStatusForNonTokens +) + +type TokenValidationStatus string + type JasScanType string func (jst JasScanType) String() string { return string(jst) } +func (tvs TokenValidationStatus) String() string { return string(tvs) } + +func (tvs TokenValidationStatus) ToString() string { + switch tvs { + case Active: + return color.New(color.Red).Render(tvs) + case Inactive: + return color.New(color.Green).Render(tvs) + default: + return tvs.String() + } +} + type ApplicabilityStatus string const ( @@ -88,6 +119,15 @@ var applicableMapToScore = map[string]int{ "NotApplicable": 0, } +var TokenValidationOrder = map[string]int{ + "Active": 1, + "Unsupported": 2, + "Unavailable": 3, + "Inactive": 4, + "Not a token": 5, + "": 6, +} + func ConvertApplicableToScore(applicability string) int { if level, ok := applicableMapToScore[strings.ToLower(applicability)]; ok { return level diff --git a/utils/results.go b/utils/results.go index bcee459b..e78e2cf7 100644 --- a/utils/results.go +++ b/utils/results.go @@ -115,6 +115,7 @@ type ExtendedScanResults struct { IacScanResults []*sarif.Run SastScanResults []*sarif.Run EntitledForJas bool + SecretValidation bool } func (e *ExtendedScanResults) IsIssuesFound() bool { diff --git a/utils/resultstable.go b/utils/resultstable.go index 0e3599d4..6d25da70 100644 --- a/utils/resultstable.go +++ b/utils/resultstable.go @@ -352,6 +352,13 @@ func prepareSecrets(secrets []*sarif.Run, isTable bool) []formats.SourceCodeRow 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), @@ -365,6 +372,7 @@ func prepareSecrets(secrets []*sarif.Run, isTable bool) []formats.SourceCodeRow EndColumn: sarifutils.GetLocationEndColumn(location), Snippet: sarifutils.GetLocationSnippet(location), }, + Applicability: applicability, }, ) } @@ -372,18 +380,28 @@ func prepareSecrets(secrets []*sarif.Run, isTable bool) []formats.SourceCodeRow } sort.Slice(secretsRows, func(i, j int) bool { - return secretsRows[i].SeverityNumValue > secretsRows[j].SeverityNumValue + 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) error { +func PrintSecretsTable(secrets []*sarif.Run, entitledForSecretsScan bool, tokenValidationEnabled bool) error { if entitledForSecretsScan { secretsRows := prepareSecrets(secrets, true) log.Output() - return coreutils.PrintTable(formats.ConvertToSecretsTableRow(secretsRows), "Secret Detection", + 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 } @@ -1033,6 +1051,14 @@ 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) diff --git a/utils/resultstable_test.go b/utils/resultstable_test.go index ebeab1a0..6c300cea 100644 --- a/utils/resultstable_test.go +++ b/utils/resultstable_test.go @@ -1052,9 +1052,10 @@ func TestPrepareIac(t *testing.T) { func TestPrepareSecrets(t *testing.T) { testCases := []struct { - name string - input []*sarif.Run - expectedOutput []formats.SourceCodeRow + name string + isTokenValidationRun bool + input []*sarif.Run + expectedOutput []formats.SourceCodeRow }{ { name: "No Secret run", @@ -1138,11 +1139,80 @@ func TestPrepareSecrets(t *testing.T) { }, }, }, + { + 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) { - assert.ElementsMatch(t, tc.expectedOutput, prepareSecrets(tc.input, false)) + 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) + } }) } } diff --git a/utils/resultwriter.go b/utils/resultwriter.go index 331d7724..9eb70064 100644 --- a/utils/resultwriter.go +++ b/utils/resultwriter.go @@ -175,7 +175,7 @@ func (rw *ResultsWriter) printScanResultsTables() (err error) { } } if shouldPrintTable(rw.subScansPreformed, SecretsScan, rw.results.ResultType) { - if err = PrintSecretsTable(rw.results.ExtendedScanResults.SecretsScanResults, rw.results.ExtendedScanResults.EntitledForJas); err != nil { + if err = PrintSecretsTable(rw.results.ExtendedScanResults.SecretsScanResults, rw.results.ExtendedScanResults.EntitledForJas, rw.results.ExtendedScanResults.SecretValidation); err != nil { return } } @@ -747,7 +747,7 @@ func getBaseBinaryDescriptionMarkdown(subScanType SubScanType, cmdResults *Resul if len(result.Locations) > 0 { location = result.Locations[0] } - return content + getBinaryLocationMarkdownString(cmdResults.ResultType, subScanType, location) + return content + getBinaryLocationMarkdownString(cmdResults.ResultType, subScanType, location, result) } func getDockerImageTag(cmdResults *Results) string { @@ -766,7 +766,7 @@ func getDockerImageTag(cmdResults *Results) string { // * Layer: // * Filepath: // * Evidence: -func getBinaryLocationMarkdownString(commandType CommandType, subScanType SubScanType, location *sarif.Location) (content string) { +func getBinaryLocationMarkdownString(commandType CommandType, subScanType SubScanType, location *sarif.Location, result *sarif.Result) (content string) { if location == nil { return "" } @@ -788,6 +788,9 @@ func getBinaryLocationMarkdownString(commandType CommandType, subScanType SubSca 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 } diff --git a/utils/utils.go b/utils/utils.go index 2a6faf0e..c429f632 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -25,12 +25,12 @@ var ( ) const ( - ContextualAnalysisScan SubScanType = "contextual_analysis" - ScaScan SubScanType = "sca" - IacScan SubScanType = "iac" - SastScan SubScanType = "sast" - SecretsScan SubScanType = "secrets" - + ContextualAnalysisScan SubScanType = "contextual_analysis" + ScaScan SubScanType = "sca" + IacScan SubScanType = "iac" + SastScan SubScanType = "sast" + SecretsScan SubScanType = "secrets" + SecretTokenValidationScan SubScanType = "secrets_token_validation" ViolationTypeSecurity ViolationIssueType = "security" ViolationTypeLicense ViolationIssueType = "license" ViolationTypeOperationalRisk ViolationIssueType = "operational_risk" @@ -64,7 +64,7 @@ func (s CommandType) IsTargetBinary() bool { } func GetAllSupportedScans() []SubScanType { - return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan} + return []SubScanType{ScaScan, ContextualAnalysisScan, IacScan, SastScan, SecretsScan, SecretTokenValidationScan} } func Md5Hash(values ...string) (string, error) {