diff --git a/go.mod b/go.mod index 7667d869e..a44a8085d 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,9 @@ require ( github.com/jfrog/build-info-go v1.10.7 github.com/jfrog/froggit-go v1.16.2 github.com/jfrog/gofrog v1.7.6 - github.com/jfrog/jfrog-cli-core/v2 v2.57.2 + github.com/jfrog/jfrog-cli-core/v2 v2.57.5 github.com/jfrog/jfrog-cli-security v1.13.6 - github.com/jfrog/jfrog-client-go v1.48.4 + github.com/jfrog/jfrog-client-go v1.48.6 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/owenrumney/go-sarif/v2 v2.3.1 github.com/stretchr/testify v1.10.0 @@ -56,7 +56,7 @@ require ( 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.6.3 // indirect + github.com/jedib0t/go-pretty/v6 v6.6.5 // indirect github.com/jfrog/archiver/v3 v3.6.1 // indirect github.com/jfrog/jfrog-apps-config v1.0.1 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect @@ -64,7 +64,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/ktrysmt/go-bitbucket v0.9.80 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/magiconair/properties v1.8.9 // 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 @@ -106,7 +106,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.28.0 // indirect @@ -118,12 +118,12 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect ) -// replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security dev +replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security v1.13.9-0.20250106070938-333ac33ce675 // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go dev -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go dev +replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20241230154616-e342ed5065f1 // replace github.com/jfrog/froggit-go => github.com/jfrog/froggit-go dev diff --git a/go.sum b/go.sum index 30442997f..1ce2e27a1 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ 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/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.6.3 h1:nGqgS0tgIO1Hto47HSaaK4ac/I/Bu7usmdD3qvs0WvM= -github.com/jedib0t/go-pretty/v6 v6.6.3/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo= +github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= github.com/jfrog/build-info-go v1.10.7 h1:10NVHYg0193gJpQft+S4WQfvYMtj5jlwwhJRvkFJtBE= @@ -129,12 +129,12 @@ 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= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-core/v2 v2.57.2 h1:2shy1CRWm/8yf6WWfVyAW3AdmryQiI73Tkhfb62vgPE= -github.com/jfrog/jfrog-cli-core/v2 v2.57.2/go.mod h1:sgi0gw96J00Yzx2cKG5xTG/x9XD0YiJbglJOnXUeaD0= -github.com/jfrog/jfrog-cli-security v1.13.6 h1:gm8TnGTlprMJbRga0cujeoN2xal7Pagd2kDkUfclvrw= -github.com/jfrog/jfrog-cli-security v1.13.6/go.mod h1:NarJyhl8Kh0HL6br74oeStIosLmCjA7atLYxZIFHJc4= -github.com/jfrog/jfrog-client-go v1.48.4 h1:uXvBr2ebFKpBRUhWgC9TSSJe32IbSYGlbDp9tDzBcaY= -github.com/jfrog/jfrog-client-go v1.48.4/go.mod h1:2ySOMva54L3EYYIlCBYBTcTgqfrrQ19gtpA/MWfA/ec= +github.com/jfrog/jfrog-cli-core/v2 v2.57.5 h1:guVB/zPPtS8CWpNvAFPCxNvSgVra4TyX8lzs4V4+I/4= +github.com/jfrog/jfrog-cli-core/v2 v2.57.5/go.mod h1:LfKvCRXbvwgE0V6aX3/GabkzCedghXq0Y6lmsEuxr44= +github.com/jfrog/jfrog-cli-security v1.13.9-0.20250106070938-333ac33ce675 h1:tAGGhyUrHXS8xpFJcR9TsEUOYI3zV2OuEHttgLTrHk4= +github.com/jfrog/jfrog-cli-security v1.13.9-0.20250106070938-333ac33ce675/go.mod h1:lwaXWYJirqBhNmQ148xpM2h4s9xGLaoG50T2/Ax2jM0= +github.com/jfrog/jfrog-client-go v1.28.1-0.20241230154616-e342ed5065f1 h1:JQvbTSPDkPNpts1NLHGTKvtG4cMFY1ptBHTNMYFyMhs= +github.com/jfrog/jfrog-client-go v1.28.1-0.20241230154616-e342ed5065f1/go.mod h1:2ySOMva54L3EYYIlCBYBTcTgqfrrQ19gtpA/MWfA/ec= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= @@ -160,8 +160,8 @@ 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.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/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -324,8 +324,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= diff --git a/resources/v2/smallCritical.svg b/resources/v2/smallCritical.svg new file mode 100644 index 000000000..85d984521 --- /dev/null +++ b/resources/v2/smallCritical.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/v2/smallHigh.svg b/resources/v2/smallHigh.svg new file mode 100644 index 000000000..e367e459f --- /dev/null +++ b/resources/v2/smallHigh.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/v2/smallLow.svg b/resources/v2/smallLow.svg new file mode 100644 index 000000000..265842939 --- /dev/null +++ b/resources/v2/smallLow.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/v2/smallMedium.svg b/resources/v2/smallMedium.svg new file mode 100644 index 000000000..1018c9e4c --- /dev/null +++ b/resources/v2/smallMedium.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/v2/smallUnknown.svg b/resources/v2/smallUnknown.svg new file mode 100644 index 000000000..7d66301be --- /dev/null +++ b/resources/v2/smallUnknown.svg @@ -0,0 +1,3 @@ + + + diff --git a/scanpullrequest/scanpullrequest.go b/scanpullrequest/scanpullrequest.go index 5cfa3e066..875263355 100644 --- a/scanpullrequest/scanpullrequest.go +++ b/scanpullrequest/scanpullrequest.go @@ -7,6 +7,7 @@ import ( "os" "github.com/jfrog/frogbot/v2/utils" + "github.com/jfrog/frogbot/v2/utils/issues" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/froggit-go/vcsutils" "github.com/jfrog/gofrog/datastructures" @@ -26,9 +27,7 @@ const ( analyticsScanPrScanType = "PR" ) -type ScanPullRequestCmd struct { - XrayVersion string -} +type ScanPullRequestCmd struct{} // Run ScanPullRequest method only works for a single repository scan. // Therefore, the first repository config represents the repository on which Frogbot runs, and it is the only one that matters. @@ -94,22 +93,22 @@ func scanPullRequest(repo *utils.Repository, client vcsclient.VcsClient) (err er log.Info("-----------------------------------------------------------") // Audit PR code - issues, err := auditPullRequest(repo, client) + issues, resultContext, err := auditPullRequest(repo, client) if err != nil { return } // Output results - shouldSendExposedSecretsEmail := issues.SecretsExists() && repo.SmtpServer != "" + shouldSendExposedSecretsEmail := issues.SecretsIssuesExists() && repo.SmtpServer != "" if shouldSendExposedSecretsEmail { - secretsEmailDetails := utils.NewSecretsEmailDetails(client, repo, issues.Secrets) + secretsEmailDetails := utils.NewSecretsEmailDetails(client, repo, append(issues.SecretsVulnerabilities, issues.SecretsViolations...)) if err = utils.AlertSecretsExposed(secretsEmailDetails); err != nil { return } } // Handle PR comments for scan output - if err = utils.HandlePullRequestCommentsAfterScan(issues, repo, client, int(pullRequestDetails.ID)); err != nil { + if err = utils.HandlePullRequestCommentsAfterScan(issues, resultContext, repo, client, int(pullRequestDetails.ID)); err != nil { return } @@ -121,25 +120,30 @@ func scanPullRequest(repo *utils.Repository, client vcsclient.VcsClient) (err er return } -func toFailTaskStatus(repo *utils.Repository, issues *utils.IssuesCollection) bool { +func toFailTaskStatus(repo *utils.Repository, issues *issues.ScansIssuesCollection) bool { failFlagSet := repo.FailOnSecurityIssues != nil && *repo.FailOnSecurityIssues - return failFlagSet && issues.IssuesExists() + return failFlagSet && issues.IssuesExists(repo.PullRequestSecretComments) } // Downloads Pull Requests branches code and audits them -func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient) (issuesCollection *utils.IssuesCollection, err error) { +func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient) (issuesCollection *issues.ScansIssuesCollection, resultContext results.ResultContext, err error) { + repositoryCloneUrl, err := repoConfig.GetRepositoryHttpsCloneUrl(client) + if err != nil { + return + } + scanDetails := utils.NewScanDetails(client, &repoConfig.Server, &repoConfig.Git). - SetXrayGraphScanParams(repoConfig.Watches, repoConfig.JFrogProjectKey, len(repoConfig.AllowedLicenses) > 0). + SetJfrogVersions(repoConfig.XrayVersion, repoConfig.XscVersion). + SetResultsContext(repositoryCloneUrl, repoConfig.Watches, repoConfig.JFrogProjectKey, repoConfig.IncludeVulnerabilities, len(repoConfig.AllowedLicenses) > 0). SetFixableOnly(repoConfig.FixableOnly). SetFailOnInstallationErrors(*repoConfig.FailOnSecurityIssues). SetConfigProfile(repoConfig.ConfigProfile). SetSkipAutoInstall(repoConfig.SkipAutoInstall). SetDisableJas(repoConfig.DisableJas) + if scanDetails, err = scanDetails.SetMinSeverity(repoConfig.MinSeverity); err != nil { return } - scanDetails.XrayVersion = repoConfig.XrayVersion - scanDetails.XscVersion = repoConfig.XscVersion scanDetails.MultiScanId, scanDetails.StartTime = xsc.SendNewScanEvent( scanDetails.XrayVersion, @@ -150,23 +154,26 @@ func auditPullRequest(repoConfig *utils.Repository, client vcsclient.VcsClient) defer func() { if issuesCollection != nil { - xsc.SendScanEndedEvent(scanDetails.XrayVersion, scanDetails.XscVersion, scanDetails.ServerDetails, scanDetails.MultiScanId, scanDetails.StartTime, issuesCollection.CountIssuesCollectionFindings(), err) + xsc.SendScanEndedEvent(scanDetails.XrayVersion, scanDetails.XscVersion, scanDetails.ServerDetails, scanDetails.MultiScanId, scanDetails.StartTime, issuesCollection.GetAllIssuesCount(true), err) } }() - issuesCollection = &utils.IssuesCollection{} + issuesCollection = &issues.ScansIssuesCollection{} for i := range repoConfig.Projects { scanDetails.SetProject(&repoConfig.Projects[i]) - var projectIssues *utils.IssuesCollection + var projectIssues *issues.ScansIssuesCollection if projectIssues, err = auditPullRequestInProject(repoConfig, scanDetails); err != nil { + // Make sure status on scans are passed to show in the summary + issuesCollection.AppendStatus(projectIssues.ScanStatus) return } issuesCollection.Append(projectIssues) } + resultContext = scanDetails.ResultContext return } -func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils.ScanDetails) (auditIssues *utils.IssuesCollection, err error) { +func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils.ScanDetails) (auditIssues *issues.ScansIssuesCollection, err error) { // Download source branch sourcePullRequestInfo := scanDetails.PullRequestDetails.Source sourceBranchWd, cleanupSource, err := utils.DownloadRepoToTempDir(scanDetails.Client(), sourcePullRequestInfo.Owner, sourcePullRequestInfo.Repository, sourcePullRequestInfo.Name) @@ -183,15 +190,16 @@ func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils. log.Info("Scanning source branch...") sourceResults = scanDetails.RunInstallAndAudit(workingDirs...) if err = sourceResults.GetErrors(); err != nil { + // We get the scan status even if the scan failed to report the scan status in the summary + auditIssues = getResultScanStatues(sourceResults) return } // Set JAS output flags - repoConfig.OutputWriter.SetJasOutputFlags(sourceResults.EntitledForJas, len(sourceResults.GetJasScansResults(jasutils.Applicability)) > 0) - + repoConfig.OutputWriter.SetJasOutputFlags(sourceResults.EntitledForJas, sourceResults.HasJasScansResults(jasutils.Applicability)) // Get all issues that exist in the source branch if repoConfig.IncludeAllVulnerabilities { - if auditIssues, err = getAllIssues(sourceResults, repoConfig.AllowedLicenses, scanDetails.HasViolationContext()); err != nil { + if auditIssues, err = getAllIssues(sourceResults, repoConfig.AllowedLicenses); err != nil { return } utils.ConvertSarifPathsToRelative(auditIssues, sourceBranchWd) @@ -206,7 +214,7 @@ func auditPullRequestInProject(repoConfig *utils.Repository, scanDetails *utils. return } -func auditTargetBranch(repoConfig *utils.Repository, scanDetails *utils.ScanDetails, sourceScanResults *results.SecurityCommandResults) (newIssues *utils.IssuesCollection, targetBranchWd string, err error) { +func auditTargetBranch(repoConfig *utils.Repository, scanDetails *utils.ScanDetails, sourceScanResults *results.SecurityCommandResults) (newIssues *issues.ScansIssuesCollection, targetBranchWd string, err error) { // Download target branch (if needed) cleanupTarget := func() error { return nil } if !repoConfig.IncludeAllVulnerabilities { @@ -224,11 +232,13 @@ func auditTargetBranch(repoConfig *utils.Repository, scanDetails *utils.ScanDeta log.Info("Scanning target branch...") targetResults = scanDetails.RunInstallAndAudit(workingDirs...) if err = targetResults.GetErrors(); err != nil { + // We get the scan status even if the scan failed to report the scan status in the summary + newIssues = getResultScanStatues(sourceScanResults, targetResults) return } // Get newly added issues - newIssues, err = getNewlyAddedIssues(targetResults, sourceScanResults, repoConfig.AllowedLicenses, scanDetails.HasViolationContext()) + newIssues, err = getNewlyAddedIssues(targetResults, sourceScanResults, repoConfig.AllowedLicenses, targetResults.IncludesVulnerabilities(), targetResults.HasViolationContext()) return } @@ -242,24 +252,19 @@ func prepareTargetForScan(gitDetails utils.Git, scanDetails *utils.ScanDetails) return } log.Debug("Using most common ancestor commit as target branch commit") + // Get common parent commit between source and target and use it (checkout) to the target branch commit - if e := tryCheckoutToMostCommonAncestor(scanDetails, gitDetails.PullRequestDetails.Source.Name, target.Name, targetBranchWd, gitDetails.RepositoryCloneUrl); e != nil { + repoCloneUrl, err := scanDetails.GetRepositoryHttpsCloneUrl(scanDetails.Client()) + if err != nil { + return + } + if e := tryCheckoutToMostCommonAncestor(scanDetails, gitDetails.PullRequestDetails.Source.Name, target.Name, targetBranchWd, repoCloneUrl); e != nil { log.Warn(fmt.Sprintf("Failed to get best common ancestor commit between source branch: %s and target branch: %s, defaulting to target branch commit. Error: %s", gitDetails.PullRequestDetails.Source.Name, target.Name, e.Error())) } return } func tryCheckoutToMostCommonAncestor(scanDetails *utils.ScanDetails, baseBranch, headBranch, targetBranchWd, cloneRepoUrl string) (err error) { - if cloneRepoUrl != "" { - scanDetails.Git.RepositoryCloneUrl = cloneRepoUrl - } else { - var repositoryInfo vcsclient.RepositoryInfo - repositoryInfo, err = scanDetails.Client().GetRepositoryInfo(context.Background(), scanDetails.RepoOwner, scanDetails.RepoName) - if err != nil { - return - } - scanDetails.Git.RepositoryCloneUrl = repositoryInfo.CloneInfo.HTTP - } // Change working directory to the temp target branch directory cwd, err := os.Getwd() if err != nil { @@ -272,7 +277,7 @@ func tryCheckoutToMostCommonAncestor(scanDetails *utils.ScanDetails, baseBranch, err = errors.Join(err, os.Chdir(cwd)) }() // Create a new git manager and fetch - gitManager, err := utils.NewGitManager().SetAuth(scanDetails.Username, scanDetails.Token).SetRemoteGitUrl(scanDetails.Git.RepositoryCloneUrl) + gitManager, err := utils.NewGitManager().SetAuth(scanDetails.Username, scanDetails.Token).SetRemoteGitUrl(cloneRepoUrl) if err != nil { return } @@ -287,11 +292,11 @@ func tryCheckoutToMostCommonAncestor(scanDetails *utils.ScanDetails, baseBranch, return gitManager.CheckoutToHash(bestAncestorHash) } -func getAllIssues(cmdResults *results.SecurityCommandResults, allowedLicenses []string, hasViolationContext bool) (*utils.IssuesCollection, error) { - log.Info("Frogbot is configured to show all vulnerabilities") +func getAllIssues(cmdResults *results.SecurityCommandResults, allowedLicenses []string) (*issues.ScansIssuesCollection, error) { + log.Info("Frogbot is configured to show all issues") simpleJsonResults, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ - IncludeVulnerabilities: true, - HasViolationContext: hasViolationContext, + IncludeVulnerabilities: cmdResults.IncludesVulnerabilities(), + HasViolationContext: cmdResults.HasViolationContext(), AllowedLicenses: allowedLicenses, IncludeLicenses: true, SimplifiedOutput: true, @@ -299,61 +304,97 @@ func getAllIssues(cmdResults *results.SecurityCommandResults, allowedLicenses [] if err != nil { return nil, err } - return &utils.IssuesCollection{ - Vulnerabilities: append(simpleJsonResults.Vulnerabilities, simpleJsonResults.SecurityViolations...), - Iacs: simpleJsonResults.Iacs, - Secrets: simpleJsonResults.Secrets, - Sast: simpleJsonResults.Sast, - Licenses: simpleJsonResults.LicensesViolations, + return &issues.ScansIssuesCollection{ + ScanStatus: simpleJsonResults.Statuses, + ScaVulnerabilities: simpleJsonResults.Vulnerabilities, + ScaViolations: simpleJsonResults.SecurityViolations, + LicensesViolations: simpleJsonResults.LicensesViolations, + + IacVulnerabilities: simpleJsonResults.IacsVulnerabilities, + IacViolations: simpleJsonResults.IacsViolations, + + SecretsVulnerabilities: simpleJsonResults.SecretsVulnerabilities, + SecretsViolations: simpleJsonResults.SecretsViolations, + + SastVulnerabilities: simpleJsonResults.SastVulnerabilities, + SastViolations: simpleJsonResults.SastViolations, }, nil } +func getResultScanStatues(cmdResults ...*results.SecurityCommandResults) *issues.ScansIssuesCollection { + converted := make([]formats.SimpleJsonResults, len(cmdResults)) + for i, cmdResult := range cmdResults { + convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: cmdResult.IncludesVulnerabilities(), HasViolationContext: cmdResult.HasViolationContext(), SimplifiedOutput: true}) + var err error + if converted[i], err = convertor.ConvertToSimpleJson(cmdResult); err != nil { + log.Debug(fmt.Sprintf("Failed to get scan status for failed scan #%d. Error: %s", i, err.Error())) + continue + } + } + return &issues.ScansIssuesCollection{ScanStatus: getScanStatus(converted...)} +} + // Returns all the issues found in the source branch that didn't exist in the target branch. -func getNewlyAddedIssues(targetResults, sourceResults *results.SecurityCommandResults, allowedLicenses []string, hasViolationContext bool) (*utils.IssuesCollection, error) { - var err error - convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: hasViolationContext, IncludeLicenses: len(allowedLicenses) > 0, AllowedLicenses: allowedLicenses, SimplifiedOutput: true}) +func getNewlyAddedIssues(targetResults, sourceResults *results.SecurityCommandResults, allowedLicenses []string, includeVulnerabilities, hasViolationContext bool) (newIssues *issues.ScansIssuesCollection, err error) { + convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: includeVulnerabilities, HasViolationContext: hasViolationContext, IncludeLicenses: len(allowedLicenses) > 0, AllowedLicenses: allowedLicenses, SimplifiedOutput: true}) simpleJsonSource, err := convertor.ConvertToSimpleJson(sourceResults) if err != nil { - return nil, err + return } simpleJsonTarget, err := convertor.ConvertToSimpleJson(targetResults) if err != nil { - return nil, err + return } + // ResultContext is general attribute similar for all results, taking it from the source results + newIssues = &issues.ScansIssuesCollection{} + newIssues.ScanStatus = getScanStatus(simpleJsonTarget, simpleJsonSource) + // Get the unique sca vulnerabilities and violations between the source and target branches + newIssues.ScaVulnerabilities = getUniqueVulnerabilityOrViolationRows(simpleJsonTarget.Vulnerabilities, simpleJsonSource.Vulnerabilities) + newIssues.ScaViolations = getUniqueVulnerabilityOrViolationRows(simpleJsonTarget.SecurityViolations, simpleJsonSource.SecurityViolations) + newIssues.LicensesViolations = getUniqueLicenseRows(simpleJsonTarget.LicensesViolations, simpleJsonSource.LicensesViolations) + // Get the unique source code vulnerabilities and violations between the source and target branches + newIssues.IacVulnerabilities = createNewSourceCodeRows(simpleJsonTarget.IacsVulnerabilities, simpleJsonSource.IacsVulnerabilities) + newIssues.IacViolations = createNewSourceCodeRows(simpleJsonTarget.IacsViolations, simpleJsonSource.IacsViolations) + newIssues.SecretsVulnerabilities = createNewSourceCodeRows(simpleJsonTarget.SecretsVulnerabilities, simpleJsonSource.SecretsVulnerabilities) + newIssues.SecretsViolations = createNewSourceCodeRows(simpleJsonTarget.SecretsViolations, simpleJsonSource.SecretsViolations) + newIssues.SastVulnerabilities = createNewSourceCodeRows(simpleJsonTarget.SastVulnerabilities, simpleJsonSource.SastVulnerabilities) + newIssues.SastViolations = createNewSourceCodeRows(simpleJsonTarget.SastViolations, simpleJsonSource.SastViolations) + return +} - var newVulnerabilitiesOrViolations []formats.VulnerabilityOrViolationRow - if len(simpleJsonSource.Vulnerabilities) > 0 || len(simpleJsonSource.SecurityViolations) > 0 { - newVulnerabilitiesOrViolations = append( - getUniqueVulnerabilityOrViolationRows(simpleJsonTarget.Vulnerabilities, simpleJsonSource.Vulnerabilities), - getUniqueVulnerabilityOrViolationRows(simpleJsonTarget.SecurityViolations, simpleJsonSource.SecurityViolations)..., - ) +func getScanStatus(cmdResults ...formats.SimpleJsonResults) formats.ScanStatus { + if len(cmdResults) == 0 { + return formats.ScanStatus{} } - - var newLicenses []formats.LicenseRow - if len(simpleJsonSource.LicensesViolations) > 0 { - newLicenses = getUniqueLicenseRows(simpleJsonTarget.LicensesViolations, simpleJsonSource.LicensesViolations) + if len(cmdResults) == 1 { + return cmdResults[0].Statuses } + statuses := cmdResults[0].Statuses + for _, sourceResults := range cmdResults[1:] { + statuses.ScaStatusCode = getWorstScanStatus(statuses.ScaStatusCode, sourceResults.Statuses.ScaStatusCode) + statuses.IacStatusCode = getWorstScanStatus(statuses.IacStatusCode, sourceResults.Statuses.IacStatusCode) + statuses.SecretsStatusCode = getWorstScanStatus(statuses.SecretsStatusCode, sourceResults.Statuses.SecretsStatusCode) + statuses.SastStatusCode = getWorstScanStatus(statuses.SastStatusCode, sourceResults.Statuses.SastStatusCode) + statuses.ApplicabilityStatusCode = getWorstScanStatus(statuses.ApplicabilityStatusCode, sourceResults.Statuses.ApplicabilityStatusCode) + } + return statuses +} - var newIacs []formats.SourceCodeRow - if len(simpleJsonSource.Iacs) > 0 { - newIacs = createNewSourceCodeRows(simpleJsonTarget.Iacs, simpleJsonSource.Iacs) +func getWorstScanStatus(targetStatus, sourceStatus *int) *int { + if sourceStatus == nil && targetStatus == nil { + // Scan not performed. + return nil } - var newSecrets []formats.SourceCodeRow - if len(simpleJsonSource.Secrets) > 0 { - newSecrets = createNewSourceCodeRows(simpleJsonTarget.Secrets, simpleJsonSource.Secrets) + if targetStatus == nil { + return sourceStatus } - var newSast []formats.SourceCodeRow - if len(simpleJsonSource.Sast) > 0 { - newSast = createNewSourceCodeRows(simpleJsonTarget.Sast, simpleJsonSource.Sast) + if sourceStatus == nil { + return targetStatus } - - return &utils.IssuesCollection{ - Vulnerabilities: newVulnerabilitiesOrViolations, - Iacs: newIacs, - Secrets: newSecrets, - Sast: newSast, - Licenses: newLicenses, - }, nil + if *sourceStatus == 0 { + return targetStatus + } + return sourceStatus } func createNewSourceCodeRows(targetResults, sourceResults []formats.SourceCodeRow) []formats.SourceCodeRow { @@ -388,14 +429,14 @@ func getUniqueVulnerabilityOrViolationRows(targetRows, sourceRows []formats.Vuln return newRows } -func getUniqueLicenseRows(targetRows, sourceRows []formats.LicenseRow) []formats.LicenseRow { - existingLicenses := make(map[string]formats.LicenseRow) - var newLicenses []formats.LicenseRow +func getUniqueLicenseRows(targetRows, sourceRows []formats.LicenseViolationRow) []formats.LicenseViolationRow { + existingLicenses := make(map[string]formats.LicenseViolationRow) + var newLicenses []formats.LicenseViolationRow for _, row := range targetRows { - existingLicenses[getUniqueLicenseKey(row)] = row + existingLicenses[getUniqueLicenseKey(row.LicenseRow)] = row } for _, row := range sourceRows { - if _, exists := existingLicenses[getUniqueLicenseKey(row)]; !exists { + if _, exists := existingLicenses[getUniqueLicenseKey(row.LicenseRow)]; !exists { newLicenses = append(newLicenses, row) } } diff --git a/scanpullrequest/scanpullrequest_test.go b/scanpullrequest/scanpullrequest_test.go index c00a49fac..ffb5776ec 100644 --- a/scanpullrequest/scanpullrequest_test.go +++ b/scanpullrequest/scanpullrequest_test.go @@ -16,6 +16,7 @@ import ( "time" "github.com/jfrog/frogbot/v2/utils" + "github.com/jfrog/frogbot/v2/utils/issues" "github.com/jfrog/frogbot/v2/utils/outputwriter" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/froggit-go/vcsutils" @@ -27,6 +28,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/results/conversion" "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-cli-security/utils/validations" "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" @@ -45,7 +47,7 @@ const ( testTargetBranchName = "master" ) -func createScaDiff(t *testing.T, previousScan, currentScan services.ScanResponse, applicable bool, allowedLicenses ...string) (securityViolationsRows []formats.VulnerabilityOrViolationRow, licenseViolations []formats.LicenseRow) { +func createScaDiff(t *testing.T, previousScan, currentScan services.ScanResponse, applicable bool, allowedLicenses ...string) (securityViolationsRows []formats.VulnerabilityOrViolationRow, licenseViolations []formats.LicenseViolationRow) { sourceResults, err := scaToDummySimpleJsonResults(currentScan, applicable, allowedLicenses...) assert.NoError(t, err) targetResults, err := scaToDummySimpleJsonResults(previousScan, applicable, allowedLicenses...) @@ -62,12 +64,12 @@ func scaToDummySimpleJsonResults(response services.ScanResponse, applicable bool convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: true, AllowedLicenses: allowedLicenses}) jasResults := &results.JasScansResults{} if applicable { - jasResults.ApplicabilityScanResults = append(jasResults.ApplicabilityScanResults, sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithOneLocation("file1", 1, 10, 2, 11, "snippet", "applic_CVE-2023-4321", ""))) + jasResults.ApplicabilityScanResults = append(jasResults.ApplicabilityScanResults, validations.NewMockJasRuns(sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithOneLocation("file1", 1, 10, 2, 11, "snippet", "applic_CVE-2023-4321", "")))...) } cmdResults := &results.SecurityCommandResults{EntitledForJas: applicable, Targets: []*results.TargetResults{{ ScanTarget: results.ScanTarget{Target: "dummy"}, JasResults: jasResults, - ScaResults: &results.ScaScanResults{XrayResults: []services.ScanResponse{response}}, + ScaResults: &results.ScaScanResults{XrayResults: validations.NewMockScaResults(response)}, }}} return convertor.ConvertToSimpleJson(cmdResults) } @@ -81,14 +83,14 @@ func createJasDiff(t *testing.T, scanType jasutils.JasScanType, source, target [ var targetJasResults, sourceJasResults []formats.SourceCodeRow switch scanType { case jasutils.Sast: - targetJasResults = targetResults.Sast - sourceJasResults = sourceResults.Sast + targetJasResults = targetResults.SastVulnerabilities + sourceJasResults = sourceResults.SastVulnerabilities case jasutils.Secrets: - targetJasResults = targetResults.Secrets - sourceJasResults = sourceResults.Secrets + targetJasResults = targetResults.SecretsVulnerabilities + sourceJasResults = sourceResults.SecretsVulnerabilities case jasutils.IaC: - targetJasResults = targetResults.Iacs - sourceJasResults = sourceResults.Iacs + targetJasResults = targetResults.IacsVulnerabilities + sourceJasResults = sourceResults.IacsVulnerabilities } return createNewSourceCodeRows(targetJasResults, sourceJasResults) @@ -97,14 +99,14 @@ func createJasDiff(t *testing.T, scanType jasutils.JasScanType, source, target [ func jasToDummySimpleJsonResults(scanType jasutils.JasScanType, jasScanResults []*sarif.Result) (formats.SimpleJsonResults, error) { convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: true}) - jasResults := &results.JasScansResults{} + jasResults := &results.JasScansResults{JasVulnerabilities: results.JasScanResults{}} switch scanType { case jasutils.Sast: - jasResults.SastScanResults = append(jasResults.SastScanResults, sarifutils.CreateRunWithDummyResults(jasScanResults...)) + jasResults.JasVulnerabilities.SastScanResults = append(jasResults.JasVulnerabilities.SastScanResults, validations.NewMockJasRuns(sarifutils.CreateRunWithDummyResults(jasScanResults...))...) case jasutils.Secrets: - jasResults.SecretsScanResults = append(jasResults.SecretsScanResults, sarifutils.CreateRunWithDummyResults(jasScanResults...)) + jasResults.JasVulnerabilities.SecretsScanResults = append(jasResults.JasVulnerabilities.SecretsScanResults, validations.NewMockJasRuns(sarifutils.CreateRunWithDummyResults(jasScanResults...))...) case jasutils.IaC: - jasResults.IacScanResults = append(jasResults.IacScanResults, sarifutils.CreateRunWithDummyResults(jasScanResults...)) + jasResults.JasVulnerabilities.IacScanResults = append(jasResults.JasVulnerabilities.IacScanResults, validations.NewMockJasRuns(sarifutils.CreateRunWithDummyResults(jasScanResults...))...) } cmdResults := &results.SecurityCommandResults{EntitledForJas: true, Targets: []*results.TargetResults{{ ScanTarget: results.ScanTarget{Target: "dummy"}, @@ -331,7 +333,7 @@ func TestGetNewVulnerabilities(t *testing.T) { SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 13}, ImpactedDependencyName: "component-C", }, - Cves: []formats.CveRow{{Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, + Cves: []formats.CveRow{{Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable", ScannerDescription: "rule-msg", Evidence: []formats.Evidence{{Reason: "result-msg", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, Technology: techutils.Yarn, }, { @@ -342,7 +344,7 @@ func TestGetNewVulnerabilities(t *testing.T) { SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 13}, ImpactedDependencyName: "component-D", }, - Cves: []formats.CveRow{{Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, + Cves: []formats.CveRow{{Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable", ScannerDescription: "rule-msg", Evidence: []formats.Evidence{{Reason: "result-msg", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, Technology: techutils.Yarn, }, } @@ -443,49 +445,51 @@ func TestGetNewVulnerabilitiesCaseNoNewVulnerabilities(t *testing.T) { func TestGetAllIssues(t *testing.T) { allowedLicenses := []string{"MIT"} - auditResults := &results.SecurityCommandResults{EntitledForJas: true, Targets: []*results.TargetResults{{ + auditResults := &results.SecurityCommandResults{EntitledForJas: true, ResultContext: results.ResultContext{IncludeVulnerabilities: true}, Targets: []*results.TargetResults{{ ScanTarget: results.ScanTarget{Target: "dummy"}, ScaResults: &results.ScaScanResults{ - XrayResults: []services.ScanResponse{{ + XrayResults: validations.NewMockScaResults(services.ScanResponse{ Vulnerabilities: []services.Vulnerability{ {Cves: []services.Cve{{Id: "CVE-2022-2122"}}, Severity: "High", Components: map[string]services.Component{"Dep-1": {FixedVersions: []string{"1.2.3"}}}}, {Cves: []services.Cve{{Id: "CVE-2023-3122"}}, Severity: "Low", Components: map[string]services.Component{"Dep-2": {FixedVersions: []string{"1.2.2"}}}}, }, Licenses: []services.License{{Key: "Apache-2.0", Components: map[string]services.Component{"Dep-1": {FixedVersions: []string{"1.2.3"}}}}}, - }}, + }), }, JasResults: &results.JasScansResults{ - ApplicabilityScanResults: []*sarif.Run{ + ApplicabilityScanResults: validations.NewMockJasRuns( sarifutils.CreateRunWithDummyResults( sarifutils.CreateDummyPassingResult("applic_CVE-2023-3122"), sarifutils.CreateResultWithOneLocation("file1", 1, 10, 2, 11, "snippet", "applic_CVE-2022-2122", ""), ), - }, - IacScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("Missing auto upgrade was detected", "rule", severityutils.SeverityToSarifSeverityLevel(severityutils.High).String(), - sarifutils.CreateLocation("file1", 1, 10, 2, 11, "aws-violation"), + ), + JasVulnerabilities: results.JasScanResults{ + IacScanResults: validations.NewMockJasRuns( + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateResultWithLocations("Missing auto upgrade was detected", "rule", severityutils.SeverityToSarifSeverityLevel(severityutils.High).String(), + sarifutils.CreateLocation("file1", 1, 10, 2, 11, "aws-violation"), + ), ), ), - }, - SecretsScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("Secret", "rule", severityutils.SeverityToSarifSeverityLevel(severityutils.High).String(), - sarifutils.CreateLocation("index.js", 5, 6, 7, 8, "access token exposed"), + SecretsScanResults: validations.NewMockJasRuns( + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateResultWithLocations("Secret", "rule", severityutils.SeverityToSarifSeverityLevel(severityutils.High).String(), + sarifutils.CreateLocation("index.js", 5, 6, 7, 8, "access token exposed"), + ), ), ), - }, - SastScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults( - sarifutils.CreateResultWithLocations("XSS Vulnerability", "rule", severityutils.SeverityToSarifSeverityLevel(severityutils.High).String(), - sarifutils.CreateLocation("file1", 1, 10, 2, 11, "snippet"), + SastScanResults: validations.NewMockJasRuns( + sarifutils.CreateRunWithDummyResults( + sarifutils.CreateResultWithLocations("XSS Vulnerability", "rule", severityutils.SeverityToSarifSeverityLevel(severityutils.High).String(), + sarifutils.CreateLocation("file1", 1, 10, 2, 11, "snippet"), + ), ), ), }, }, }}} - expectedOutput := &utils.IssuesCollection{ - Vulnerabilities: []formats.VulnerabilityOrViolationRow{ + expectedOutput := &issues.ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{ { Applicable: "Applicable", FixedVersions: []string{"1.2.3"}, @@ -493,7 +497,7 @@ func TestGetAllIssues(t *testing.T) { SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 21}, ImpactedDependencyName: "Dep-1", }, - Cves: []formats.CveRow{{Id: "CVE-2022-2122", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, + Cves: []formats.CveRow{{Id: "CVE-2022-2122", Applicability: &formats.Applicability{Status: "Applicable", ScannerDescription: "rule-msg", Evidence: []formats.Evidence{{Reason: "result-msg", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, }, { Applicable: "Not Applicable", @@ -502,15 +506,19 @@ func TestGetAllIssues(t *testing.T) { SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 2}, ImpactedDependencyName: "Dep-2", }, - Cves: []formats.CveRow{{Id: "CVE-2023-3122", Applicability: &formats.Applicability{Status: "Not Applicable"}}}, + Cves: []formats.CveRow{{Id: "CVE-2023-3122", Applicability: &formats.Applicability{Status: "Not Applicable", ScannerDescription: "rule-msg"}}}, }, }, - Iacs: []formats.SourceCodeRow{ + IacVulnerabilities: []formats.SourceCodeRow{ { SeverityDetails: formats.SeverityDetails{ Severity: "High", SeverityNumValue: 21, }, + ScannerInfo: formats.ScannerInfo{ + ScannerDescription: "rule-msg", + RuleId: "rule", + }, Finding: "Missing auto upgrade was detected", Location: formats.Location{ File: "file1", @@ -522,12 +530,16 @@ func TestGetAllIssues(t *testing.T) { }, }, }, - Secrets: []formats.SourceCodeRow{ + SecretsVulnerabilities: []formats.SourceCodeRow{ { SeverityDetails: formats.SeverityDetails{ Severity: "High", SeverityNumValue: 21, }, + ScannerInfo: formats.ScannerInfo{ + ScannerDescription: "rule-msg", + RuleId: "rule", + }, Finding: "Secret", Location: formats.Location{ File: "index.js", @@ -539,12 +551,16 @@ func TestGetAllIssues(t *testing.T) { }, }, }, - Sast: []formats.SourceCodeRow{ + SastVulnerabilities: []formats.SourceCodeRow{ { SeverityDetails: formats.SeverityDetails{ Severity: "High", SeverityNumValue: 21, }, + ScannerInfo: formats.ScannerInfo{ + ScannerDescription: "rule-msg", + RuleId: "rule", + }, Finding: "XSS Vulnerability", Location: formats.Location{ File: "file1", @@ -556,28 +572,33 @@ func TestGetAllIssues(t *testing.T) { }, }, }, - Licenses: []formats.LicenseRow{ + LicensesViolations: []formats.LicenseViolationRow{ { - LicenseKey: "Apache-2.0", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{ - Severity: "Medium", - SeverityNumValue: 14, + LicenseRow: formats.LicenseRow{ + LicenseKey: "Apache-2.0", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{ + Severity: "Medium", + SeverityNumValue: 14, + }, + ImpactedDependencyName: "Dep-1", }, - ImpactedDependencyName: "Dep-1", + }, + ViolationContext: formats.ViolationContext{ + Watch: "jfrog_custom_license_violation", }, }, }, } - issuesRows, err := getAllIssues(auditResults, allowedLicenses, false) + issuesRows, err := getAllIssues(auditResults, allowedLicenses) if assert.NoError(t, err) { - assert.ElementsMatch(t, expectedOutput.Vulnerabilities, issuesRows.Vulnerabilities) - assert.ElementsMatch(t, expectedOutput.Iacs, issuesRows.Iacs) - assert.ElementsMatch(t, expectedOutput.Secrets, issuesRows.Secrets) - assert.ElementsMatch(t, expectedOutput.Sast, issuesRows.Sast) - assert.ElementsMatch(t, expectedOutput.Licenses, issuesRows.Licenses) + assert.ElementsMatch(t, expectedOutput.ScaVulnerabilities, issuesRows.ScaVulnerabilities) + assert.ElementsMatch(t, expectedOutput.IacVulnerabilities, issuesRows.IacVulnerabilities) + assert.ElementsMatch(t, expectedOutput.SecretsVulnerabilities, issuesRows.SecretsVulnerabilities) + assert.ElementsMatch(t, expectedOutput.SastVulnerabilities, issuesRows.SastVulnerabilities) + assert.ElementsMatch(t, expectedOutput.LicensesViolations, issuesRows.LicensesViolations) } } @@ -859,6 +880,10 @@ func TestCreateNewIacRows(t *testing.T) { Severity: "High", SeverityNumValue: 21, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule", + ScannerDescription: "rule-msg", + }, Finding: "Missing auto upgrade was detected", Location: formats.Location{ File: "file1", @@ -889,6 +914,10 @@ func TestCreateNewIacRows(t *testing.T) { Severity: "Medium", SeverityNumValue: 17, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule", + ScannerDescription: "rule-msg", + }, Finding: "enable_private_endpoint=false was detected", Location: formats.Location{ File: "file2", @@ -942,6 +971,10 @@ func TestCreateNewSecretRows(t *testing.T) { Severity: "High", SeverityNumValue: 21, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule", + ScannerDescription: "rule-msg", + }, Finding: "Secret", Location: formats.Location{ File: "file1", @@ -972,6 +1005,10 @@ func TestCreateNewSecretRows(t *testing.T) { Severity: "Medium", SeverityNumValue: 17, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule", + ScannerDescription: "rule-msg", + }, Finding: "Secret", Location: formats.Location{ File: "file2", @@ -1025,6 +1062,10 @@ func TestCreateNewSastRows(t *testing.T) { Severity: "High", SeverityNumValue: 21, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule", + ScannerDescription: "rule-msg", + }, Finding: "XSS Vulnerability", Location: formats.Location{ File: "file1", @@ -1055,6 +1096,10 @@ func TestCreateNewSastRows(t *testing.T) { Severity: "Medium", SeverityNumValue: 17, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule", + ScannerDescription: "rule-msg", + }, Finding: "Stack Trace Exposure", Location: formats.Location{ File: "file2", diff --git a/scanrepository/scanrepository.go b/scanrepository/scanrepository.go index 489fe42b9..8c339109f 100644 --- a/scanrepository/scanrepository.go +++ b/scanrepository/scanrepository.go @@ -122,9 +122,14 @@ func (cfp *ScanRepositoryCmd) scanAndFixBranch(repository *utils.Repository) (er } func (cfp *ScanRepositoryCmd) setCommandPrerequisites(repository *utils.Repository, client vcsclient.VcsClient) (err error) { + repositoryCloneUrl, err := repository.Git.GetRepositoryHttpsCloneUrl(client) + if err != nil { + return + } // Set the scan details cfp.scanDetails = utils.NewScanDetails(client, &repository.Server, &repository.Git). - SetXrayGraphScanParams(repository.Watches, repository.JFrogProjectKey, len(repository.AllowedLicenses) > 0). + SetJfrogVersions(cfp.XrayVersion, cfp.XscVersion). + SetResultsContext(repositoryCloneUrl, repository.Watches, repository.JFrogProjectKey, repository.IncludeVulnerabilities, len(repository.AllowedLicenses) > 0). SetFailOnInstallationErrors(*repository.FailOnSecurityIssues). SetFixableOnly(repository.FixableOnly). SetConfigProfile(repository.ConfigProfile). @@ -132,22 +137,9 @@ func (cfp *ScanRepositoryCmd) setCommandPrerequisites(repository *utils.Reposito SetAllowPartialResults(repository.AllowPartialResults). SetDisableJas(repository.DisableJas) - cfp.scanDetails.XrayVersion = cfp.XrayVersion - cfp.scanDetails.XscVersion = cfp.XscVersion - if cfp.scanDetails, err = cfp.scanDetails.SetMinSeverity(repository.MinSeverity); err != nil { return } - if repository.Git.RepositoryCloneUrl != "" { - cfp.scanDetails.Git.RepositoryCloneUrl = repository.Git.RepositoryCloneUrl - } else { - var repositoryInfo vcsclient.RepositoryInfo - repositoryInfo, err = client.GetRepositoryInfo(context.Background(), cfp.scanDetails.RepoOwner, cfp.scanDetails.RepoName) - if err != nil { - return - } - cfp.scanDetails.Git.RepositoryCloneUrl = repositoryInfo.CloneInfo.HTTP - } // Set the flag for aggregating fixes to generate a unified pull request for fixing vulnerabilities cfp.aggregateFixes = repository.Git.AggregateFixes @@ -158,7 +150,7 @@ func (cfp *ScanRepositoryCmd) setCommandPrerequisites(repository *utils.Reposito cfp.gitManager, err = utils.NewGitManager(). SetAuth(cfp.scanDetails.Username, cfp.scanDetails.Token). SetDryRun(cfp.dryRun, cfp.dryRunRepoPath). - SetRemoteGitUrl(cfp.scanDetails.Git.RepositoryCloneUrl) + SetRemoteGitUrl(repositoryCloneUrl) if err != nil { return } @@ -182,7 +174,7 @@ func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (i } continue } - if summary, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: cfp.scanDetails.HasViolationContext()}).ConvertToSummary(scanResults); err != nil { + if summary, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: scanResults.IncludesVulnerabilities(), HasViolationContext: scanResults.HasViolationContext()}).ConvertToSummary(scanResults); err != nil { return totalFindings, err } else { findingCount := summary.GetTotalViolations() @@ -196,7 +188,7 @@ func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (i // Uploads Sarif results to GitHub in order to view the scan in the code scanning UI // Currently available on GitHub only and JFrog Advance Security package // Only if Jas entitlement is available - if err = utils.UploadSarifResultsToGithubSecurityTab(scanResults, repository, cfp.scanDetails.BaseBranch(), cfp.scanDetails.Client(), cfp.scanDetails.HasViolationContext()); err != nil { + if err = utils.UploadSarifResultsToGithubSecurityTab(scanResults, repository, cfp.scanDetails.BaseBranch(), cfp.scanDetails.Client()); err != nil { log.Warn(err) } } @@ -232,13 +224,13 @@ func (cfp *ScanRepositoryCmd) scan(currentWorkingDir string) (*results.SecurityC return nil, err } log.Info("Xray scan completed") - cfp.OutputWriter.SetJasOutputFlags(auditResults.EntitledForJas, len(auditResults.GetJasScansResults(jasutils.Applicability)) > 0) + cfp.OutputWriter.SetJasOutputFlags(auditResults.EntitledForJas, auditResults.HasJasScansResults(jasutils.Applicability)) cfp.projectTech = auditResults.GetTechnologies(cfp.projectTech...) return auditResults, nil } func (cfp *ScanRepositoryCmd) getVulnerabilitiesMap(scanResults *results.SecurityCommandResults) (map[string]*utils.VulnerabilityDetails, error) { - vulnerabilitiesMap, err := cfp.createVulnerabilitiesMap(scanResults, cfp.scanDetails.HasViolationContext()) + vulnerabilitiesMap, err := cfp.createVulnerabilitiesMap(scanResults) if err != nil { return nil, err } @@ -570,9 +562,9 @@ func (cfp *ScanRepositoryCmd) cloneRepositoryOrUseLocalAndCheckoutToBranch() (te } // Create a vulnerabilities map - a map with 'impacted package' as a key and all the necessary information of this vulnerability as value. -func (cfp *ScanRepositoryCmd) createVulnerabilitiesMap(scanResults *results.SecurityCommandResults, hasViolationContext bool) (map[string]*utils.VulnerabilityDetails, error) { +func (cfp *ScanRepositoryCmd) createVulnerabilitiesMap(scanResults *results.SecurityCommandResults) (map[string]*utils.VulnerabilityDetails, error) { vulnerabilitiesMap := map[string]*utils.VulnerabilityDetails{} - simpleJsonResult, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: true, HasViolationContext: hasViolationContext}).ConvertToSimpleJson(scanResults) + simpleJsonResult, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{IncludeVulnerabilities: scanResults.IncludesVulnerabilities(), HasViolationContext: scanResults.HasViolationContext()}).ConvertToSimpleJson(scanResults) if err != nil { return nil, err } diff --git a/scanrepository/scanrepository_test.go b/scanrepository/scanrepository_test.go index 90ee39dda..e795d8aeb 100644 --- a/scanrepository/scanrepository_test.go +++ b/scanrepository/scanrepository_test.go @@ -21,6 +21,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/techutils" + "github.com/jfrog/jfrog-cli-security/utils/validations" "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" @@ -434,10 +435,8 @@ func TestPackageTypeFromScan(t *testing.T) { frogbotParams.Projects[0].InstallCommandName = pkg.commandName frogbotParams.Projects[0].InstallCommandArgs = pkg.commandArgs scanSetup := utils.ScanDetails{ - XrayGraphScanParams: &services.XrayGraphScanParams{ - XrayVersion: xrayVersion, - XscVersion: xscVersion, - }, + XrayVersion: xrayVersion, + XscVersion: xscVersion, Project: &frogbotParams.Projects[0], ServerDetails: &frogbotParams.Server, } @@ -486,44 +485,47 @@ func TestCreateVulnerabilitiesMap(t *testing.T) { }, { name: "Scan results with vulnerabilities and no violations", - scanResults: &results.SecurityCommandResults{Targets: []*results.TargetResults{{ - ScanTarget: results.ScanTarget{Target: "target1"}, - ScaResults: &results.ScaScanResults{ - XrayResults: []services.ScanResponse{ - { - Vulnerabilities: []services.Vulnerability{ - { - Cves: []services.Cve{ - {Id: "CVE-2023-1234", CvssV3Score: "9.1"}, - {Id: "CVE-2023-4321", CvssV3Score: "8.9"}, - }, - Severity: "Critical", - Components: map[string]services.Component{ - "vuln1": { - FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"}, - ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}}}, + scanResults: &results.SecurityCommandResults{ + ResultContext: results.ResultContext{IncludeVulnerabilities: true}, + Targets: []*results.TargetResults{{ + ScanTarget: results.ScanTarget{Target: "target1"}, + ScaResults: &results.ScaScanResults{ + XrayResults: validations.NewMockScaResults( + services.ScanResponse{ + Vulnerabilities: []services.Vulnerability{ + { + Cves: []services.Cve{ + {Id: "CVE-2023-1234", CvssV3Score: "9.1"}, + {Id: "CVE-2023-4321", CvssV3Score: "8.9"}, + }, + Severity: "Critical", + Components: map[string]services.Component{ + "vuln1": { + FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"}, + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}}}, + }, }, }, - }, - { - Cves: []services.Cve{ - {Id: "CVE-2022-1234", CvssV3Score: "7.1"}, - {Id: "CVE-2022-4321", CvssV3Score: "7.9"}, - }, - Severity: "High", - Components: map[string]services.Component{ - "vuln2": { - FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"}, - ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}, {ComponentId: "vuln2"}}}, + { + Cves: []services.Cve{ + {Id: "CVE-2022-1234", CvssV3Score: "7.1"}, + {Id: "CVE-2022-4321", CvssV3Score: "7.9"}, + }, + Severity: "High", + Components: map[string]services.Component{ + "vuln2": { + FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"}, + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}, {ComponentId: "vuln2"}}}, + }, }, }, }, }, - }, + ), }, - }, - JasResults: &results.JasScansResults{}, - }}}, + JasResults: &results.JasScansResults{}, + }}, + }, expectedMap: map[string]*utils.VulnerabilityDetails{ "vuln1": { SuggestedFixedVersion: "1.9.1", @@ -538,46 +540,51 @@ func TestCreateVulnerabilitiesMap(t *testing.T) { }, { name: "Scan results with violations and no vulnerabilities", - scanResults: &results.SecurityCommandResults{Targets: []*results.TargetResults{{ - ScanTarget: results.ScanTarget{Target: "target1"}, - ScaResults: &results.ScaScanResults{ - XrayResults: []services.ScanResponse{ - { - Violations: []services.Violation{ - { - ViolationType: "security", - Cves: []services.Cve{ - {Id: "CVE-2023-1234", CvssV3Score: "9.1"}, - {Id: "CVE-2023-4321", CvssV3Score: "8.9"}, - }, - Severity: "Critical", - Components: map[string]services.Component{ - "viol1": { - FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"}, - ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "viol1"}}}, + scanResults: &results.SecurityCommandResults{ + ResultContext: results.ResultContext{IncludeVulnerabilities: true, Watches: []string{"w1"}}, + Targets: []*results.TargetResults{{ + ScanTarget: results.ScanTarget{Target: "target1"}, + ScaResults: &results.ScaScanResults{ + XrayResults: validations.NewMockScaResults( + services.ScanResponse{ + Violations: []services.Violation{ + { + ViolationType: "security", + WatchName: "w1", + Cves: []services.Cve{ + {Id: "CVE-2023-1234", CvssV3Score: "9.1"}, + {Id: "CVE-2023-4321", CvssV3Score: "8.9"}, + }, + Severity: "Critical", + Components: map[string]services.Component{ + "viol1": { + FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"}, + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "viol1"}}}, + }, }, }, - }, - { - ViolationType: "security", - Cves: []services.Cve{ - {Id: "CVE-2022-1234", CvssV3Score: "7.1"}, - {Id: "CVE-2022-4321", CvssV3Score: "7.9"}, - }, - Severity: "High", - Components: map[string]services.Component{ - "viol2": { - FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"}, - ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "viol1"}, {ComponentId: "viol2"}}}, + { + ViolationType: "security", + WatchName: "w1", + Cves: []services.Cve{ + {Id: "CVE-2022-1234", CvssV3Score: "7.1"}, + {Id: "CVE-2022-4321", CvssV3Score: "7.9"}, + }, + Severity: "High", + Components: map[string]services.Component{ + "viol2": { + FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"}, + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "viol1"}, {ComponentId: "viol2"}}}, + }, }, }, }, }, - }, + ), }, - }, - JasResults: &results.JasScansResults{}, - }}}, + JasResults: &results.JasScansResults{}, + }}, + }, expectedMap: map[string]*utils.VulnerabilityDetails{ "viol1": { SuggestedFixedVersion: "1.9.1", @@ -594,7 +601,7 @@ func TestCreateVulnerabilitiesMap(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - fixVersionsMap, err := cfp.createVulnerabilitiesMap(testCase.scanResults, true) + fixVersionsMap, err := cfp.createVulnerabilitiesMap(testCase.scanResults) assert.NoError(t, err) for name, expectedVuln := range testCase.expectedMap { actualVuln, exists := fixVersionsMap[name] diff --git a/testdata/messages/integration/test_proj_pip_with_vulnerability.md b/testdata/messages/integration/test_proj_pip_with_vulnerability.md index 240ce4a05..03c9b00cf 100644 --- a/testdata/messages/integration/test_proj_pip_with_vulnerability.md +++ b/testdata/messages/integration/test_proj_pip_with_vulnerability.md @@ -9,21 +9,47 @@ -## 📦 Vulnerable Dependencies -### ✍️ Summary +## 📗 Scan Summary +- Frogbot scanned for vulnerabilities and found 1 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done |
1 Issues Found 1 High
| +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | Not Found | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ✅ Done | Not Found | + +### 📦 Vulnerable Dependencies +
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Not Covered | pip-example:1.2.3 | pyjwt 1.7.1 | [2.4.0] | CVE-2022-29217 | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | CVE-2022-29217 | Not Covered | pip-example:1.2.3 | pyjwt 1.7.1 | [2.4.0] |
-### 🔬 Research Details +### 🔖 Details + +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Jfrog Research Severity:** | Medium | +| **Contextual Analysis:** | Not Covered | +| **Direct Dependencies:** | pip-example:1.2.3 | +| **Impacted Dependency:** | pyjwt:1.7.1 | +| **Fixed Versions:** | [2.4.0] | +| **CVSS V3:** | 7.5 | + +Algorithm confusion in PyJWT leads to authentication bypass. + +### 🔬 JFrog Research Details + **Description:** [PyJWT](https://pypi.org/project/PyJWT) is a Python implementation of the RFC 7519 standard (JSON Web Tokens). [JSON Web Tokens](https://jwt.io/) are an open, industry standard method for representing claims securely between two parties. A JWT comes with an inline signature that is meant to be verified by the receiving application. JWT supports multiple standard algorithms, and the algorithm itself is **specified in the JWT token itself**. @@ -63,6 +89,7 @@ With - `jwt.decode(encoded_jwt, pub_key_bytes, algorithms=["ES256"])` + ---
diff --git a/testdata/messages/integration/test_proj_with_vulnerability_simplified.md b/testdata/messages/integration/test_proj_with_vulnerability_simplified.md index e02a44fc2..bbec4b096 100644 --- a/testdata/messages/integration/test_proj_with_vulnerability_simplified.md +++ b/testdata/messages/integration/test_proj_with_vulnerability_simplified.md @@ -5,26 +5,60 @@ **🚨 Frogbot scanned this pull request and found the below:** + --- -## 📦 Vulnerable Dependencies +## 📗 Scan Summary --- +- Frogbot scanned for vulnerabilities and found 1 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done | 1 Issues Found: ❗️ 1 Critical | +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | Not Found | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ✅ Done | Not Found | --- -### ✍️ Summary +### 📦 Vulnerable Dependencies --- -| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | + +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| Critical | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4], [1.2.6] | CVE-2021-44906 | +| Critical | CVE-2021-44906 | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4], [1.2.6] | + + +--- +### 🔖 Details + +--- + + +--- +### Vulnerability Details + --- -### 🔬 Research Details +| | | +| --------------------- | :-----------------------------------: | +| **Jfrog Research Severity:** | 🔴 High | +| **Contextual Analysis:** | Not Applicable | +| **Direct Dependencies:** | minimist:1.2.5 | +| **Impacted Dependency:** | minimist:1.2.5 | +| **Fixed Versions:** | [0.2.4], [1.2.6] | +| **CVSS V3:** | 9.8 | + +Insufficient input validation in Minimist npm package leads to prototype pollution of constructor functions when parsing arbitrary arguments. + --- +### 🔬 JFrog Research Details +--- **Description:** [Minimist](https://github.com/substack/minimist) is a simple and very popular argument parser. It is used by more than 14 million by Mar 2022. This package developers stopped developing it since April 2020 and its community released a [newer version](https://github.com/meszaros-lajos-gyorgy/minimist-lite) supported by the community. @@ -46,5 +80,6 @@ This vulnerability can be triggered when the attacker-controlled input is parsed Add 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. + --- [🐸 JFrog Frogbot](https://docs.jfrog-applications.jfrog.io/jfrog-applications/frogbot) \ No newline at end of file diff --git a/testdata/messages/integration/test_proj_with_vulnerability_standard.md b/testdata/messages/integration/test_proj_with_vulnerability_standard.md index 9a9649763..836075a91 100644 --- a/testdata/messages/integration/test_proj_with_vulnerability_standard.md +++ b/testdata/messages/integration/test_proj_with_vulnerability_standard.md @@ -9,21 +9,47 @@
-## 📦 Vulnerable Dependencies -### ✍️ Summary +## 📗 Scan Summary +- Frogbot scanned for vulnerabilities and found 1 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done |
1 Issues Found 1 Critical
| +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | Not Found | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ✅ Done | Not Found | + +### 📦 Vulnerable Dependencies +
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4]
[1.2.6] | CVE-2021-44906 | +| ![critical (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | CVE-2021-44906 | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4]
[1.2.6] |
-### 🔬 Research Details +### 🔖 Details + +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Jfrog Research Severity:** | High | +| **Contextual Analysis:** | Not Applicable | +| **Direct Dependencies:** | minimist:1.2.5 | +| **Impacted Dependency:** | minimist:1.2.5 | +| **Fixed Versions:** | [0.2.4], [1.2.6] | +| **CVSS V3:** | 9.8 | + +Insufficient input validation in Minimist npm package leads to prototype pollution of constructor functions when parsing arbitrary arguments. + +### 🔬 JFrog Research Details + **Description:** [Minimist](https://github.com/substack/minimist) is a simple and very popular argument parser. It is used by more than 14 million by Mar 2022. This package developers stopped developing it since April 2020 and its community released a [newer version](https://github.com/meszaros-lajos-gyorgy/minimist-lite) supported by the community. @@ -44,6 +70,7 @@ This vulnerability can be triggered when the attacker-controlled input is parsed Add 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. + ---
diff --git a/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_simplified.md b/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_simplified.md index a372250ce..3ac239297 100644 --- a/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_simplified.md +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_simplified.md @@ -1,21 +1,24 @@ --- -## 📦🔍 Contextual Analysis CVE Vulnerability +## 📦🔍 Contextual Analysis CVE --- -| Severity | Impacted Dependency | Finding | CVE | +| Severity | ID | Impacted Dependency | Finding | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| Critical | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | CVE-2022-29361 | +| Critical | CVE-2022-29361 | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | --- ### Description --- + The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`. + --- ### CVE details --- -cveDetails \ No newline at end of file + +cveDetails diff --git a/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_standard.md b/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_standard.md index 610aeac07..b04e3f0a8 100644 --- a/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_standard.md +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_standard.md @@ -1,25 +1,16 @@ -## 📦🔍 Contextual Analysis CVE Vulnerability +## 📦🔍 Contextual Analysis CVE
-| Severity | Impacted Dependency | Finding | CVE | +| Severity | ID | Impacted Dependency | Finding | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableCriticalSeverity.png)
Critical | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | CVE-2022-29361 | +| Critical | CVE-2022-29361 | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called |
-
- Description -
- +
Description The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`. - -
- -
- CVE details -
- +
+
CVE details cveDetails - -
+
\ No newline at end of file diff --git a/testdata/messages/reviewcomment/applicable/applicable_review_content_simplified.md b/testdata/messages/reviewcomment/applicable/applicable_review_content_simplified.md index 3e04f85ba..4d4700253 100644 --- a/testdata/messages/reviewcomment/applicable/applicable_review_content_simplified.md +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_simplified.md @@ -1,27 +1,34 @@ --- -## 📦🔍 Contextual Analysis CVE Vulnerability +## 📦🔍 Contextual Analysis CVE --- -| Severity | Impacted Dependency | Finding | CVE | +| Severity | ID | Impacted Dependency | Finding | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| Critical | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | CVE-2022-29361 | +| Critical | CVE-2022-29361 | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | --- ### Description --- + The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`. + --- ### CVE details --- + cveDetails + --- ### Remediation --- -some remediation \ No newline at end of file + + +some remediation + diff --git a/testdata/messages/reviewcomment/applicable/applicable_review_content_standard.md b/testdata/messages/reviewcomment/applicable/applicable_review_content_standard.md index 6cc4fe622..aad78f2ed 100644 --- a/testdata/messages/reviewcomment/applicable/applicable_review_content_standard.md +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_standard.md @@ -1,33 +1,21 @@ -## 📦🔍 Contextual Analysis CVE Vulnerability +## 📦🔍 Contextual Analysis CVE
-| Severity | Impacted Dependency | Finding | CVE | +| Severity | ID | Impacted Dependency | Finding | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableCriticalSeverity.png)
Critical | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | CVE-2022-29361 | +| ![critical](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableCriticalSeverity.png)
Critical | CVE-2022-29361 | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called |
-
- Description -
- +
Description The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`. - -
- -
- CVE details -
- +
+
CVE details cveDetails - -
- -
- Remediation -
+
+
Remediation some remediation -
+
\ No newline at end of file diff --git a/testdata/messages/reviewcomment/iac/iac_review_content_simplified.md b/testdata/messages/reviewcomment/iac/iac_review_content_simplified.md index c526eb6f4..771f3e4c5 100644 --- a/testdata/messages/reviewcomment/iac/iac_review_content_simplified.md +++ b/testdata/messages/reviewcomment/iac/iac_review_content_simplified.md @@ -8,17 +8,17 @@ | :---------------------: | :-----------------------------------: | | Medium | Missing auto upgrade was detected | + --- ### Full description --- -Resource `google_container_node_pool` should have `management.auto_upgrade=true` - -Vulnerable example - -``` -resource "google_container_node_pool" "vulnerable_example" { - management { - auto_upgrade = false - } -} -``` + + + +--- +### Vulnerability Details + +--- +Scanner Description.... + diff --git a/testdata/messages/reviewcomment/iac/iac_review_content_standard.md b/testdata/messages/reviewcomment/iac/iac_review_content_standard.md index 7c96b0682..2800ec55d 100644 --- a/testdata/messages/reviewcomment/iac/iac_review_content_standard.md +++ b/testdata/messages/reviewcomment/iac/iac_review_content_standard.md @@ -4,24 +4,14 @@ | Severity | Finding | | :---------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Missing auto upgrade was detected | +| ![medium](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Missing auto upgrade was detected |
-
- Full description -
-Resource `google_container_node_pool` should have `management.auto_upgrade=true` +
Full description -Vulnerable example - -``` -resource "google_container_node_pool" "vulnerable_example" { - management { - auto_upgrade = false - } -} -``` +### Vulnerability Details +Scanner Description.... - -
+
\ No newline at end of file diff --git a/testdata/messages/reviewcomment/iac/iac_violation_review_content_simplified.md b/testdata/messages/reviewcomment/iac/iac_violation_review_content_simplified.md new file mode 100644 index 000000000..bc1b3a215 --- /dev/null +++ b/testdata/messages/reviewcomment/iac/iac_violation_review_content_simplified.md @@ -0,0 +1,24 @@ + + +--- +## 🛠️ Infrastructure as Code Violation + +--- +| Severity | ID | Finding | Watch Name | Policies | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Medium | iac-violation-id | Missing auto upgrade was detected | jas-watch | policy1, policy2 | + + +--- +### Full description + +--- + + + +--- +### Violation Details + +--- +Scanner Description.... + diff --git a/testdata/messages/reviewcomment/iac/iac_violation_review_content_standard.md b/testdata/messages/reviewcomment/iac/iac_violation_review_content_standard.md new file mode 100644 index 000000000..65b56b2cc --- /dev/null +++ b/testdata/messages/reviewcomment/iac/iac_violation_review_content_standard.md @@ -0,0 +1,17 @@ + +## 🛠️ Infrastructure as Code Violation +
+ +| Severity | ID | Finding | Watch Name | Policies | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![medium](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | iac-violation-id | Missing auto upgrade was detected | jas-watch | policy1
policy2 | + +
+ + +
Full description + +### Violation Details +Scanner Description.... + +
\ No newline at end of file diff --git a/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_simplified.md b/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_simplified.md index 73567b6bd..e7c8a9fb4 100644 --- a/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_simplified.md +++ b/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_simplified.md @@ -6,16 +6,24 @@ --- | Severity | Finding | | :---------------------: | :-----------------------------------: | -| Low | Stack Trace Exposure | +| Low | Found a Use of Insecure Random | + --- ### Full description --- -### Overview -Stack trace exposure is a type of security vulnerability that occurs when a program reveals -sensitive information, such as the names and locations of internal files and variables, -in error messages or other diagnostic output. This can happen when a program crashes or -encounters an error, and the stack trace (a record of the program's call stack at the time -of the error) is included in the output. \ No newline at end of file + + +--- +### Vulnerability Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798, CWE-799 | +| **Rule ID:** | js-insecure-random | + +Scanner Description.... + diff --git a/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_standard.md b/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_standard.md index eb2cffdbc..a02a52a9d 100644 --- a/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_standard.md +++ b/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_standard.md @@ -4,20 +4,19 @@ | Severity | Finding | | :---------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Stack Trace Exposure | +| Low | Found a Use of Insecure Random | -
- Full description -
+
Full description -### Overview -Stack trace exposure is a type of security vulnerability that occurs when a program reveals -sensitive information, such as the names and locations of internal files and variables, -in error messages or other diagnostic output. This can happen when a program crashes or -encounters an error, and the stack trace (a record of the program's call stack at the time -of the error) is included in the output. +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798
CWE-799 | +| **Rule ID:** | js-insecure-random | -
+Scanner Description.... + +
\ No newline at end of file diff --git a/testdata/messages/reviewcomment/sast/sast_review_content_simplified.md b/testdata/messages/reviewcomment/sast/sast_review_content_simplified.md index 366c7c4c2..18e64d62e 100644 --- a/testdata/messages/reviewcomment/sast/sast_review_content_simplified.md +++ b/testdata/messages/reviewcomment/sast/sast_review_content_simplified.md @@ -6,19 +6,28 @@ --- | Severity | Finding | | :---------------------: | :-----------------------------------: | -| Low | Stack Trace Exposure | +| Low | Found a Use of Insecure Random | + --- ### Full description --- -### Overview -Stack trace exposure is a type of security vulnerability that occurs when a program reveals -sensitive information, such as the names and locations of internal files and variables, -in error messages or other diagnostic output. This can happen when a program crashes or -encounters an error, and the stack trace (a record of the program's call stack at the time -of the error) is included in the output. + + +--- +### Vulnerability Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798, CWE-799 | +| **Rule ID:** | js-insecure-random | + +Scanner Description.... + + --- ### Code Flows @@ -31,6 +40,7 @@ of the error) is included in the output. --- + ↘️ `other-snippet` (at file2 line 1) ↘️ `snippet` (at file line 0) @@ -41,6 +51,7 @@ of the error) is included in the output. --- + ↘️ `a-snippet` (at file line 10) ↘️ `snippet` (at file line 0) diff --git a/testdata/messages/reviewcomment/sast/sast_review_content_standard.md b/testdata/messages/reviewcomment/sast/sast_review_content_standard.md index 6794f48e9..29d579cd3 100644 --- a/testdata/messages/reviewcomment/sast/sast_review_content_standard.md +++ b/testdata/messages/reviewcomment/sast/sast_review_content_standard.md @@ -4,52 +4,32 @@ | Severity | Finding | | :---------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Stack Trace Exposure | +| ![low](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Found a Use of Insecure Random | -
- Full description -
+
Full description -### Overview -Stack trace exposure is a type of security vulnerability that occurs when a program reveals -sensitive information, such as the names and locations of internal files and variables, -in error messages or other diagnostic output. This can happen when a program crashes or -encounters an error, and the stack trace (a record of the program's call stack at the time -of the error) is included in the output. +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798
CWE-799 | +| **Rule ID:** | js-insecure-random | -
+Scanner Description.... -
- Code Flows -
- - -
- Vulnerable data flow analysis result -
+
Code Flows +
Vulnerable data flow analysis result ↘️ `other-snippet` (at file2 line 1) ↘️ `snippet` (at file line 0) - - -
- -
- Vulnerable data flow analysis result -
- +
+
Vulnerable data flow analysis result ↘️ `a-snippet` (at file line 10) ↘️ `snippet` (at file line 0) - - -
- - -
+


\ No newline at end of file diff --git a/testdata/messages/reviewcomment/sast/sast_violation_review_content_simplified.md b/testdata/messages/reviewcomment/sast/sast_violation_review_content_simplified.md new file mode 100644 index 000000000..a51db5c0c --- /dev/null +++ b/testdata/messages/reviewcomment/sast/sast_violation_review_content_simplified.md @@ -0,0 +1,78 @@ + + +--- +## 🎯 Static Application Security Testing (SAST) Violation + +--- +| Severity | ID | Finding | Watch Name | Policies | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Low | sast-violation-id | Found a Use of Insecure Random | jas-watch | policy1, policy2 | +| High | sast-violation-id-2 | Found a Use of Insecure Random | jas-watch2 | policy3 | +| High | sast-violation-id-3 | Found An Express Not Using Helmet | jas-watch2 | policy3 | + + +--- +### [ Express Not Using Helmet ] + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Rule ID:** | js-express-without-helmet | + +Scanner Description.... + + + +--- +### [ Use of Insecure Random ] + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798, CWE-799 | +| **Rule ID:** | js-insecure-random | + +Scanner Description.... + + + +--- +### Code Flows + +--- + + +--- +#### Vulnerable data flow analysis result + +--- + + +↘️ `other-snippet` (at file2 line 1) + +↘️ `snippet` (at file line 0) + + +--- +#### Vulnerable data flow analysis result + +--- + + +↘️ `a-snippet` (at file line 10) + +↘️ `snippet` (at file line 0) diff --git a/testdata/messages/reviewcomment/sast/sast_violation_review_content_standard.md b/testdata/messages/reviewcomment/sast/sast_violation_review_content_standard.md new file mode 100644 index 000000000..a7336d73c --- /dev/null +++ b/testdata/messages/reviewcomment/sast/sast_violation_review_content_standard.md @@ -0,0 +1,47 @@ + +## 🎯 Static Application Security Testing (SAST) Violation +
+ +| Severity | ID | Finding | Watch Name | Policies | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![low](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | sast-violation-id | Found a Use of Insecure Random | jas-watch | policy1
policy2 | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | sast-violation-id-2 | Found a Use of Insecure Random | jas-watch2 | policy3 | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | sast-violation-id-3 | Found An Express Not Using Helmet | jas-watch2 | policy3 | + +
+ + +
[ Express Not Using Helmet ] + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **Rule ID:** | js-express-without-helmet | + +Scanner Description.... + +
+
[ Use of Insecure Random ] + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798
CWE-799 | +| **Rule ID:** | js-insecure-random | + +Scanner Description.... + + +
Code Flows +
Vulnerable data flow analysis result + +↘️ `other-snippet` (at file2 line 1) + +↘️ `snippet` (at file line 0) +
+
Vulnerable data flow analysis result + +↘️ `a-snippet` (at file line 10) + +↘️ `snippet` (at file line 0) +


\ No newline at end of file diff --git a/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_simplified.md b/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_simplified.md index d953e303a..bd908fba0 100644 --- a/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_simplified.md +++ b/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_simplified.md @@ -1,43 +1,29 @@ --- -## 🗝️ Secret Detected +## 🤫 Secret Vulnerability --- | Severity | Finding | | :---------------------: | :-----------------------------------: | -| Medium | Secret keys were found | +| High | Secret keys were found | + --- ### Full description --- -Storing hardcoded secrets in your source code or binary artifact could lead to several risks. - -If 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. - -## Best practices - -Use safe storage when storing high-privilege secrets such as passwords and tokens, for example - -* ### Environment Variables -Environment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example - -`SECRET_VAR=MySecret ./my_application` -This way, `MySecret` does not have to be hardcoded into `my_application`. -Note 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. - -* ### Secret management services - -External 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 - +--- +### Vulnerability Details -* [Hashicorp Vault](https://www.vaultproject.io) -* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service) -* [Google Cloud KMS](https://cloud.google.com/security-key-management) +--- +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798, CWE-799 | +| **Abbreviation:** | rule-id | -## Least-privilege principle +Scanner Description.... -Storing 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. -For 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. -That 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. diff --git a/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_standard.md b/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_standard.md index 6f2607504..07e267c96 100644 --- a/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_standard.md +++ b/testdata/messages/reviewcomment/secrets/secret_review_content_no_ca_standard.md @@ -1,46 +1,22 @@ -## 🗝️ Secret Detected +## 🤫 Secret Vulnerability
| Severity | Finding | | :---------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Secret keys were found | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Secret keys were found |
-
- Full description -
-Storing hardcoded secrets in your source code or binary artifact could lead to several risks. +
Full description -If 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. +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798
CWE-799 | +| **Abbreviation:** | rule-id | -## Best practices +Scanner Description.... -Use safe storage when storing high-privilege secrets such as passwords and tokens, for example - - -* ### Environment Variables - -Environment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example - -`SECRET_VAR=MySecret ./my_application` -This way, `MySecret` does not have to be hardcoded into `my_application`. - -Note 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. - -* ### Secret management services - -External 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 - - -* [Hashicorp Vault](https://www.vaultproject.io) -* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service) -* [Google Cloud KMS](https://cloud.google.com/security-key-management) - -## Least-privilege principle - -Storing 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. -For 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. -That 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. - - -
+
\ No newline at end of file diff --git a/testdata/messages/reviewcomment/secrets/secret_review_content_simplified.md b/testdata/messages/reviewcomment/secrets/secret_review_content_simplified.md index e258a4311..2dccece96 100644 --- a/testdata/messages/reviewcomment/secrets/secret_review_content_simplified.md +++ b/testdata/messages/reviewcomment/secrets/secret_review_content_simplified.md @@ -1,43 +1,29 @@ --- -## 🗝️ Secret Detected +## 🤫 Secret Vulnerability --- -| Severity | Finding | Status | +| Severity | Status | Finding | | :---------------------: | :-----------------------------------: | :-----------------------------------: | -| Medium | Secret keys were found | Active | +| High | Active | Secret keys were found | + --- ### Full description --- -Storing hardcoded secrets in your source code or binary artifact could lead to several risks. - -If 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. - -## Best practices - -Use safe storage when storing high-privilege secrets such as passwords and tokens, for example - -* ### Environment Variables -Environment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example - -`SECRET_VAR=MySecret ./my_application` -This way, `MySecret` does not have to be hardcoded into `my_application`. -Note 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. - -* ### Secret management services - -External 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 - +--- +### Vulnerability Details -* [Hashicorp Vault](https://www.vaultproject.io) -* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service) -* [Google Cloud KMS](https://cloud.google.com/security-key-management) +--- +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798, CWE-799 | +| **Abbreviation:** | rule-id | -## Least-privilege principle +Scanner Description.... -Storing 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. -For 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. -That 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. diff --git a/testdata/messages/reviewcomment/secrets/secret_review_content_standard.md b/testdata/messages/reviewcomment/secrets/secret_review_content_standard.md index d0b5a45f8..97e70ed1f 100644 --- a/testdata/messages/reviewcomment/secrets/secret_review_content_standard.md +++ b/testdata/messages/reviewcomment/secrets/secret_review_content_standard.md @@ -1,46 +1,22 @@ -## 🗝️ Secret Detected +## 🤫 Secret Vulnerability
-| Severity | Finding | Status | +| Severity | Status | Finding | | :---------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Secret keys were found | Active | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Active | Secret keys were found |
-
- Full description -
-Storing hardcoded secrets in your source code or binary artifact could lead to several risks. +
Full description -If 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. +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798
CWE-799 | +| **Abbreviation:** | rule-id | -## Best practices +Scanner Description.... -Use safe storage when storing high-privilege secrets such as passwords and tokens, for example - - -* ### Environment Variables - -Environment variables are set outside of the application code, and can be dynamically passed to the application only when needed, for example - -`SECRET_VAR=MySecret ./my_application` -This way, `MySecret` does not have to be hardcoded into `my_application`. - -Note 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. - -* ### Secret management services - -External 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 - - -* [Hashicorp Vault](https://www.vaultproject.io) -* [AWS KMS](https://aws.amazon.com/kms) (Key Management Service) -* [Google Cloud KMS](https://cloud.google.com/security-key-management) - -## Least-privilege principle - -Storing 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. -For 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. -That 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. - - -
+
\ No newline at end of file diff --git a/testdata/messages/reviewcomment/secrets/secret_violation_review_content_simplified.md b/testdata/messages/reviewcomment/secrets/secret_violation_review_content_simplified.md new file mode 100644 index 000000000..54788f9b8 --- /dev/null +++ b/testdata/messages/reviewcomment/secrets/secret_violation_review_content_simplified.md @@ -0,0 +1,30 @@ + + +--- +## 🤫 Secret Violation + +--- +| Severity | ID | Status | Finding | Watch Name | Policies | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| High | secret-violation-id | Active | Secret keys were found | jas-watch | policy1 | +| Critical | secret-violation-id-2 | Inactive | Secret keys were found | jas-watch2 | policy1, policy2 | + + +--- +### Full description + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798, CWE-799 | +| **Abbreviation:** | rule-id | + +Scanner Description.... + diff --git a/testdata/messages/reviewcomment/secrets/secret_violation_review_content_standard.md b/testdata/messages/reviewcomment/secrets/secret_violation_review_content_standard.md new file mode 100644 index 000000000..395e57ce7 --- /dev/null +++ b/testdata/messages/reviewcomment/secrets/secret_violation_review_content_standard.md @@ -0,0 +1,23 @@ + +## 🤫 Secret Violation +
+ +| Severity | ID | Status | Finding | Watch Name | Policies | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | secret-violation-id | Active | Secret keys were found | jas-watch | policy1 | +| ![critical (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | secret-violation-id-2 | Inactive | Secret keys were found | jas-watch2 | policy1
policy2 | + +
+ + +
Full description + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **CWE:** | CWE-798
CWE-799 | +| **Abbreviation:** | rule-id | + +Scanner Description.... + +
\ No newline at end of file diff --git a/testdata/messages/summarycomment/license/license_violation_simplified.md b/testdata/messages/summarycomment/license/license_violation_simplified.md deleted file mode 100644 index 9ab5310f6..000000000 --- a/testdata/messages/summarycomment/license/license_violation_simplified.md +++ /dev/null @@ -1,10 +0,0 @@ - - ---- -## ⚖️ Violated Licenses - ---- -| SEVERITY | LICENSE | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | -| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| High | License1 | Comp1 1.0 | Dep1 2.0 | -| High | License2 | root 1.0.0, minimatch 1.2.3 | Dep2 3.0 | \ No newline at end of file diff --git a/testdata/messages/summarycomment/license/license_violation_standard.md b/testdata/messages/summarycomment/license/license_violation_standard.md deleted file mode 100644 index b94665bfb..000000000 --- a/testdata/messages/summarycomment/license/license_violation_standard.md +++ /dev/null @@ -1,10 +0,0 @@ - -## ⚖️ Violated Licenses -
- -| SEVERITY | LICENSE | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | -| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| High | License1 | Comp1 1.0 | Dep1 2.0 | -| High | License2 | root 1.0.0
minimatch 1.2.3 | Dep2 3.0 | - -
diff --git a/testdata/messages/summarycomment/structure/fix_mr_not_entitled.md b/testdata/messages/summarycomment/structure/fix_mr_not_entitled.md index 2d5fafec7..a9209d088 100644 --- a/testdata/messages/summarycomment/structure/fix_mr_not_entitled.md +++ b/testdata/messages/summarycomment/structure/fix_mr_not_entitled.md @@ -12,9 +12,7 @@ ``` some content ``` -
- Note: - +
Note ---
@@ -22,10 +20,7 @@ some content **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/fix_pr_not_entitled.md b/testdata/messages/summarycomment/structure/fix_pr_not_entitled.md index 8e10ad30f..5c733d825 100644 --- a/testdata/messages/summarycomment/structure/fix_pr_not_entitled.md +++ b/testdata/messages/summarycomment/structure/fix_pr_not_entitled.md @@ -12,9 +12,7 @@ ``` some content ``` -
- Note: - +
Note ---
@@ -22,10 +20,7 @@ some content **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/fix_simplified_not_entitled.md b/testdata/messages/summarycomment/structure/fix_simplified_not_entitled.md index 6b9aeb3fa..e646ce1a1 100644 --- a/testdata/messages/summarycomment/structure/fix_simplified_not_entitled.md +++ b/testdata/messages/summarycomment/structure/fix_simplified_not_entitled.md @@ -7,11 +7,7 @@ ``` some content ``` - ---- -Note: - ---- +Note: --- **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system. diff --git a/testdata/messages/summarycomment/structure/summary_comment_issues_mr_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_issues_mr_not_entitled.md index c0d9be271..1854bf32b 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_issues_mr_not_entitled.md +++ b/testdata/messages/summarycomment/structure/summary_comment_issues_mr_not_entitled.md @@ -12,9 +12,7 @@ ``` some content ``` -
- Note: - +
Note ---
@@ -22,10 +20,7 @@ some content **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md index 8011e0be6..8de14eb5b 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md +++ b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled.md @@ -12,9 +12,7 @@ ``` some content ``` -
- Note: - +
Note ---
@@ -22,10 +20,7 @@ some content **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md index 96c89e5c7..428ee98d1 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md +++ b/testdata/messages/summarycomment/structure/summary_comment_issues_pr_not_entitled_with_title.md @@ -13,9 +13,7 @@ ``` some content ``` -
- Note: - +
Note ---
@@ -23,10 +21,7 @@ some content **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md index ed09e547e..94fad5000 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md +++ b/testdata/messages/summarycomment/structure/summary_comment_issues_simplified_not_entitled.md @@ -7,11 +7,7 @@ ``` some content ``` - ---- -Note: - ---- +Note: --- **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system. diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md index a58ad8a9d..269ca7943 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md +++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_mr_not_entitled.md @@ -8,9 +8,7 @@
-
- Note: - +
Note ---
@@ -18,10 +16,7 @@ **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md index 3c88b0a97..225fea261 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md +++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled.md @@ -8,9 +8,7 @@
-
- Note: - +
Note ---
@@ -18,10 +16,7 @@ **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md index 3382eeb5f..62a6afc04 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md +++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_pr_not_entitled_with_title.md @@ -9,9 +9,7 @@
## **Custom title** -
- Note: - +
Note ---
@@ -19,10 +17,7 @@ **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system.
- - -
- +
---
diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md index f55ec3edb..6340da3d1 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md +++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled.md @@ -3,11 +3,7 @@ [comment]: <> (FrogbotReviewComment) **👍 Frogbot scanned this pull request and did not find any new security issues.** - ---- -Note: - ---- +Note: --- **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system. diff --git a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md index a6830f144..6039ac225 100644 --- a/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md +++ b/testdata/messages/summarycomment/structure/summary_comment_no_issues_simplified_not_entitled_with_title.md @@ -8,11 +8,7 @@ ## **Custom title** --- - ---- -Note: - ---- +Note: --- **Frogbot** also supports **Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/advanced-security) package, which isn't enabled on your system. diff --git a/testdata/messages/summarycomment/summary/summary_both_simplified.md b/testdata/messages/summarycomment/summary/summary_both_simplified.md new file mode 100644 index 000000000..c7c9bf2c2 --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_both_simplified.md @@ -0,0 +1,15 @@ + + +--- +## 📗 Scan Summary + +--- +- Frogbot scanned for violations and vulnerabilities and found 13 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done | 9 Issues Found: ❗️ 2 Critical, 🔴 3 High, 🟠 2 Medium, 🟡 1 Low, ⚪️ 1 Unknown | +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | 4 Issues Found: 🔴 3 High, 🟡 1 Low | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - | \ No newline at end of file diff --git a/testdata/messages/summarycomment/summary/summary_both_standard.md b/testdata/messages/summarycomment/summary/summary_both_standard.md new file mode 100644 index 000000000..30f1b29a4 --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_both_standard.md @@ -0,0 +1,11 @@ + +## 📗 Scan Summary +- Frogbot scanned for violations and vulnerabilities and found 13 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done |
9 Issues Found 2 Critical
3 High
2 Medium
1 Low
1 Unknown
| +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done |
4 Issues Found 3 High
1 Low
| +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - | \ No newline at end of file diff --git a/testdata/messages/summarycomment/summary/summary_error_simplified.md b/testdata/messages/summarycomment/summary/summary_error_simplified.md new file mode 100644 index 000000000..ebe16f635 --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_error_simplified.md @@ -0,0 +1,7 @@ + + +--- +## 📗 Scan Summary + +--- +- Frogbot attempted to scan for violations but encountered an error. \ No newline at end of file diff --git a/testdata/messages/summarycomment/summary/summary_error_standard.md b/testdata/messages/summarycomment/summary/summary_error_standard.md new file mode 100644 index 000000000..7849db553 --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_error_standard.md @@ -0,0 +1,3 @@ + +## 📗 Scan Summary +- Frogbot attempted to scan for violations but encountered an error. \ No newline at end of file diff --git a/testdata/messages/summarycomment/summary/summary_simplified.md b/testdata/messages/summarycomment/summary/summary_simplified.md new file mode 100644 index 000000000..64bab849c --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_simplified.md @@ -0,0 +1,15 @@ + + +--- +## 📗 Scan Summary + +--- +- Frogbot scanned for vulnerabilities and found 9 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done | 6 Issues Found: ❗️ 1 Critical, 🔴 2 High, 🟠 1 Medium, 🟡 1 Low, ⚪️ 1 Unknown | +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | 3 Issues Found: 🔴 2 High, 🟡 1 Low | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - | \ No newline at end of file diff --git a/testdata/messages/summarycomment/summary/summary_standard.md b/testdata/messages/summarycomment/summary/summary_standard.md new file mode 100644 index 000000000..5244ba6bf --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_standard.md @@ -0,0 +1,11 @@ + +## 📗 Scan Summary +- Frogbot scanned for vulnerabilities and found 9 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done |
6 Issues Found 1 Critical
2 High
1 Medium
1 Low
1 Unknown
| +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done |
3 Issues Found 2 High
1 Low
| +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - | \ No newline at end of file diff --git a/testdata/messages/summarycomment/summary/summary_violation_simplified.md b/testdata/messages/summarycomment/summary/summary_violation_simplified.md new file mode 100644 index 000000000..58185dd84 --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_violation_simplified.md @@ -0,0 +1,15 @@ + + +--- +## 📗 Scan Summary + +--- +- Frogbot scanned for violations and found 4 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done | 3 Issues Found: ❗️ 1 Critical, 🔴 1 High, 🟠 1 Medium | +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | 1 Issues Found: 🔴 1 High | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - | \ No newline at end of file diff --git a/testdata/messages/summarycomment/summary/summary_violation_standard.md b/testdata/messages/summarycomment/summary/summary_violation_standard.md new file mode 100644 index 000000000..245da163e --- /dev/null +++ b/testdata/messages/summarycomment/summary/summary_violation_standard.md @@ -0,0 +1,11 @@ + +## 📗 Scan Summary +- Frogbot scanned for violations and found 4 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done |
3 Issues Found 1 Critical
1 High
1 Medium
| +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done |
1 Issues Found 1 High
| +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ℹ️ Not Scanned | - | \ No newline at end of file diff --git a/testdata/messages/summarycomment/violations/license/license_violation_simplified.md b/testdata/messages/summarycomment/violations/license/license_violation_simplified.md new file mode 100644 index 000000000..a1297e325 --- /dev/null +++ b/testdata/messages/summarycomment/violations/license/license_violation_simplified.md @@ -0,0 +1,69 @@ + + +--- +## 🚥 Policy Violations + +--- + + + +--- +### ⚖️ License Violations + +--- + +| Severity | License | Direct Dependencies | Impacted Dependency | Watch Name | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| High | License1 | Comp1:1.0 | Dep1:2.0 | watch | +| High | License2 | root:1.0.0 | Dep2:3.0 | watch2 | +| | | minimatch:1.2.3 | | | + + +--- +### 🔖 Details + +--- + + + +--- +#### [ License1 ] Dep1 2.0 (watch) + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Policies:** | policy1, policy2 | +| **Watch Name:** | watch | +| **Direct Dependencies:** | Comp1:1.0 | +| **Impacted Dependency:** | Dep1:2.0 | +| **Full Name:** | License1 full name | + + + + +--- +#### [ License2 ] Dep2 3.0 (watch2) + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Policies:** | policy3 | +| **Watch Name:** | watch2 | +| **Direct Dependencies:** | root:1.0.0, minimatch:1.2.3 | +| **Impacted Dependency:** | Dep2:3.0 | +| **Full Name:** | - | + diff --git a/testdata/messages/summarycomment/violations/license/license_violation_standard.md b/testdata/messages/summarycomment/violations/license/license_violation_standard.md new file mode 100644 index 000000000..1aecc3806 --- /dev/null +++ b/testdata/messages/summarycomment/violations/license/license_violation_standard.md @@ -0,0 +1,44 @@ + +## 🚥 Policy Violations + + +### ⚖️ License Violations + +
+ +| Severity | License | Direct Dependencies | Impacted Dependency | Watch Name | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | License1 | Comp1:1.0 | Dep1:2.0 | watch | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | License2 | root:1.0.0
minimatch:1.2.3 | Dep2:3.0 | watch2 | + +
+ + +### 🔖 Details + + +
[ License1 ] Dep1 2.0 (watch) + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **Policies:** | policy1, policy2 | +| **Watch Name:** | watch | +| **Direct Dependencies:** | Comp1:1.0 | +| **Impacted Dependency:** | Dep1:2.0 | +| **Full Name:** | License1 full name | + +
+ +
[ License2 ] Dep2 3.0 (watch2) + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **Policies:** | policy3 | +| **Watch Name:** | watch2 | +| **Direct Dependencies:** | root:1.0.0, minimatch:1.2.3 | +| **Impacted Dependency:** | Dep2:3.0 | +| **Full Name:** | - | + +
\ No newline at end of file diff --git a/testdata/messages/summarycomment/violations/security/security_violation_simplified.md b/testdata/messages/summarycomment/violations/security/security_violation_simplified.md new file mode 100644 index 000000000..159980804 --- /dev/null +++ b/testdata/messages/summarycomment/violations/security/security_violation_simplified.md @@ -0,0 +1,106 @@ + + + +--- +### 🚨 Security Violations + +--- + +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Watch Name | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0 | impacted:3.0.0 | - | +| | | | dep2:2.0.0 | | | +| High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server:v0.21.0 | - | +| Medium | CVE-2022-26652, CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D:v0.21.0 | - | +| Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3:v3.5.1 | - | + + +--- +### 🔖 Details + +--- + + + +--- +#### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.1] | +| **CVSS V3:** | - | + +Summary XRAY-122345 + + +--- +### 🔬 JFrog Research Details + +--- + +**Remediation:** +some remediation + + + +--- +#### [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Applicable | +| **Direct Dependencies:** | component-D:v0.21.0 | +| **Impacted Dependency:** | component-D:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | + + +--- +### 🔬 JFrog Research Details + +--- + +**Remediation:** +some remediation + + + +--- +#### github.com/mholt/archiver/v3 v3.5.1 + +--- + + + +--- +### Violation Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Fixed Versions:** | - | +| **CVSS V3:** | - | + +Summary \ No newline at end of file diff --git a/testdata/messages/summarycomment/violations/security/security_violation_standard.md b/testdata/messages/summarycomment/violations/security/security_violation_standard.md new file mode 100644 index 000000000..55b1d196b --- /dev/null +++ b/testdata/messages/summarycomment/violations/security/security_violation_standard.md @@ -0,0 +1,67 @@ + + +### 🚨 Security Violations + +
+ +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Watch Name | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![critical (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted:3.0.0 | - | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server:v0.21.0 | - | +| ![medium](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | CVE-2022-26652
CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D:v0.21.0 | - | +| ![low](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3:v3.5.1 | - | + +
+ + +### 🔖 Details + + +
[ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.1] | +| **CVSS V3:** | - | + +Summary XRAY-122345 + +### 🔬 JFrog Research Details + +**Remediation:** +some remediation +
+ +
[ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Applicable | +| **Direct Dependencies:** | component-D:v0.21.0 | +| **Impacted Dependency:** | component-D:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | + +### 🔬 JFrog Research Details + +**Remediation:** +some remediation +
+ +
github.com/mholt/archiver/v3 v3.5.1 + +### Violation Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Fixed Versions:** | - | +| **CVSS V3:** | - | + +Summary
\ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md index f032744fd..899a11a99 100644 --- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md @@ -1,15 +1,11 @@ ---- -## 📦 Vulnerable Dependencies --- - +### 📦 Vulnerable Dependencies --- -### ✍️ Summary ---- -| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 | \ No newline at end of file +| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | \ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md index b194f1f7c..09cfb721e 100644 --- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md @@ -1,11 +1,11 @@ -## 📦 Vulnerable Dependencies -### ✍️ Summary +### 📦 Vulnerable Dependencies +
-| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 | +| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] |
diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md index e2ec84fb2..41609c4b6 100644 --- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md @@ -1,28 +1,46 @@ + --- -## 📦 Vulnerable Dependencies +### 📦 Vulnerable Dependencies --- +| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | + --- -### ✍️ Summary +### 🔖 Details --- -| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | -| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 | + + --- -### 🔬 Research Details +### Vulnerability Details --- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | +Summary CVE-2022-26652 + + +--- +### 🔬 JFrog Research Details + +--- **Description:** Research CVE-2022-26652 details **Remediation:** -some remediation \ No newline at end of file +some remediation diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md index 7a9f1c5fa..535a6abb1 100644 --- a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md @@ -1,21 +1,35 @@ -## 📦 Vulnerable Dependencies -### ✍️ Summary +### 📦 Vulnerable Dependencies +
-| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] | CVE-2022-26652 | +| Medium | CVE-2022-26652 | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.3] |
-### 🔬 Research Details +### 🔖 Details + + + +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | + +Summary CVE-2022-26652 +### 🔬 JFrog Research Details **Description:** Research CVE-2022-26652 details **Remediation:** -some remediation \ No newline at end of file +some remediation diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md index 31866fcc6..cc77fc444 100644 --- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md @@ -1,53 +1,106 @@ + --- -## 📦 Vulnerable Dependencies +### 📦 Vulnerable Dependencies --- +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 | +| | | | dep2:2.0.0 | | | +| High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | +| Medium | CVE-2022-26652, CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | +| Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | + --- -### ✍️ Summary +### 🔖 Details --- -| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | -| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| Critical | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 | CVE-1111-11111 | -| | | dep2:2.0.0 | | | | -| High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - | -| Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652, CVE-2023-4321 | -| Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - | + --- -### 🔬 Research Details +#### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 --- + --- -#### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 +### Vulnerability Details --- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.1] | +| **CVSS V3:** | - | -**Description:** Summary XRAY-122345 + +--- +### 🔬 JFrog Research Details + +--- + **Remediation:** some remediation + + --- #### [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 --- + + +--- +### Vulnerability Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Applicable | +| **Direct Dependencies:** | component-D:v0.21.0 | +| **Impacted Dependency:** | component-D:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | + + +--- +### 🔬 JFrog Research Details + +--- + **Remediation:** some remediation + + +--- +#### github.com/mholt/archiver/v3 v3.5.1 + +--- + + + --- -#### github.com/mholt/archiver/v3 v3.5.1 +### Vulnerability Details --- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Fixed Versions:** | - | +| **CVSS V3:** | - | -**Description:** Summary \ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md index e3e2343ed..237e2b70a 100644 --- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split1.md @@ -1,19 +1,15 @@ ---- -## 📦 Vulnerable Dependencies --- - +### 📦 Vulnerable Dependencies --- -### ✍️ Summary ---- -| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| Critical | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 | CVE-1111-11111 | -| | | dep2:2.0.0 | | | | -| High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - | -| Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652, CVE-2023-4321 | -| Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - | \ No newline at end of file +| Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0 | impacted 3.0.0 | 4.0.0, 5.0.0 | +| | | | dep2:2.0.0 | | | +| High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | +| Medium | CVE-2022-26652, CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | +| Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | \ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md index 01b482e36..57e2fae2e 100644 --- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified_split2.md @@ -2,34 +2,105 @@ --- -### 🔬 Research Details +### 📦 Vulnerable Dependencies --- + +--- +### 🔖 Details + +--- + + + --- #### [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 --- -**Description:** + + +--- +### Vulnerability Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.1] | +| **CVSS V3:** | - | + Summary XRAY-122345 + +--- +### 🔬 JFrog Research Details + +--- + **Remediation:** some remediation + + +--- +### 🔖 Details + +--- + + + --- #### [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 --- + + +--- +### Vulnerability Details + +--- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Applicable | +| **Direct Dependencies:** | component-D:v0.21.0 | +| **Impacted Dependency:** | component-D:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | + + +--- +### 🔬 JFrog Research Details + +--- + **Remediation:** some remediation + + +--- +#### github.com/mholt/archiver/v3 v3.5.1 + +--- + + + --- -#### github.com/mholt/archiver/v3 v3.5.1 +### Vulnerability Details --- +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Fixed Versions:** | - | +| **CVSS V3:** | - | -**Description:** Summary \ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md index a9f78101c..1280d16b8 100644 --- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md @@ -1,50 +1,67 @@ -## 📦 Vulnerable Dependencies -### ✍️ Summary +### 📦 Vulnerable Dependencies +
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 | CVE-1111-11111 | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652
CVE-2023-4321 | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - | +| ![critical (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | +| ![medium](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | CVE-2022-26652
CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | +| ![low](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - |
-### 🔬 Research Details +### 🔖 Details + -
- [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 -
+
[ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.1] | +| **CVSS V3:** | - | -**Description:** Summary XRAY-122345 +### 🔬 JFrog Research Details + **Remediation:** some remediation +
-
+
[ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 -
- [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 -
+### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Applicable | +| **Direct Dependencies:** | component-D:v0.21.0 | +| **Impacted Dependency:** | component-D:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | +### 🔬 JFrog Research Details **Remediation:** some remediation +
-
- -
- github.com/mholt/archiver/v3 v3.5.1 -
- +
github.com/mholt/archiver/v3 v3.5.1 -**Description:** -Summary +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Fixed Versions:** | - | +| **CVSS V3:** | - | -
+Summary
\ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md index bbe11a316..11e307420 100644 --- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split1.md @@ -1,14 +1,14 @@ -## 📦 Vulnerable Dependencies -### ✍️ Summary +### 📦 Vulnerable Dependencies +
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 | CVE-1111-11111 | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | - | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | CVE-2022-26652
CVE-2023-4321 | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - | - | +| ![critical (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | CVE-1111-11111 | Not Applicable | dep1:1.0.0
dep2:2.0.0 | impacted 3.0.0 | 4.0.0
5.0.0 | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | XRAY-122345 | Undetermined | github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server v0.21.0 | [0.24.1] | +| ![medium](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | CVE-2022-26652
CVE-2023-4321 | Applicable | component-D:v0.21.0 | component-D v0.21.0 | [0.24.3] | +| ![low](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | - | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3 v3.5.1 | - |
diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md index c9114db28..40be1e952 100644 --- a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard_split2.md @@ -1,36 +1,56 @@ -### 🔬 Research Details +### 📦 Vulnerable Dependencies -
- [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 -
+### 🔖 Details + + +
[ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 + +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Impacted Dependency:** | github.com/nats-io/nats-streaming-server:v0.21.0 | +| **Fixed Versions:** | [0.24.1] | +| **CVSS V3:** | - | -**Description:** Summary XRAY-122345 +### 🔬 JFrog Research Details + **Remediation:** some remediation +
-
+
[ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 -
- [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 -
+### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Applicable | +| **Direct Dependencies:** | component-D:v0.21.0 | +| **Impacted Dependency:** | component-D:v0.21.0 | +| **Fixed Versions:** | [0.24.3] | +| **CVSS V3:** | - | +### 🔬 JFrog Research Details **Remediation:** some remediation +
-
- -
- github.com/mholt/archiver/v3 v3.5.1 -
- +
github.com/mholt/archiver/v3 v3.5.1 -**Description:** -Summary +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Undetermined | +| **Direct Dependencies:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Impacted Dependency:** | github.com/mholt/archiver/v3:v3.5.1 | +| **Fixed Versions:** | - | +| **CVSS V3:** | - | -
+Summary
\ No newline at end of file diff --git a/testdata/projects/poetry/pyproject.toml b/testdata/projects/poetry/pyproject.toml index c878acfc1..cc42d0070 100755 --- a/testdata/projects/poetry/pyproject.toml +++ b/testdata/projects/poetry/pyproject.toml @@ -3,6 +3,7 @@ name = "poetry-project" version = "0.1.0" description = "" authors = ["Your Name "] +package-mode = false [tool.poetry.dependencies] python = "^3.10" diff --git a/testdata/scanpullrequest/expected_response.md b/testdata/scanpullrequest/expected_response.md index 9a9649763..836075a91 100644 --- a/testdata/scanpullrequest/expected_response.md +++ b/testdata/scanpullrequest/expected_response.md @@ -9,21 +9,47 @@
-## 📦 Vulnerable Dependencies -### ✍️ Summary +## 📗 Scan Summary +- Frogbot scanned for vulnerabilities and found 1 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done |
1 Issues Found 1 Critical
| +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | Not Found | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ✅ Done | Not Found | + +### 📦 Vulnerable Dependencies +
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4]
[1.2.6] | CVE-2021-44906 | +| ![critical (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
Critical | CVE-2021-44906 | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4]
[1.2.6] |
-### 🔬 Research Details +### 🔖 Details + +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Jfrog Research Severity:** | High | +| **Contextual Analysis:** | Not Applicable | +| **Direct Dependencies:** | minimist:1.2.5 | +| **Impacted Dependency:** | minimist:1.2.5 | +| **Fixed Versions:** | [0.2.4], [1.2.6] | +| **CVSS V3:** | 9.8 | + +Insufficient input validation in Minimist npm package leads to prototype pollution of constructor functions when parsing arbitrary arguments. + +### 🔬 JFrog Research Details + **Description:** [Minimist](https://github.com/substack/minimist) is a simple and very popular argument parser. It is used by more than 14 million by Mar 2022. This package developers stopped developing it since April 2020 and its community released a [newer version](https://github.com/meszaros-lajos-gyorgy/minimist-lite) supported by the community. @@ -44,6 +70,7 @@ This vulnerability can be triggered when the attacker-controlled input is parsed Add 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. + ---
diff --git a/testdata/scanpullrequest/expected_response_multi_dir.md b/testdata/scanpullrequest/expected_response_multi_dir.md index ae7d4a745..e83ab7d5d 100644 --- a/testdata/scanpullrequest/expected_response_multi_dir.md +++ b/testdata/scanpullrequest/expected_response_multi_dir.md @@ -9,35 +9,61 @@
-## 📦 Vulnerable Dependencies -### ✍️ Summary +## 📗 Scan Summary +- Frogbot scanned for vulnerabilities and found 2 issues + +| Scan Category | Status | Security Issues | +| --------------------- | :-----------------------------------: | ----------------------------------- | +| **Software Composition Analysis** | ✅ Done |
2 Issues Found 2 High
| +| **Contextual Analysis** | ✅ Done | - | +| **Static Application Security Testing (SAST)** | ✅ Done | Not Found | +| **Secrets** | ✅ Done | - | +| **Infrastructure as Code (IaC)** | ✅ Done | Not Found | + +### 📦 Vulnerable Dependencies +
-| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| Severity | ID | Contextual Analysis | Direct Dependencies | Impacted Dependency | Fixed Versions | | :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableHigh.png)
High | Not Applicable | minimatch:3.0.4 | minimatch 3.0.4 | [3.0.5] | CVE-2022-3517 | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Not Covered | pyjwt:1.7.1 | pyjwt 1.7.1 | [2.4.0] | CVE-2022-29217 | +| ![high (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableHigh.png)
High | CVE-2022-3517 | Not Applicable | minimatch:3.0.4 | minimatch 3.0.4 | [3.0.5] | +| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | CVE-2022-29217 | Not Covered | pyjwt:1.7.1 | pyjwt 1.7.1 | [2.4.0] |
-### 🔬 Research Details +### 🔖 Details -
- [ CVE-2022-3517 ] minimatch 3.0.4 -
+
[ CVE-2022-3517 ] minimatch 3.0.4 -**Description:** -A vulnerability was found in the minimatch package. This flaw allows a Regular Expression Denial of Service (ReDoS) when calling the braceExpand function with specific arguments, resulting in a Denial of Service. +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Contextual Analysis:** | Not Applicable | +| **Direct Dependencies:** | minimatch:3.0.4 | +| **Impacted Dependency:** | minimatch:3.0.4 | +| **Fixed Versions:** | [3.0.5] | +| **CVSS V3:** | 7.5 | -
+A vulnerability was found in the minimatch package. This flaw allows a Regular Expression Denial of Service (ReDoS) when calling the braceExpand function with specific arguments, resulting in a Denial of Service.
-
- [ CVE-2022-29217 ] pyjwt 1.7.1 -
+
[ CVE-2022-29217 ] pyjwt 1.7.1 +### Vulnerability Details +| | | +| --------------------- | :-----------------------------------: | +| **Jfrog Research Severity:** | Medium | +| **Contextual Analysis:** | Not Covered | +| **Direct Dependencies:** | pyjwt:1.7.1 | +| **Impacted Dependency:** | pyjwt:1.7.1 | +| **Fixed Versions:** | [2.4.0] | +| **CVSS V3:** | 7.5 | + +Algorithm confusion in PyJWT leads to authentication bypass. + +### 🔬 JFrog Research Details **Description:** [PyJWT](https://pypi.org/project/PyJWT) is a Python implementation of the RFC 7519 standard (JSON Web Tokens). [JSON Web Tokens](https://jwt.io/) are an open, industry standard method for representing claims securely between two parties. A JWT comes with an inline signature that is meant to be verified by the receiving application. JWT supports multiple standard algorithms, and the algorithm itself is **specified in the JWT token itself**. @@ -76,9 +102,7 @@ For example, replace the following call - `jwt.decode(encoded_jwt, pub_key_bytes, algorithms=jwt.algorithms.get_default_algorithms())` With - `jwt.decode(encoded_jwt, pub_key_bytes, algorithms=["ES256"])` - -
- +
--- diff --git a/utils/analytics.go b/utils/analytics.go index aeaf31e4e..a36274b7a 100644 --- a/utils/analytics.go +++ b/utils/analytics.go @@ -6,11 +6,10 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-security/utils/xsc" - "github.com/jfrog/jfrog-client-go/xray/services" xscservices "github.com/jfrog/jfrog-client-go/xsc/services" ) -func CreateScanEvent(serviceDetails *config.ServerDetails, gitInfo *services.XscGitInfoContext, scanType string) *xscservices.XscAnalyticsGeneralEvent { +func CreateScanEvent(serviceDetails *config.ServerDetails, gitInfo *xscservices.XscGitInfoContext, scanType string) *xscservices.XscAnalyticsGeneralEvent { event := xsc.CreateAnalyticsEvent(xscservices.FrogbotProduct, xscservices.FrogbotType, serviceDetails) event.ProductVersion = FrogbotVersion event.FrogbotScanType = scanType diff --git a/utils/analytics_test.go b/utils/analytics_test.go index 5fc5d48eb..a722ad9dd 100644 --- a/utils/analytics_test.go +++ b/utils/analytics_test.go @@ -4,23 +4,22 @@ import ( "testing" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-client-go/xray/services" xscservices "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/stretchr/testify/assert" ) func TestCreateAnalyticsGeneralEvent(t *testing.T) { - gitInfoContext := &services.XscGitInfoContext{ - GitRepoUrl: "http://localhost:8080/my-user/my-project.git", - GitRepoName: "my-project", - GitProject: "my-user", - GitProvider: "GitHub", - Technologies: nil, - BranchName: "main", - LastCommit: "https://api.github.com/repos/my-user/my-project/commits/a23ba44a0d379dida668nmb72003a82e4e11d0ba", - CommitHash: "a23ba44a0d379dida668nmb72003a82e4e11d0ba", - CommitMessage: ".", - CommitAuthor: "User", + gitInfoContext := &xscservices.XscGitInfoContext{ + GitRepoHttpsCloneUrl: "http://localhost:8080/my-user/my-project.git", + GitRepoName: "my-project", + GitProject: "my-user", + GitProvider: "GitHub", + Technologies: nil, + BranchName: "main", + LastCommitUrl: "https://api.github.com/repos/my-user/my-project/commits/a23ba44a0d379dida668nmb72003a82e4e11d0ba", + LastCommitHash: "a23ba44a0d379dida668nmb72003a82e4e11d0ba", + LastCommitMessage: ".", + LastCommitAuthor: "User", } serverDetails := &config.ServerDetails{ diff --git a/utils/comment.go b/utils/comment.go index f353a17bd..6c1563a11 100644 --- a/utils/comment.go +++ b/utils/comment.go @@ -7,9 +7,11 @@ import ( "sort" "strings" + "github.com/jfrog/frogbot/v2/utils/issues" "github.com/jfrog/frogbot/v2/utils/outputwriter" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -31,7 +33,8 @@ const ( commentRemovalErrorMsg = "An error occurred while attempting to remove older Frogbot pull request comments:" ) -func HandlePullRequestCommentsAfterScan(issues *IssuesCollection, repo *Repository, client vcsclient.VcsClient, pullRequestID int) (err error) { +// In Scan PR, if there are no issues, comments will be added to the PR with a message that there are no issues. +func HandlePullRequestCommentsAfterScan(issues *issues.ScansIssuesCollection, resultContext results.ResultContext, repo *Repository, client vcsclient.VcsClient, pullRequestID int) (err error) { if !repo.Params.AvoidPreviousPrCommentsDeletion { // The removal of comments may fail for various reasons, // such as concurrent scanning of pull requests and attempts @@ -45,8 +48,8 @@ func HandlePullRequestCommentsAfterScan(issues *IssuesCollection, repo *Reposito } // Add summary (SCA, license) scan comment - if issues.IssuesExists() || repo.AddPrCommentOnSuccess { - for _, comment := range generatePullRequestSummaryComment(issues, repo.OutputWriter) { + if issues.IssuesExists(repo.PullRequestSecretComments) || repo.AddPrCommentOnSuccess { + for _, comment := range generatePullRequestSummaryComment(*issues, resultContext, repo.PullRequestSecretComments, repo.OutputWriter) { if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, comment, pullRequestID); err != nil { err = errors.New("couldn't add pull request comment: " + err.Error()) return @@ -78,7 +81,7 @@ func DeleteExistingPullRequestComments(repository *Repository, client vcsclient. "failed to get comments. the following details were used in order to fetch the comments: <%s/%s> pull request #%d. the error received: %s", repository.RepoOwner, repository.RepoName, int(repository.PullRequestDetails.ID), err.Error()) } - commentsToDelete := getFrogbotComments(repository.OutputWriter, comments) + commentsToDelete := getFrogbotComments(comments) // Delete if len(commentsToDelete) > 0 { for _, commentToDelete := range commentsToDelete { @@ -91,7 +94,7 @@ func DeleteExistingPullRequestComments(repository *Repository, client vcsclient. } func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) (description string, extraComments []string) { - content := outputwriter.GetPRSummaryContent(outputwriter.VulnerabilitiesContent(vulnerabilities, writer), true, false, writer) + content := outputwriter.GetMainCommentContent(outputwriter.GetVulnerabilitiesContent(vulnerabilities, writer), true, false, writer) if len(content) == 1 { // Limit is not reached, use the entire content as the description description = content[0] @@ -108,19 +111,22 @@ func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViol return } -func generatePullRequestSummaryComment(issuesCollection *IssuesCollection, writer outputwriter.OutputWriter) []string { - if !issuesCollection.IssuesExists() { - return outputwriter.GetPRSummaryContent([]string{}, false, true, writer) +func generatePullRequestSummaryComment(issuesCollection issues.ScansIssuesCollection, resultContext results.ResultContext, includeSecrets bool, writer outputwriter.OutputWriter) []string { + if !issuesCollection.IssuesExists(includeSecrets) { + // No Issues + return outputwriter.GetMainCommentContent([]string{}, false, true, writer) } - - content := []string{} - if vulnerabilitiesContent := outputwriter.VulnerabilitiesContent(issuesCollection.Vulnerabilities, writer); len(vulnerabilitiesContent) > 0 { - content = append(content, vulnerabilitiesContent...) + // Summary + content := []string{outputwriter.ScanSummaryContent(issuesCollection, resultContext, includeSecrets, writer)} + // Violations + if violationsContent := outputwriter.PolicyViolationsContent(issuesCollection, writer); len(violationsContent) > 0 { + content = append(content, violationsContent...) } - if licensesContent := outputwriter.LicensesContent(issuesCollection.Licenses, writer); len(licensesContent) > 0 { - content = append(content, licensesContent) + // Vulnerabilities + if vulnerabilitiesContent := outputwriter.GetVulnerabilitiesContent(issuesCollection.ScaVulnerabilities, writer); len(vulnerabilitiesContent) > 0 { + content = append(content, vulnerabilitiesContent...) } - return outputwriter.GetPRSummaryContent(content, true, true, writer) + return outputwriter.GetMainCommentContent(content, true, true, writer) } func IsFrogbotRescanComment(comment string) bool { @@ -139,7 +145,7 @@ func GetSortedPullRequestComments(client vcsclient.VcsClient, repoOwner, repoNam return pullRequestsComments, nil } -func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.VcsClient, issues *IssuesCollection) (err error) { +func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.VcsClient, issues *issues.ScansIssuesCollection) (err error) { commentsToAdd := getNewReviewComments(repo, issues) if len(commentsToAdd) == 0 { return @@ -149,7 +155,7 @@ func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.Vcs log.Debug("creating a review comment for", comment.Type, comment.Location.File, comment.Location.StartLine, comment.Location.StartColumn) if e := client.AddPullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, comment.CommentInfo); e != nil { log.Debug("couldn't add pull request review comment, fallback to regular comment: " + e.Error()) - if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, outputwriter.GetFallbackReviewCommentContent(comment.CommentInfo.Content, comment.Location, repo.OutputWriter), pullRequestID); err != nil { + if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, outputwriter.GetFallbackReviewCommentContent(comment.CommentInfo.Content, comment.Location), pullRequestID); err != nil { err = errors.New("couldn't add pull request comment, fallback to comment: " + err.Error()) return } @@ -168,7 +174,7 @@ func DeleteExistingPullRequestReviewComments(repo *Repository, pullRequestID int } // Delete old review comments if len(existingComments) > 0 { - if err = client.DeletePullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, getFrogbotComments(repo.OutputWriter, existingComments)...); err != nil { + if err = client.DeletePullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, getFrogbotComments(existingComments)...); err != nil { err = errors.New("couldn't delete pull request review comment: " + err.Error()) return } @@ -176,9 +182,9 @@ func DeleteExistingPullRequestReviewComments(repo *Repository, pullRequestID int return } -func getFrogbotComments(writer outputwriter.OutputWriter, existingComments []vcsclient.CommentInfo) (reviewComments []vcsclient.CommentInfo) { +func getFrogbotComments(existingComments []vcsclient.CommentInfo) (reviewComments []vcsclient.CommentInfo) { for _, comment := range existingComments { - if outputwriter.IsFrogbotComment(comment.Content) || outputwriter.IsFrogbotSummaryComment(writer, comment.Content) { + if outputwriter.IsFrogbotComment(comment.Content) { log.Debug("Deleting comment id:", comment.ID) reviewComments = append(reviewComments, comment) } @@ -186,33 +192,76 @@ func getFrogbotComments(writer outputwriter.OutputWriter, existingComments []vcs return } -func getNewReviewComments(repo *Repository, issues *IssuesCollection) (commentsToAdd []ReviewComment) { +func getNewReviewComments(repo *Repository, issues *issues.ScansIssuesCollection) (commentsToAdd []ReviewComment) { writer := repo.OutputWriter - - for _, vulnerability := range issues.Vulnerabilities { - for _, cve := range vulnerability.Cves { - if cve.Applicability != nil { - for _, evidence := range cve.Applicability.Evidence { - commentsToAdd = append(commentsToAdd, generateReviewComment(ApplicableComment, evidence.Location, generateApplicabilityReviewContent(evidence, cve, vulnerability, writer))) - } - } - } + // CVE Applicable Evidence review comments + for _, applicableEvidences := range issues.GetApplicableEvidences() { + commentsToAdd = append(commentsToAdd, generateReviewComment(ApplicableComment, applicableEvidences.Evidence.Location, generateApplicabilityReviewContent(applicableEvidences, writer))) } - for _, iac := range issues.Iacs { - commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, iac.Location, generateSourceCodeReviewContent(IacComment, iac, writer))) + // IAC review comments + for _, iac := range issues.IacVulnerabilities { + commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, iac.Location, generateSourceCodeReviewContent(IacComment, false, writer, iac))) } - for _, sast := range issues.Sast { - commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, sast.Location, generateSourceCodeReviewContent(SastComment, sast, writer))) + for _, similarIacIssues := range groupSimilarJasIssues(issues.IacViolations) { + commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, similarIacIssues.Location, generateSourceCodeReviewContent(IacComment, true, writer, similarIacIssues.issues...))) } + // SAST review comments + for _, sast := range issues.SastVulnerabilities { + commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, sast.Location, generateSourceCodeReviewContent(SastComment, false, writer, sast))) + } + if len(issues.SastViolations) > 0 { + for _, similarSastIssues := range groupSimilarJasIssues(issues.SastViolations) { + commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, similarSastIssues.Location, generateSourceCodeReviewContent(SastComment, true, writer, similarSastIssues.issues...))) + } + } + // Secrets review comments if !repo.Params.PullRequestSecretComments { return } - for _, secret := range issues.Secrets { - commentsToAdd = append(commentsToAdd, generateReviewComment(SecretComment, secret.Location, generateSourceCodeReviewContent(SecretComment, secret, writer))) + for _, secret := range issues.SecretsVulnerabilities { + commentsToAdd = append(commentsToAdd, generateReviewComment(SecretComment, secret.Location, generateSourceCodeReviewContent(SecretComment, false, writer, secret))) + } + if len(issues.SecretsViolations) > 0 { + for _, similarSecretsIssues := range groupSimilarJasIssues(issues.SecretsViolations) { + commentsToAdd = append(commentsToAdd, generateReviewComment(SecretComment, similarSecretsIssues.Location, generateSourceCodeReviewContent(SecretComment, true, writer, similarSecretsIssues.issues...))) + } + } + return +} + +type jasCommentIssues struct { + // The location of the issue that the comment will be added to. + formats.Location + // Similar issues at the same location that will be shown in the same comment. + issues []formats.SourceCodeRow +} + +// For JAS violations we can have similar issues at the same location, we need to group similar issues to add them to the same comment based on `getSourceCodeRowId`. +func groupSimilarJasIssues(issues []formats.SourceCodeRow) (groupedIssues []jasCommentIssues) { + idToIssues := make(map[string]jasCommentIssues) + for _, issue := range issues { + id := getSourceCodeRowId(issue) + if similarIssue, ok := idToIssues[id]; ok { + similarIssue.issues = append(similarIssue.issues, issue) + idToIssues[id] = similarIssue + continue + } + idToIssues[id] = jasCommentIssues{ + Location: issue.Location, + issues: []formats.SourceCodeRow{issue}, + } + } + for _, similarIssue := range idToIssues { + groupedIssues = append(groupedIssues, similarIssue) } return } +// Similar comment should have the same location and rule-id. +func getSourceCodeRowId(issue formats.SourceCodeRow) string { + return issue.RuleId + issue.Location.ToString() +} + func generateReviewComment(commentType ReviewCommentType, location formats.Location, content string) (comment ReviewComment) { return ReviewComment{ Location: location, @@ -227,52 +276,18 @@ func generateReviewComment(commentType ReviewCommentType, location formats.Locat } -func generateApplicabilityReviewContent(issue formats.Evidence, relatedCve formats.CveRow, relatedVulnerability formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) string { - remediation := "" - if relatedVulnerability.JfrogResearchInformation != nil { - remediation = relatedVulnerability.JfrogResearchInformation.Remediation - } - return outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent( - relatedVulnerability.Severity, - issue.Reason, - relatedCve.Applicability.ScannerDescription, - relatedCve.Id, - relatedVulnerability.Summary, - fmt.Sprintf("%s:%s", relatedVulnerability.ImpactedDependencyName, relatedVulnerability.ImpactedDependencyVersion), - remediation, - writer, - ), writer) +func generateApplicabilityReviewContent(issue issues.ApplicableEvidences, writer outputwriter.OutputWriter) string { + return outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent(issue, writer), writer) } -func generateSourceCodeReviewContent(commentType ReviewCommentType, issue formats.SourceCodeRow, writer outputwriter.OutputWriter) (content string) { +func generateSourceCodeReviewContent(commentType ReviewCommentType, violation bool, writer outputwriter.OutputWriter, similarIssues ...formats.SourceCodeRow) (content string) { switch commentType { case IacComment: - return outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent( - issue.Severity, - issue.Finding, - issue.ScannerDescription, - writer, - ), writer) + return outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent(violation, writer, similarIssues...), writer) case SastComment: - return outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent( - issue.Severity, - issue.Finding, - issue.ScannerDescription, - issue.CodeFlow, - writer, - ), writer) + return outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent(violation, writer, similarIssues...), writer) case SecretComment: - applicability := "" - if issue.Applicability != nil { - applicability = issue.Applicability.Status - } - return outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent( - issue.Severity, - issue.Finding, - issue.ScannerDescription, - applicability, - writer, - ), writer) + return outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent(violation, writer, similarIssues...), writer) } return } diff --git a/utils/comment_test.go b/utils/comment_test.go index 30591b7be..9c7fb29f7 100644 --- a/utils/comment_test.go +++ b/utils/comment_test.go @@ -3,6 +3,7 @@ package utils import ( "testing" + "github.com/jfrog/frogbot/v2/utils/issues" "github.com/jfrog/frogbot/v2/utils/outputwriter" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/jfrog-cli-security/utils/formats" @@ -11,7 +12,6 @@ import ( ) func TestGetFrogbotReviewComments(t *testing.T) { - writer := &outputwriter.StandardOutput{} testCases := []struct { name string existingComments []vcsclient.CommentInfo @@ -43,24 +43,177 @@ func TestGetFrogbotReviewComments(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := getFrogbotComments(writer, tc.existingComments) + output := getFrogbotComments(tc.existingComments) assert.ElementsMatch(t, tc.expectedOutput, output) }) } } +func TestGroupSimilarJasIssues(t *testing.T) { + testCases := []struct { + name string + issues []formats.SourceCodeRow + groupedIssues []jasCommentIssues + }{ + { + name: "No issues", + }, + { + name: "Single issue", + issues: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + }, + groupedIssues: []jasCommentIssues{ + { + formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + }, + }, + }, + }, + { + name: "Multiple issues - no similar issues", + issues: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule2"}, + }, + }, + groupedIssues: []jasCommentIssues{ + { + formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + }, + }, + { + formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule2"}, + }, + }, + }, + }, + }, + { + name: "Multiple issues - with similar issues", + issues: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding2", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding3", + ScannerInfo: formats.ScannerInfo{RuleId: "rule2"}, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + Location: formats.Location{File: "file2", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding2", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + }, + groupedIssues: []jasCommentIssues{ + { + formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding1", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding2", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + }, + }, + { + formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding3", + ScannerInfo: formats.ScannerInfo{RuleId: "rule2"}, + }, + }, + }, + { + formats.Location{File: "file2", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + Location: formats.Location{File: "file2", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}, + Finding: "finding2", + ScannerInfo: formats.ScannerInfo{RuleId: "rule1"}, + }, + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output := groupSimilarJasIssues(tc.issues) + assert.ElementsMatch(t, tc.groupedIssues, output) + }) + } +} + func TestGetNewReviewComments(t *testing.T) { writer := &outputwriter.StandardOutput{} testCases := []struct { name string generateSecretsComments bool - issues *IssuesCollection + issues *issues.ScansIssuesCollection expectedOutput []ReviewComment }{ { name: "No issues for review comments", - issues: &IssuesCollection{ - Vulnerabilities: []formats.VulnerabilityOrViolationRow{ + issues: &issues.ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{ { Summary: "summary-2", Applicable: "Applicable", @@ -73,13 +226,16 @@ func TestGetNewReviewComments(t *testing.T) { Technology: techutils.Npm, }, }, - Secrets: []formats.SourceCodeRow{ + SecretsVulnerabilities: []formats.SourceCodeRow{ { SeverityDetails: formats.SeverityDetails{ Severity: "High", SeverityNumValue: 13, }, Finding: "Secret", + ScannerInfo: formats.ScannerInfo{ + RuleId: "secret-rule", + }, Location: formats.Location{ File: "index.js", StartLine: 5, @@ -96,8 +252,8 @@ func TestGetNewReviewComments(t *testing.T) { { name: "Secret review comments", generateSecretsComments: true, - issues: &IssuesCollection{ - Vulnerabilities: []formats.VulnerabilityOrViolationRow{ + issues: &issues.ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{ { Summary: "summary-2", Applicable: "Applicable", @@ -110,7 +266,7 @@ func TestGetNewReviewComments(t *testing.T) { Technology: techutils.Npm, }, }, - Secrets: []formats.SourceCodeRow{ + SecretsVulnerabilities: []formats.SourceCodeRow{ { SeverityDetails: formats.SeverityDetails{ Severity: "High", @@ -118,6 +274,9 @@ func TestGetNewReviewComments(t *testing.T) { }, Finding: "secret finding", Applicability: &formats.Applicability{Status: "Inactive"}, + ScannerInfo: formats.ScannerInfo{ + RuleId: "secret-rule", + }, Location: formats.Location{ File: "index.js", StartLine: 5, @@ -142,7 +301,135 @@ func TestGetNewReviewComments(t *testing.T) { Type: SecretComment, CommentInfo: vcsclient.PullRequestComment{ CommentInfo: vcsclient.CommentInfo{ - Content: outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent("High", "secret finding", "", "Inactive", writer), writer), + Content: outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent(false, writer, formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "secret-rule", + }, + Finding: "secret finding", + Applicability: &formats.Applicability{Status: "Inactive"}, + }), writer), + }, + PullRequestDiff: vcsclient.PullRequestDiff{ + OriginalFilePath: "index.js", + OriginalStartLine: 5, + OriginalStartColumn: 6, + OriginalEndLine: 7, + OriginalEndColumn: 8, + NewFilePath: "index.js", + NewStartLine: 5, + NewStartColumn: 6, + NewEndLine: 7, + NewEndColumn: 8, + }, + }, + }, + }, + }, + { + name: "Multiple violations, one review comments", + generateSecretsComments: true, + issues: &issues.ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{ + { + Summary: "summary-2", + Applicable: "Applicable", + IssueId: "XRAY-2", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "low"}, + ImpactedDependencyName: "component-C", + }, + Cves: []formats.CveRow{{Id: "CVE-2023-4321"}}, + Technology: techutils.Npm, + }, + }, + SecretsViolations: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "secret-rule", + }, + Finding: "secret finding", + Applicability: &formats.Applicability{Status: "Inactive"}, + Location: formats.Location{ + File: "index.js", + StartLine: 5, + StartColumn: 6, + EndLine: 7, + EndColumn: 8, + Snippet: "access token exposed", + }, + ViolationContext: formats.ViolationContext{ + Watch: "watch", + }, + }, + { + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "secret-rule", + }, + Finding: "secret finding", + Applicability: &formats.Applicability{Status: "Inactive"}, + Location: formats.Location{ + File: "index.js", + StartLine: 5, + StartColumn: 6, + EndLine: 7, + EndColumn: 8, + Snippet: "access token exposed", + }, + ViolationContext: formats.ViolationContext{ + Watch: "watch2", + }, + }, + }, + }, + expectedOutput: []ReviewComment{ + { + Location: formats.Location{ + File: "index.js", + StartLine: 5, + StartColumn: 6, + EndLine: 7, + EndColumn: 8, + Snippet: "access token exposed", + }, + Type: SecretComment, + CommentInfo: vcsclient.PullRequestComment{ + CommentInfo: vcsclient.CommentInfo{ + Content: outputwriter.GenerateReviewCommentContent(outputwriter.SecretReviewContent(true, writer, + formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 13}, + ScannerInfo: formats.ScannerInfo{ + RuleId: "secret-rule", + }, + Finding: "secret finding", + Applicability: &formats.Applicability{Status: "Inactive"}, + ViolationContext: formats.ViolationContext{ + Watch: "watch", + }, + }, + formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 13}, + ScannerInfo: formats.ScannerInfo{ + RuleId: "secret-rule", + }, + Finding: "secret finding", + Applicability: &formats.Applicability{Status: "Inactive"}, + ViolationContext: formats.ViolationContext{ + Watch: "watch2", + }, + }, + ), writer), }, PullRequestDiff: vcsclient.PullRequestDiff{ OriginalFilePath: "index.js", @@ -162,8 +449,8 @@ func TestGetNewReviewComments(t *testing.T) { }, { name: "With issues for review comments", - issues: &IssuesCollection{ - Vulnerabilities: []formats.VulnerabilityOrViolationRow{ + issues: &issues.ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{ { Summary: "summary-2", Applicable: "Applicable", @@ -176,12 +463,15 @@ func TestGetNewReviewComments(t *testing.T) { Technology: techutils.Npm, }, }, - Iacs: []formats.SourceCodeRow{ + IacVulnerabilities: []formats.SourceCodeRow{ { SeverityDetails: formats.SeverityDetails{ Severity: "High", SeverityNumValue: 13, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "aws-violation", + }, Finding: "Missing auto upgrade was detected", Location: formats.Location{ File: "file1", @@ -193,12 +483,15 @@ func TestGetNewReviewComments(t *testing.T) { }, }, }, - Sast: []formats.SourceCodeRow{ + SastVulnerabilities: []formats.SourceCodeRow{ { SeverityDetails: formats.SeverityDetails{ Severity: "High", SeverityNumValue: 13, }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "sast-rule", + }, Finding: "XSS Vulnerability", Location: formats.Location{ File: "file1", @@ -224,7 +517,10 @@ func TestGetNewReviewComments(t *testing.T) { Type: ApplicableComment, CommentInfo: vcsclient.PullRequestComment{ CommentInfo: vcsclient.CommentInfo{ - Content: outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent("Low", "", "", "CVE-2023-4321", "summary-2", "component-C:", "", writer), writer), + Content: outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent(issues.ApplicableEvidences{ + Evidence: formats.Evidence{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}, + Severity: "Low", IssueId: "CVE-2023-4321", CveSummary: "summary-2", ImpactedDependency: "component-C", + }, writer), writer), }, PullRequestDiff: vcsclient.PullRequestDiff{ OriginalFilePath: "file1", @@ -252,7 +548,16 @@ func TestGetNewReviewComments(t *testing.T) { Type: IacComment, CommentInfo: vcsclient.PullRequestComment{ CommentInfo: vcsclient.CommentInfo{ - Content: outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent("High", "Missing auto upgrade was detected", "", writer), writer), + Content: outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent(false, writer, formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "aws-violation", + }, + Finding: "Missing auto upgrade was detected", + }), writer), }, PullRequestDiff: vcsclient.PullRequestDiff{ OriginalFilePath: "file1", @@ -280,7 +585,16 @@ func TestGetNewReviewComments(t *testing.T) { Type: SastComment, CommentInfo: vcsclient.PullRequestComment{ CommentInfo: vcsclient.CommentInfo{ - Content: outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent("High", "XSS Vulnerability", "", [][]formats.Location{}, writer), writer), + Content: outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent(false, writer, formats.SourceCodeRow{ + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + ScannerInfo: formats.ScannerInfo{ + RuleId: "sast-rule", + }, + Finding: "XSS Vulnerability", + }), writer), }, PullRequestDiff: vcsclient.PullRequestDiff{ OriginalFilePath: "file1", diff --git a/utils/consts.go b/utils/consts.go index f51e44b77..97b064e6a 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -52,13 +52,16 @@ const ( PullRequestSecretCommentsEnv = "JF_PR_SHOW_SECRETS_COMMENTS" // Repository environment variables - Ignored if the frogbot-config.yml file is used - InstallCommandEnv = "JF_INSTALL_DEPS_CMD" - MaxPnpmTreeDepthEnv = "JF_PNPM_MAX_TREE_DEPTH" - RequirementsFileEnv = "JF_REQUIREMENTS_FILE" - WorkingDirectoryEnv = "JF_WORKING_DIR" - PathExclusionsEnv = "JF_PATH_EXCLUSIONS" - jfrogWatchesEnv = "JF_WATCHES" - jfrogProjectEnv = "JF_PROJECT" + InstallCommandEnv = "JF_INSTALL_DEPS_CMD" + MaxPnpmTreeDepthEnv = "JF_PNPM_MAX_TREE_DEPTH" + RequirementsFileEnv = "JF_REQUIREMENTS_FILE" + WorkingDirectoryEnv = "JF_WORKING_DIR" + PathExclusionsEnv = "JF_PATH_EXCLUSIONS" + jfrogWatchesEnv = "JF_WATCHES" + jfrogProjectEnv = "JF_PROJECT" + // To include vulnerabilities and violations + IncludeVulnerabilitiesEnv = "JF_INCLUDE_VULNERABILITIES" + // To include all the vulnerabilities in the source branch at PR scan IncludeAllVulnerabilitiesEnv = "JF_INCLUDE_ALL_VULNERABILITIES" AvoidPreviousPrCommentsDeletionEnv = "JF_AVOID_PREVIOUS_PR_COMMENTS_DELETION" AddPrCommentOnSuccessEnv = "JF_PR_ADD_SUCCESS_COMMENT" diff --git a/utils/issues/issuescollection.go b/utils/issues/issuescollection.go new file mode 100644 index 000000000..e611cc9df --- /dev/null +++ b/utils/issues/issuescollection.go @@ -0,0 +1,303 @@ +package issues + +import ( + "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" +) + +// Group issues by scan type +type ScansIssuesCollection struct { + formats.ScanStatus + + LicensesViolations []formats.LicenseViolationRow + + ScaVulnerabilities []formats.VulnerabilityOrViolationRow + ScaViolations []formats.VulnerabilityOrViolationRow + + IacVulnerabilities []formats.SourceCodeRow + IacViolations []formats.SourceCodeRow + + SecretsVulnerabilities []formats.SourceCodeRow + SecretsViolations []formats.SourceCodeRow + + SastViolations []formats.SourceCodeRow + SastVulnerabilities []formats.SourceCodeRow +} + +// General methods + +func (ic *ScansIssuesCollection) Append(issues *ScansIssuesCollection) { + if issues == nil { + return + } + // Status + ic.AppendStatus(issues.ScanStatus) + // Sca + if len(issues.ScaVulnerabilities) > 0 { + ic.ScaVulnerabilities = append(ic.ScaVulnerabilities, issues.ScaVulnerabilities...) + } + if len(issues.ScaViolations) > 0 { + ic.ScaViolations = append(ic.ScaViolations, issues.ScaViolations...) + } + if len(issues.LicensesViolations) > 0 { + ic.LicensesViolations = append(ic.LicensesViolations, issues.LicensesViolations...) + } + // Secrets + if len(issues.SecretsVulnerabilities) > 0 { + ic.SecretsVulnerabilities = append(ic.SecretsVulnerabilities, issues.SecretsVulnerabilities...) + } + if len(issues.SecretsViolations) > 0 { + ic.SecretsViolations = append(ic.SecretsViolations, issues.SecretsViolations...) + } + // Sast + if len(issues.SastVulnerabilities) > 0 { + ic.SastVulnerabilities = append(ic.SastVulnerabilities, issues.SastVulnerabilities...) + } + if len(issues.SastViolations) > 0 { + ic.SastViolations = append(ic.SastViolations, issues.SastViolations...) + } + // Iac + if len(issues.IacVulnerabilities) > 0 { + ic.IacVulnerabilities = append(ic.IacVulnerabilities, issues.IacVulnerabilities...) + } + if len(issues.IacViolations) > 0 { + ic.IacViolations = append(ic.IacViolations, issues.IacViolations...) + } +} + +func (ic *ScansIssuesCollection) AppendStatus(scanStatus formats.ScanStatus) { + if ic.ScaStatusCode == nil || (*ic.ScaStatusCode == 0 && scanStatus.ScaStatusCode != nil) { + ic.ScaStatusCode = scanStatus.ScaStatusCode + } + if ic.IacStatusCode == nil || (*ic.IacStatusCode == 0 && scanStatus.IacStatusCode != nil) { + ic.IacStatusCode = scanStatus.IacStatusCode + } + if ic.SecretsStatusCode == nil || (*ic.SecretsStatusCode == 0 && scanStatus.SecretsStatusCode != nil) { + ic.SecretsStatusCode = scanStatus.SecretsStatusCode + } + if ic.SastStatusCode == nil || (*ic.SastStatusCode == 0 && scanStatus.SastStatusCode != nil) { + ic.SastStatusCode = scanStatus.SastStatusCode + } + if ic.ApplicabilityStatusCode == nil || (*ic.ApplicabilityStatusCode == 0 && scanStatus.ApplicabilityStatusCode != nil) { + ic.ApplicabilityStatusCode = scanStatus.ApplicabilityStatusCode + } +} + +func (ic *ScansIssuesCollection) IsScanNotCompleted(scanType utils.SubScanType) bool { + status := ic.GetScanStatus(scanType) + // Failed or not performed scans + return status == nil || *status != 0 +} + +func (ic *ScansIssuesCollection) GetScanStatus(scanType utils.SubScanType) *int { + switch scanType { + case utils.ScaScan: + return ic.ScaStatusCode + case utils.IacScan: + return ic.IacStatusCode + case utils.SecretsScan: + return ic.SecretsStatusCode + case utils.SastScan: + return ic.SastStatusCode + case utils.ContextualAnalysisScan: + return ic.ApplicabilityStatusCode + } + return nil +} + +// Only if performed and failed +func (ic *ScansIssuesCollection) HasErrors() bool { + if scaStatus := ic.GetScanStatus(utils.ScaScan); scaStatus != nil && *scaStatus != 0 { + return true + } + if applicabilityStatus := ic.GetScanStatus(utils.ContextualAnalysisScan); applicabilityStatus != nil && *applicabilityStatus != 0 { + return true + } + if iacStatus := ic.GetScanStatus(utils.IacScan); iacStatus != nil && *iacStatus != 0 { + return true + } + if secretsStatus := ic.GetScanStatus(utils.SecretsScan); secretsStatus != nil && *secretsStatus != 0 { + return true + } + if sastStatus := ic.GetScanStatus(utils.SastScan); sastStatus != nil && *sastStatus != 0 { + return true + } + return false +} + +func (ic *ScansIssuesCollection) GetScanIssuesSeverityCount(scanType utils.SubScanType, vulnerabilities, isViolation bool) map[severityutils.Severity]int { + scanDetails := map[severityutils.Severity]int{} + if scanType == utils.ScaScan { + // Count Sca issues only if requested + if isViolation { + for _, violation := range ic.ScaViolations { + scanDetails[severityutils.GetSeverity(violation.Severity)]++ + } + for _, violation := range ic.LicensesViolations { + scanDetails[severityutils.GetSeverity(violation.Severity)]++ + } + } + if vulnerabilities { + for _, vulnerability := range ic.ScaVulnerabilities { + scanDetails[severityutils.GetSeverity(vulnerability.Severity)]++ + } + } + return scanDetails + } + jasVulnerabilities := []formats.SourceCodeRow{} + jasViolations := []formats.SourceCodeRow{} + switch scanType { + case utils.IacScan: + // Count Iac issues only if requested + if isViolation { + jasViolations = ic.IacViolations + } + if vulnerabilities { + jasVulnerabilities = ic.IacVulnerabilities + } + case utils.SecretsScan: + // Count Secrets issues only if requested + if isViolation { + jasViolations = ic.SecretsViolations + } + if vulnerabilities { + jasVulnerabilities = ic.SecretsVulnerabilities + } + case utils.SastScan: + // Count Sast issues only if requested + if isViolation { + jasViolations = ic.SastViolations + } + if vulnerabilities { + jasVulnerabilities = ic.SastVulnerabilities + } + } + // Count the issues + for _, issue := range jasVulnerabilities { + scanDetails[severityutils.GetSeverity(issue.Severity)]++ + } + for _, issue := range jasViolations { + scanDetails[severityutils.GetSeverity(issue.Severity)]++ + } + return scanDetails +} + +func (ic *ScansIssuesCollection) IssuesExists(includeSecrets bool) bool { + return ic.ScaIssuesExists() || ic.IacIssuesExists() || ic.SastIssuesExists() || (includeSecrets && ic.SecretsIssuesExists()) +} + +func (ic *ScansIssuesCollection) ScaIssuesExists() bool { + return len(ic.ScaVulnerabilities) > 0 || len(ic.ScaViolations) > 0 || len(ic.LicensesViolations) > 0 +} + +func (ic *ScansIssuesCollection) IacIssuesExists() bool { + return len(ic.IacVulnerabilities) > 0 || len(ic.IacViolations) > 0 +} + +func (ic *ScansIssuesCollection) SecretsIssuesExists() bool { + return len(ic.SecretsVulnerabilities) > 0 || len(ic.SecretsViolations) > 0 +} + +func (ic *ScansIssuesCollection) SastIssuesExists() bool { + return len(ic.SastVulnerabilities) > 0 || len(ic.SastViolations) > 0 +} + +func (ic *ScansIssuesCollection) GetAllIssuesCount(includeSecrets bool) int { + return ic.GetTotalVulnerabilities(includeSecrets) + ic.GetTotalViolations(includeSecrets) +} + +type ApplicableEvidences struct { + Evidence formats.Evidence + Severity string + ScannerDescription string + IssueId string + CveSummary string + ImpactedDependency string + Remediation string +} + +func toApplicableEvidences(issue formats.VulnerabilityOrViolationRow, cve formats.CveRow, evidence formats.Evidence) ApplicableEvidences { + remediation := "" + if issue.JfrogResearchInformation != nil { + remediation = issue.JfrogResearchInformation.Remediation + } + return ApplicableEvidences{ + Evidence: evidence, + Severity: issue.Severity, + ScannerDescription: cve.Applicability.ScannerDescription, + IssueId: results.GetIssueIdentifier(issue.Cves, issue.IssueId, ", "), + CveSummary: issue.Summary, + ImpactedDependency: results.GetDependencyId(issue.ImpactedDependencyName, issue.ImpactedDependencyVersion), + Remediation: remediation, + } +} + +func (ic *ScansIssuesCollection) GetApplicableEvidences() (evidences []ApplicableEvidences) { + // Collect evidences from Violations + idToEvidence := map[string]ApplicableEvidences{} + for _, securityViolation := range ic.ScaViolations { + for _, cve := range securityViolation.Cves { + if cve.Applicability != nil && cve.Applicability.Status == jasutils.Applicable.String() { + // We only want applicable issues + for _, evidence := range cve.Applicability.Evidence { + issueId := results.GetIssueIdentifier(securityViolation.Cves, securityViolation.IssueId, "-") + id := issueId + evidence.Location.ToString() + if _, exists := idToEvidence[id]; exists { + // No need to add the same issue twice + continue + } + idToEvidence[id] = toApplicableEvidences(securityViolation, cve, evidence) + } + } + } + } + // Collect evidences from Vulnerabilities + for _, vulnerability := range ic.ScaVulnerabilities { + for _, cve := range vulnerability.Cves { + if cve.Applicability != nil && cve.Applicability.Status == jasutils.Applicable.String() { + // We only want applicable issues + for _, evidence := range cve.Applicability.Evidence { + issueId := results.GetIssueIdentifier(vulnerability.Cves, vulnerability.IssueId, "-") + id := issueId + evidence.Location.ToString() + if _, exists := idToEvidence[id]; exists { + // No need to add the same issue twice + continue + } + idToEvidence[id] = toApplicableEvidences(vulnerability, cve, evidence) + } + } + } + } + + for _, evidence := range idToEvidence { + evidences = append(evidences, evidence) + } + return +} + +// Violations + +func (ic *ScansIssuesCollection) GetTotalViolations(includeSecrets bool) int { + total := ic.GetTotalScaViolations() + len(ic.IacViolations) + len(ic.SastViolations) + if includeSecrets { + total += len(ic.SecretsViolations) + } + return total +} + +func (ic *ScansIssuesCollection) GetTotalScaViolations() int { + return len(ic.ScaViolations) + len(ic.LicensesViolations) +} + +// Vulnerabilities + +func (ic *ScansIssuesCollection) GetTotalVulnerabilities(includeSecrets bool) int { + total := len(ic.ScaVulnerabilities) + len(ic.IacVulnerabilities) + len(ic.SastVulnerabilities) + if includeSecrets { + total += len(ic.SecretsVulnerabilities) + } + return total +} diff --git a/utils/issues/issuescollection_test.go b/utils/issues/issuescollection_test.go new file mode 100644 index 000000000..86cdfe2db --- /dev/null +++ b/utils/issues/issuescollection_test.go @@ -0,0 +1,491 @@ +package issues + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" + "github.com/stretchr/testify/assert" +) + +func getTestData() ScansIssuesCollection { + issuesCollection := ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{ + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "impacted-name", + ImpactedDependencyVersion: "1.0.0", + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Components: []formats.ComponentRow{ + { + Name: "vuln-pack-name1", + Version: "1.0.0", + }, + { + Name: "vuln-pack-name1", + Version: "1.2.3", + }, + { + Name: "vuln-pack-name2", + Version: "1.2.3", + }, + }, + }, + Cves: []formats.CveRow{{ + Id: "CVE-2021-1234", + Applicability: &formats.Applicability{ + Status: "Applicable", + ScannerDescription: "scanner", + Evidence: []formats.Evidence{ + {Reason: "reason", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "snippet1"}}, + {Reason: "other reason", Location: formats.Location{File: "file2", StartLine: 5, StartColumn: 6, EndLine: 7, EndColumn: 8, Snippet: "snippet2"}}, + }, + }, + }}, + JfrogResearchInformation: &formats.JfrogResearchInformation{ + Remediation: "remediation", + }, + Summary: "summary", + Applicable: "Applicable", + IssueId: "Xray-Id", + }, + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "impacted-name2", + ImpactedDependencyVersion: "1.0.0", + SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + Components: []formats.ComponentRow{ + { + Name: "vuln-pack-name3", + Version: "1.0.0", + }, + }, + }, + Cves: []formats.CveRow{{ + Id: "CVE-1111-2222", + Applicability: &formats.Applicability{Status: "Not Applicable"}, + }}, + Summary: "other summary", + Applicable: "Not Applicable", + IssueId: "Xray-Id2", + }, + }, + + ScaViolations: []formats.VulnerabilityOrViolationRow{ + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "impacted-name", + ImpactedDependencyVersion: "1.0.0", + SeverityDetails: formats.SeverityDetails{Severity: "Critical"}, + Components: []formats.ComponentRow{ + { + Name: "vuln-pack-name1", + Version: "1.0.0", + }, + }, + }, + Cves: []formats.CveRow{{ + Id: "CVE-2021-1234", + Applicability: &formats.Applicability{ + Status: "Applicable", + ScannerDescription: "scanner", + Evidence: []formats.Evidence{ + {Reason: "reason", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "snippet1"}}, + }, + }, + }}, + JfrogResearchInformation: &formats.JfrogResearchInformation{ + Remediation: "remediation", + }, + Summary: "summary", + Applicable: "Applicable", + IssueId: "Xray-Id", + ViolationContext: formats.ViolationContext{ + Watch: "watch", + Policies: []string{"policy1", "policy2"}, + }, + }, + }, + + LicensesViolations: []formats.LicenseViolationRow{{ + LicenseRow: formats.LicenseRow{ + LicenseKey: "license1", + LicenseName: "license-name1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + Components: []formats.ComponentRow{ + { + Name: "vuln-pack-name3", + Version: "1.0.0", + }, + }, + }, + }, + ViolationContext: formats.ViolationContext{ + Watch: "lic-watch", + Policies: []string{"policy3"}, + }, + }}, + + IacVulnerabilities: []formats.SourceCodeRow{{SeverityDetails: formats.SeverityDetails{Severity: "Low"}}}, + SecretsVulnerabilities: []formats.SourceCodeRow{{SeverityDetails: formats.SeverityDetails{Severity: "High"}}}, + SecretsViolations: []formats.SourceCodeRow{{ + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + ViolationContext: formats.ViolationContext{ + IssueId: "secret-violation-id", + Watch: "watch", + }, + }}, + SastVulnerabilities: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "Unknown"}, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + }, + }, + } + return issuesCollection +} + +func TestCountIssuesCollectionFindings(t *testing.T) { + testCases := []struct { + name string + includeSecrets bool + expectedFindings int + }{ + { + name: "With Secrets", + includeSecrets: true, + expectedFindings: 9, + }, + { + name: "No Secrets", + includeSecrets: false, + expectedFindings: 7, + }, + } + issuesCollection := getTestData() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + findingsAmount := issuesCollection.GetAllIssuesCount(tc.includeSecrets) + assert.Equal(t, tc.expectedFindings, findingsAmount) + }) + } +} + +func TestGetTotalVulnerabilities(t *testing.T) { + testCases := []struct { + name string + includeSecrets bool + expectedFindings int + }{ + { + name: "With Secrets", + includeSecrets: true, + expectedFindings: 6, + }, + { + name: "No Secrets", + includeSecrets: false, + expectedFindings: 5, + }, + } + issuesCollection := getTestData() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + findingsAmount := issuesCollection.GetTotalVulnerabilities(tc.includeSecrets) + assert.Equal(t, tc.expectedFindings, findingsAmount) + }) + } +} + +func TestGetTotalViolations(t *testing.T) { + testCases := []struct { + name string + includeSecrets bool + expectedFindings int + }{ + { + name: "With Secrets", + includeSecrets: true, + expectedFindings: 3, + }, + { + name: "No Secrets", + includeSecrets: false, + expectedFindings: 2, + }, + } + issuesCollection := getTestData() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + findingsAmount := issuesCollection.GetTotalViolations(tc.includeSecrets) + assert.Equal(t, tc.expectedFindings, findingsAmount) + }) + } +} + +func TestGetScanIssuesSeverityCount(t *testing.T) { + testCases := []struct { + name string + scanType utils.SubScanType + violation bool + vulnerabilities bool + expectedSeverityCount map[string]int + }{ + { + name: "Sca Vulnerabilities", + scanType: utils.ScaScan, + vulnerabilities: true, + expectedSeverityCount: map[string]int{"High": 1, "Low": 1}, + }, + { + name: "Sca Violations", + scanType: utils.ScaScan, + violation: true, + expectedSeverityCount: map[string]int{"Critical": 1, "Medium": 1}, + }, + { + name: "Sca Vulnerabilities and Violations", + scanType: utils.ScaScan, + vulnerabilities: true, + violation: true, + expectedSeverityCount: map[string]int{"High": 1, "Low": 1, "Critical": 1, "Medium": 1}, + }, + { + name: "Iac Vulnerabilities", + scanType: utils.IacScan, + vulnerabilities: true, + expectedSeverityCount: map[string]int{"Low": 1}, + }, + { + name: "Iac Violations", + scanType: utils.IacScan, + violation: true, + expectedSeverityCount: map[string]int{}, + }, + { + name: "Iac Vulnerabilities and Violations", + scanType: utils.IacScan, + vulnerabilities: true, + violation: true, + expectedSeverityCount: map[string]int{"Low": 1}, + }, + { + name: "Secrets Vulnerabilities", + scanType: utils.SecretsScan, + vulnerabilities: true, + expectedSeverityCount: map[string]int{"High": 1}, + }, + { + name: "Secrets Violations", + scanType: utils.SecretsScan, + violation: true, + expectedSeverityCount: map[string]int{"High": 1}, + }, + { + name: "Secrets Vulnerabilities and Violations", + scanType: utils.SecretsScan, + vulnerabilities: true, + violation: true, + expectedSeverityCount: map[string]int{"High": 2}, + }, + { + name: "Sast Vulnerabilities", + scanType: utils.SastScan, + vulnerabilities: true, + expectedSeverityCount: map[string]int{"High": 1, "Unknown": 1}, + }, + { + name: "Sast Violations", + scanType: utils.SastScan, + violation: true, + expectedSeverityCount: map[string]int{}, + }, + { + name: "Sast Vulnerabilities and Violations", + scanType: utils.SastScan, + vulnerabilities: true, + violation: true, + expectedSeverityCount: map[string]int{"High": 1, "Unknown": 1}, + }, + } + issuesCollection := getTestData() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + severityCount := issuesCollection.GetScanIssuesSeverityCount(tc.scanType, tc.vulnerabilities, tc.violation) + assert.Len(t, severityCount, len(tc.expectedSeverityCount)) + for severity, count := range tc.expectedSeverityCount { + actualCount, ok := severityCount[severityutils.GetSeverity(severity)] + assert.True(t, ok) + assert.Equal(t, count, actualCount) + } + }) + } +} + +func TestGetApplicableEvidences(t *testing.T) { + testCases := []struct { + name string + issues ScansIssuesCollection + expectedEvidences []ApplicableEvidences + }{ + { + name: "No Issues", + }, + { + name: "With Issues", + issues: getTestData(), + expectedEvidences: []ApplicableEvidences{ + { + Evidence: formats.Evidence{Reason: "reason", Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "snippet1"}}, + Severity: "Critical", ScannerDescription: "scanner", IssueId: "CVE-2021-1234", CveSummary: "summary", ImpactedDependency: "impacted-name:1.0.0", Remediation: "remediation", + }, + { + Evidence: formats.Evidence{Reason: "other reason", Location: formats.Location{File: "file2", StartLine: 5, StartColumn: 6, EndLine: 7, EndColumn: 8, Snippet: "snippet2"}}, + Severity: "High", ScannerDescription: "scanner", IssueId: "CVE-2021-1234", CveSummary: "summary", ImpactedDependency: "impacted-name:1.0.0", Remediation: "remediation", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.ElementsMatch(t, tc.expectedEvidences, tc.issues.GetApplicableEvidences()) + }) + } +} + +func TestIssuesExists(t *testing.T) { + testCases := []struct { + name string + issues ScansIssuesCollection + includeSecrets bool + expected bool + }{ + { + name: "No Issues", + }, + { + name: "With Issues", + issues: getTestData(), + expected: true, + }, + { + name: "With Secrets", + issues: getTestData(), + includeSecrets: true, + expected: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.issues.IssuesExists(tc.includeSecrets)) + }) + } +} + +func TestHasErrors(t *testing.T) { + testCases := []struct { + name string + status formats.ScanStatus + expected bool + }{ + { + name: "Some Not Scanned", + status: formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(0), + SastStatusCode: utils.NewIntPtr(0), + SecretsStatusCode: utils.NewIntPtr(0), + }, + }, + { + name: "All Completed", + status: formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(0), + SastStatusCode: utils.NewIntPtr(0), + SecretsStatusCode: utils.NewIntPtr(0), + IacStatusCode: utils.NewIntPtr(0), + ApplicabilityStatusCode: utils.NewIntPtr(0), + }, + }, + { + name: "With Errors", + status: formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(-1), + SastStatusCode: utils.NewIntPtr(0), + SecretsStatusCode: utils.NewIntPtr(33), + IacStatusCode: utils.NewIntPtr(0), + ApplicabilityStatusCode: utils.NewIntPtr(0), + }, + expected: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + issues := ScansIssuesCollection{ScanStatus: tc.status} + assert.Equal(t, tc.expected, issues.HasErrors()) + }) + } +} + +func TestIsScanNotCompleted(t *testing.T) { + issues := ScansIssuesCollection{ScanStatus: formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(-1), + SastStatusCode: utils.NewIntPtr(0), + SecretsStatusCode: utils.NewIntPtr(33), + }} + testCases := []struct { + name string + scan utils.SubScanType + expected bool + }{ + { + name: "Scanned and Passed", + scan: utils.SastScan, + }, + { + name: "Scanned and unknown Failed", + scan: utils.ScaScan, + expected: true, + }, + { + name: "Scanned and Failed", + scan: utils.SecretsScan, + expected: true, + }, + { + name: "Not Scanned", + scan: utils.IacScan, + expected: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, issues.IsScanNotCompleted(tc.scan)) + }) + } +} + +func TestAppendStatus(t *testing.T) { + oldStatus := formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(-1), + SastStatusCode: utils.NewIntPtr(0), + } + newStatus := formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(0), + SastStatusCode: utils.NewIntPtr(33), + ApplicabilityStatusCode: utils.NewIntPtr(0), + SecretsStatusCode: utils.NewIntPtr(51), + } + expectedStatus := formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(-1), + SastStatusCode: utils.NewIntPtr(33), + ApplicabilityStatusCode: utils.NewIntPtr(0), + SecretsStatusCode: utils.NewIntPtr(51), + } + issues := ScansIssuesCollection{ScanStatus: oldStatus} + issues.AppendStatus(newStatus) + assert.Equal(t, expectedStatus, issues.ScanStatus) +} diff --git a/utils/issuescollection.go b/utils/issuescollection.go deleted file mode 100644 index 73c338d78..000000000 --- a/utils/issuescollection.go +++ /dev/null @@ -1,76 +0,0 @@ -package utils - -import ( - "github.com/jfrog/gofrog/datastructures" - "github.com/jfrog/jfrog-cli-security/utils/formats" -) - -type IssuesCollection struct { - Vulnerabilities []formats.VulnerabilityOrViolationRow - Iacs []formats.SourceCodeRow - Secrets []formats.SourceCodeRow - Sast []formats.SourceCodeRow - Licenses []formats.LicenseRow -} - -func (ic *IssuesCollection) VulnerabilitiesExists() bool { - return len(ic.Vulnerabilities) > 0 -} - -func (ic *IssuesCollection) IacExists() bool { - return len(ic.Iacs) > 0 -} - -func (ic *IssuesCollection) LicensesExists() bool { - return len(ic.Licenses) > 0 -} - -func (ic *IssuesCollection) SecretsExists() bool { - return len(ic.Secrets) > 0 -} - -func (ic *IssuesCollection) SastExists() bool { - return len(ic.Sast) > 0 -} - -func (ic *IssuesCollection) IssuesExists() bool { - return ic.VulnerabilitiesExists() || ic.IacExists() || ic.LicensesExists() || ic.SastExists() -} - -func (ic *IssuesCollection) Append(issues *IssuesCollection) { - if issues == nil { - return - } - if len(issues.Vulnerabilities) > 0 { - ic.Vulnerabilities = append(ic.Vulnerabilities, issues.Vulnerabilities...) - } - if len(issues.Secrets) > 0 { - ic.Secrets = append(ic.Secrets, issues.Secrets...) - } - if len(issues.Sast) > 0 { - ic.Sast = append(ic.Sast, issues.Sast...) - } - if len(issues.Iacs) > 0 { - ic.Iacs = append(ic.Iacs, issues.Iacs...) - } - if len(issues.Licenses) > 0 { - ic.Licenses = append(ic.Licenses, issues.Licenses...) - } -} - -func (ic *IssuesCollection) CountIssuesCollectionFindings() int { - uniqueFindings := datastructures.MakeSet[string]() - - var totalFindings int - for _, vulnerability := range ic.Vulnerabilities { - for _, component := range vulnerability.Components { - uniqueFindings.Add(vulnerability.IssueId + "|" + component.Name + "|" + component.Version) - } - } - totalFindings += uniqueFindings.Size() - - totalFindings += len(ic.Iacs) - totalFindings += len(ic.Sast) - totalFindings += len(ic.Secrets) - return totalFindings -} diff --git a/utils/issuescollection_test.go b/utils/issuescollection_test.go deleted file mode 100644 index d6911f2fb..000000000 --- a/utils/issuescollection_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package utils - -import ( - "testing" - - "github.com/jfrog/jfrog-cli-security/utils/formats" - "github.com/stretchr/testify/assert" -) - -func TestCountIssuesCollectionFindings(t *testing.T) { - issuesCollection := IssuesCollection{ - Vulnerabilities: []formats.VulnerabilityOrViolationRow{ - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - { - Name: "vuln-pack-name1", - Version: "1.0.0", - }, - { - Name: "vuln-pack-name1", - Version: "1.2.3", - }, - { - Name: "vuln-pack-name2", - Version: "1.2.3", - }, - }, - }, - IssueId: "Xray-Id", - }, - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - { - Name: "vuln-pack-name3", - Version: "1.0.0", - }, - }, - }, - IssueId: "Xray-Id2", - }, - }, - - Iacs: []formats.SourceCodeRow{ - { - ScannerDescription: "Iac issue", - }, - }, - Secrets: []formats.SourceCodeRow{ - { - ScannerDescription: "Secret issue", - }, - }, - Sast: []formats.SourceCodeRow{ - { - ScannerDescription: "Sast issue1", - }, - { - ScannerDescription: "Sast issue2", - }, - }, - } - - findingsAmount := issuesCollection.CountIssuesCollectionFindings() - assert.Equal(t, 8, findingsAmount) -} diff --git a/utils/outputwriter/icons.go b/utils/outputwriter/icons.go index 3a3892e5b..49c341a0c 100644 --- a/utils/outputwriter/icons.go +++ b/utils/outputwriter/icons.go @@ -27,6 +27,12 @@ const ( notApplicableLowSeveritySource ImageSource = "v2/notApplicableLow.png" unknownSeveritySource ImageSource = "v2/applicableUnknownSeverity.png" notApplicableUnknownSeveritySource ImageSource = "v2/notApplicableUnknown.png" + + smallCriticalSeveritySource ImageSource = "v2/smallCritical.svg" + smallHighSeveritySource ImageSource = "v2/smallHigh.svg" + smallMediumSeveritySource ImageSource = "v2/smallMedium.svg" + smallLowSeveritySource ImageSource = "v2/smallLow.svg" + smallUnknownSeveritySource ImageSource = "v2/smallUnknown.svg" ) func getSeverityTag(iconName IconName, applicability string) string { @@ -36,40 +42,62 @@ func getSeverityTag(iconName IconName, applicability string) string { return getApplicableIconTags(iconName) } +func getSmallSeverityTag(iconName IconName) string { + return getSmallApplicableIconTags(iconName) +} + func getNotApplicableIconTags(iconName IconName) string { switch strings.ToLower(string(iconName)) { case "critical": - return GetIconTag(notApplicableCriticalSeveritySource) + "
" + return GetIconTag(notApplicableCriticalSeveritySource, "critical (not applicable)") + "
" case "high": - return GetIconTag(notApplicableHighSeveritySource) + "
" + return GetIconTag(notApplicableHighSeveritySource, "high (not applicable)") + "
" case "medium": - return GetIconTag(notApplicableMediumSeveritySource) + "
" + return GetIconTag(notApplicableMediumSeveritySource, "medium (not applicable)") + "
" case "low": - return GetIconTag(notApplicableLowSeveritySource) + "
" + return GetIconTag(notApplicableLowSeveritySource, "low (not applicable)") + "
" } - return GetIconTag(notApplicableUnknownSeveritySource) + "
" + return GetIconTag(notApplicableUnknownSeveritySource, "unknown (not applicable)") + "
" } func getApplicableIconTags(iconName IconName) string { switch strings.ToLower(string(iconName)) { case "critical": - return GetIconTag(criticalSeveritySource) + "
" + return GetIconTag(criticalSeveritySource, "critical") + "
" case "high": - return GetIconTag(highSeveritySource) + "
" + return GetIconTag(highSeveritySource, "high") + "
" case "medium": - return GetIconTag(mediumSeveritySource) + "
" + return GetIconTag(mediumSeveritySource, "medium") + "
" case "low": - return GetIconTag(lowSeveritySource) + "
" + return GetIconTag(lowSeveritySource, "low") + "
" } - return GetIconTag(unknownSeveritySource) + "
" + return GetIconTag(unknownSeveritySource, "unknown") + "
" +} + +func getSmallApplicableIconTags(iconName IconName) string { + switch strings.ToLower(string(iconName)) { + case "critical": + return GetImgTag(smallCriticalSeveritySource, "") + case "high": + return GetImgTag(smallHighSeveritySource, "") + case "medium": + return GetImgTag(smallMediumSeveritySource, "") + case "low": + return GetImgTag(smallLowSeveritySource, "") + } + return GetImgTag(smallUnknownSeveritySource, "") } func GetBanner(banner ImageSource) string { - return GetMarkdownCenterTag(MarkAsLink(GetIconTag(banner), FrogbotDocumentationUrl)) + return GetMarkdownCenterTag(MarkAsLink(GetIconTag(banner, GetSimplifiedTitle(banner)), FrogbotDocumentationUrl)) +} + +func GetIconTag(imageSource ImageSource, alt string) string { + return fmt.Sprintf("!%s", MarkAsLink(alt, fmt.Sprintf("%s%s", baseResourceUrl, imageSource))) } -func GetIconTag(imageSource ImageSource) string { - return fmt.Sprintf("!%s", MarkAsLink(GetSimplifiedTitle(imageSource), fmt.Sprintf("%s%s", baseResourceUrl, imageSource))) +func GetImgTag(imageSource ImageSource, alt string) string { + return fmt.Sprintf("\"%s\"/", baseResourceUrl, imageSource, alt) } func GetSimplifiedTitle(is ImageSource) string { diff --git a/utils/outputwriter/icons_test.go b/utils/outputwriter/icons_test.go index 720ff4130..51434832a 100644 --- a/utils/outputwriter/icons_test.go +++ b/utils/outputwriter/icons_test.go @@ -6,20 +6,28 @@ import ( "github.com/stretchr/testify/assert" ) +func TestGetSmallSeverityTag(t *testing.T) { + assert.Equal(t, "\"\"/", getSmallSeverityTag("Critical")) + assert.Equal(t, "\"\"/", getSmallSeverityTag("HiGh")) + assert.Equal(t, "\"\"/", getSmallSeverityTag("meDium")) + assert.Equal(t, "\"\"/", getSmallSeverityTag("low")) + assert.Equal(t, "\"\"/", getSmallSeverityTag("none")) +} + func TestGetSeverityTag(t *testing.T) { - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableCriticalSeverity.png)
", getSeverityTag("Critical", "Undetermined")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
", getSeverityTag("HiGh", "Undetermined")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
", getSeverityTag("meDium", "Undetermined")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
", getSeverityTag("low", "Applicable")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableUnknownSeverity.png)
", getSeverityTag("none", "Applicable")) + assert.Equal(t, "![critical](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableCriticalSeverity.png)
", getSeverityTag("Critical", "Undetermined")) + assert.Equal(t, "![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
", getSeverityTag("HiGh", "Undetermined")) + assert.Equal(t, "![medium](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
", getSeverityTag("meDium", "Undetermined")) + assert.Equal(t, "![low](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
", getSeverityTag("low", "Applicable")) + assert.Equal(t, "![unknown](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableUnknownSeverity.png)
", getSeverityTag("none", "Applicable")) } func TestGetSeverityTagNotApplicable(t *testing.T) { - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
", getSeverityTag("Critical", "Not Applicable")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableHigh.png)
", getSeverityTag("HiGh", "Not Applicable")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableMedium.png)
", getSeverityTag("meDium", "Not Applicable")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableLow.png)
", getSeverityTag("low", "Not Applicable")) - assert.Equal(t, "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableUnknown.png)
", getSeverityTag("none", "Not Applicable")) + assert.Equal(t, "![critical (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)
", getSeverityTag("Critical", "Not Applicable")) + assert.Equal(t, "![high (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableHigh.png)
", getSeverityTag("HiGh", "Not Applicable")) + assert.Equal(t, "![medium (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableMedium.png)
", getSeverityTag("meDium", "Not Applicable")) + assert.Equal(t, "![low (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableLow.png)
", getSeverityTag("low", "Not Applicable")) + assert.Equal(t, "![unknown (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableUnknown.png)
", getSeverityTag("none", "Not Applicable")) } func TestGetVulnerabilitiesBanners(t *testing.T) { diff --git a/utils/outputwriter/markdowntable.go b/utils/outputwriter/markdowntable.go index 9ca52fb8b..c661def2c 100644 --- a/utils/outputwriter/markdowntable.go +++ b/utils/outputwriter/markdowntable.go @@ -6,11 +6,16 @@ import ( ) const ( - tableRowFirstColumnSeparator = "| :---------------------: |" - tableRowColumnSeparator = " :-----------------------------------: |" - cellFirstCellPlaceholder = "| %s |" - cellCellPlaceholder = " %s |" - cellDefaultValue = "-" + cellDefaultValue = "-" + + firstCellPlaceholder = "| %s |" + cellPlaceholder = " %s |" + + centeredFirstColumnSeparator = "| :---------------------: |" + centeredColumnSeparator = " :-----------------------------------: |" + + defaultFirstColumnSeparator = "| --------------------- |" + defaultColumnSeparator = " ----------------------------------- |" // (Default value for columns) If more than one value exists in a cell, the values will be separated by the delimiter. SeparatorDelimited MarkdownColumnType = "single" @@ -32,8 +37,12 @@ type MarkdownColumnType string type MarkdownColumn struct { Name string + Centered bool + OmitEmpty bool ColumnType MarkdownColumnType DefaultValue string + // Internal flag to determine if the column should be hidden + shouldHideColumn bool } // CellData represents the data of a cell in the markdown table. Each cell can contain multiple values. @@ -51,17 +60,38 @@ func NewCellData(values ...string) CellData { func NewMarkdownTable(columns ...string) *MarkdownTableBuilder { columnsInfo := []*MarkdownColumn{} for _, column := range columns { - columnsInfo = append(columnsInfo, &MarkdownColumn{Name: column, ColumnType: SeparatorDelimited, DefaultValue: cellDefaultValue}) + columnsInfo = append(columnsInfo, NewMarkdownTableSingleValueColumn(column, cellDefaultValue, true)) + } + return NewMarkdownTableWithColumns(columnsInfo...) +} + +// Create a markdown table builder with the provided number of columns. +func NewNoHeaderMarkdownTable(nColumns int, firstColumnCentered bool) *MarkdownTableBuilder { + columnsInfo := []*MarkdownColumn{} + for i := 0; i < nColumns; i++ { + columnsInfo = append(columnsInfo, NewMarkdownTableSingleValueColumn("", cellDefaultValue, i != 0 || firstColumnCentered)) } + return NewMarkdownTableWithColumns(columnsInfo...) +} + +func NewMarkdownTableWithColumns(columnsInfo ...*MarkdownColumn) *MarkdownTableBuilder { return &MarkdownTableBuilder{columns: columnsInfo, delimiter: simpleSeparator} } +func NewMarkdownTableSingleValueColumn(name, defaultValue string, centered bool) *MarkdownColumn { + return &MarkdownColumn{Name: name, ColumnType: SeparatorDelimited, DefaultValue: defaultValue, Centered: centered} +} + // Set the delimiter that will be used to separate multiple values in a cell. func (t *MarkdownTableBuilder) SetDelimiter(delimiter string) *MarkdownTableBuilder { t.delimiter = delimiter return t } +func (t *MarkdownTableBuilder) HasContent() bool { + return len(t.rows) > 0 +} + // Get the column information output controller by the provided name. func (t *MarkdownTableBuilder) GetColumnInfo(name string) *MarkdownColumn { for _, column := range t.columns { @@ -111,22 +141,53 @@ func (t *MarkdownTableBuilder) Build() string { return "" } var tableBuilder strings.Builder + // Calculate Hidden columns + for c := range t.columns { + // Reset shouldHideColumn flag to the defined value in the column + // If the column OmitEmpty flag is set, the column will be hidden if all the values in the column are empty + t.columns[c].shouldHideColumn = t.columns[c].OmitEmpty + } + for _, row := range t.rows { + for c, cell := range row { + // In table, empty cell = cell with no values = cell with one empty value + // So we want don't want to hide the column if at least one cell has a value in it + t.columns[c].shouldHideColumn = t.columns[c].shouldHideColumn && (len(cell) == 0 || (len(cell) == 1 && cell[0] == "")) + } + } // Header - for c, column := range t.columns { - if c == 0 { - tableBuilder.WriteString(fmt.Sprintf(cellFirstCellPlaceholder, column.Name)) + isFirstCol := true + for _, column := range t.columns { + if column.shouldHideColumn { + continue + } + if isFirstCol { + tableBuilder.WriteString(fmt.Sprintf(firstCellPlaceholder, column.Name)) } else { - tableBuilder.WriteString(fmt.Sprintf(cellCellPlaceholder, column.Name)) + tableBuilder.WriteString(fmt.Sprintf(cellPlaceholder, column.Name)) } + isFirstCol = false } tableBuilder.WriteString("\n") // Separator - for c := range t.columns { - if c == 0 { - tableBuilder.WriteString(tableRowFirstColumnSeparator) + isFirstCol = true + for _, column := range t.columns { + if column.shouldHideColumn { + continue + } + if isFirstCol { + columnSeparator := defaultFirstColumnSeparator + if column.Centered { + columnSeparator = centeredFirstColumnSeparator + } + tableBuilder.WriteString(columnSeparator) } else { - tableBuilder.WriteString(tableRowColumnSeparator) + columnSeparator := defaultColumnSeparator + if column.Centered { + columnSeparator = centeredColumnSeparator + } + tableBuilder.WriteString(columnSeparator) } + isFirstCol = false } // Content for _, row := range t.rows { @@ -161,6 +222,9 @@ func (t *MarkdownTableBuilder) getMultiValueRowsContent(row []CellData, multiVal } content := []string{} for column, cell := range row { + if t.columns[column].shouldHideColumn { + continue + } if column == multiValueColumnIndex { // Multi values column separated by different rows, add the specific value for this row content = append(content, value) @@ -183,6 +247,9 @@ func (t *MarkdownTableBuilder) getMultiValueRowsContent(row []CellData, multiVal func (t *MarkdownTableBuilder) getSeparatorDelimitedRowContent(row []CellData) string { content := []string{} for column, columnInfo := range t.columns { + if columnInfo.shouldHideColumn { + continue + } content = append(content, t.getCellContent(row[column], columnInfo.DefaultValue)) } return buildRowContent(content...) diff --git a/utils/outputwriter/markdowntable_test.go b/utils/outputwriter/markdowntable_test.go index 556b6e1d9..fb8ff113f 100644 --- a/utils/outputwriter/markdowntable_test.go +++ b/utils/outputwriter/markdowntable_test.go @@ -97,7 +97,7 @@ func TestMarkdownTableBuild(t *testing.T) { name: "No rows", columns: []string{"col1"}, rows: [][]string{}, - expectedOutput: "| col1 |\n" + tableRowFirstColumnSeparator, + expectedOutput: "| col1 |\n" + centeredFirstColumnSeparator, }, { name: "Same number of columns", @@ -107,7 +107,7 @@ func TestMarkdownTableBuild(t *testing.T) { {"row2col1", "row2col2"}, {"row3col1", "row3col2"}, }, - expectedOutput: "| col1 | col2 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + ` + expectedOutput: "| col1 | col2 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + ` | row1col1 | row1col2 | | row2col1 | row2col2 | | row3col1 | row3col2 |`, @@ -123,7 +123,7 @@ func TestMarkdownTableBuild(t *testing.T) { {"row4col1"}, {"row5col1", "row5col2", "row5col3"}, }, - expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` + expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + ` | row1col1 | row1col2 | - | | row2col1 | row2col2 | - | | row3col1 | - | row3col3 | @@ -143,6 +143,60 @@ func TestMarkdownTableBuild(t *testing.T) { } } +func TestHideEmptyColumnsInTable(t *testing.T) { + columns := []*MarkdownColumn{ + {Name: "col1", OmitEmpty: true}, + {Name: "col2", OmitEmpty: true, Centered: true}, + {Name: "col3", OmitEmpty: false, DefaultValue: "-"}, + {Name: "col4", OmitEmpty: true}, + } + testCases := []struct { + name string + rows [][]string + expectedOutput string + }{ + { + name: "Defined as hidden but not empty", + rows: [][]string{ + {"row1col1", "row1col2", "", "row1col4"}, + {"row2col1", "row2col2", "", "row2col4"}, + }, + expectedOutput: "| col1 | col2 | col3 | col4 |\n" + defaultFirstColumnSeparator + centeredColumnSeparator + defaultColumnSeparator + defaultColumnSeparator + ` +| row1col1 | row1col2 | - | row1col4 | +| row2col1 | row2col2 | - | row2col4 |`, + }, + { + name: "Defined as hidden and some empty", + rows: [][]string{ + {"row1col1", "", "row1col3", ""}, + {"row2col1", "", "", ""}, + }, + expectedOutput: "| col1 | col3 |\n" + defaultFirstColumnSeparator + defaultColumnSeparator + ` +| row1col1 | row1col3 | +| row2col1 | - |`, + }, + { + name: "Defined as hidden and all empty", + rows: [][]string{ + {"", "", "row1col3", ""}, + {"", "", "", ""}, + }, + expectedOutput: "| col3 |\n" + defaultFirstColumnSeparator + ` +| row1col3 | +| - |`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + table := NewMarkdownTableWithColumns(columns...) + for _, row := range tc.rows { + table.AddRow(row...) + } + assert.Equal(t, tc.expectedOutput, table.Build()) + }) + } +} + func TestMultipleValuesInColumnRow(t *testing.T) { testCases := []struct { name string @@ -156,7 +210,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) { rows: [][]CellData{ {{""}, {"row1col2"}, {"row1col3"}}, }, - expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` + expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + ` | - | row1col2 | row1col3 |`, }, { @@ -166,7 +220,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) { {{"row1col1"}, {"row1col2"}, {"row1col3"}}, {{"row2col1"}, {"row2col2"}, {"row2col3"}}, }, - expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` + expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + ` | row1col1 | row1col2 | row1col3 | | row2col1 | row2col2 | row2col3 |`, }, @@ -178,7 +232,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) { {{"row2col1"}, {"row2col2"}, {"row2col3val1", "row2col3val2"}}, {{"row3col1"}, {"row3col2val1", "row3col2val2", "row3col2val3"}, {"row3col3"}}, }, - expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` + expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + ` | row1col1 | - | row1col3 | | row2col1 | row2col2 | row2col3val1, row2col3val2 | | row3col1 | row3col2val1, row3col2val2, row3col2val3 | row3col3 |`, @@ -191,7 +245,7 @@ func TestMultipleValuesInColumnRow(t *testing.T) { {{"row2col1"}, {"row2col2"}, {"row2col3val1", "row2col3val2"}}, {{"row3col1"}, {"row3col2val1", "row3col2val2", "row3col2val3"}, {"row3col3"}}, }, - expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` + expectedOutput: "| col1 | col2 | col3 |\n" + centeredFirstColumnSeparator + centeredColumnSeparator + centeredColumnSeparator + ` | row1col1 | - | row1col3 | | row2col1 | row2col2 | row2col3val1, row2col3val2 | | row3col1 | row3col2val1 | row3col3 | diff --git a/utils/outputwriter/outputcontent.go b/utils/outputwriter/outputcontent.go index 788d5637a..f68e8b69a 100644 --- a/utils/outputwriter/outputcontent.go +++ b/utils/outputwriter/outputcontent.go @@ -2,28 +2,41 @@ package outputwriter import ( "fmt" + "sort" "strings" + "github.com/jfrog/frogbot/v2/utils/issues" "github.com/jfrog/froggit-go/vcsutils" + "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" + "golang.org/x/exp/maps" ) const ( FrogbotTitlePrefix = "[🐸 Frogbot]" FrogbotRepoUrl = "https://github.com/jfrog/frogbot" FrogbotDocumentationUrl = "https://docs.jfrog-applications.jfrog.io/jfrog-applications/frogbot" + JfrogSupportUrl = "https://jfrog.com/support/" ReviewCommentId = "FrogbotReviewComment" - vulnerableDependenciesTitle = "📦 Vulnerable Dependencies" - vulnerableDependenciesResearchDetailsSubTitle = "🔬 Research Details" + scanSummaryTitle = "📗 Scan Summary" + issuesDetailsSubTitle = "🔖 Details" + jfrogResearchDetailsSubTitle = "🔬 JFrog Research Details" + + policyViolationTitle = "🚥 Policy Violations" + securityViolationTitle = "🚨 Security Violations" + licenseViolationTitle = "⚖️ License Violations" + + vulnerableDependenciesTitle = "📦 Vulnerable Dependencies" - contextualAnalysisTitle = "📦🔍 Contextual Analysis CVE Vulnerability" //#nosec G101 -- not a secret - secretsTitle = "🗝️ Secret Detected" - iacTitle = "🛠️ Infrastructure as Code Vulnerability" - sastTitle = "🎯 Static Application Security Testing (SAST) Vulnerability" + secretsTitle = "🤫 Secret" + contextualAnalysisTitle = "📦🔍 Contextual Analysis CVE" + iacTitle = "🛠️ Infrastructure as Code" + sastTitle = "🎯 Static Application Security Testing (SAST)" ) var ( @@ -31,6 +44,44 @@ var ( jasFeaturesMsgWhenNotEnabled = MarkAsBold("Frogbot") + " also supports " + MarkAsBold("Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning") + ". This features are included as part of the " + MarkAsLink("JFrog Advanced Security", "https://jfrog.com/advanced-security") + " package, which isn't enabled on your system." ) +// For review comment Frogbot creates on Scan PR +func GenerateReviewCommentContent(content string, writer OutputWriter) string { + var contentBuilder strings.Builder + contentBuilder.WriteString(MarkdownComment(ReviewCommentId)) + customCommentTitle := writer.PullRequestCommentTitle() + if customCommentTitle != "" { + WriteContent(&contentBuilder, writer.MarkAsTitle(MarkAsBold(customCommentTitle), 2)) + } + WriteContent(&contentBuilder, content, footer(writer)) + return contentBuilder.String() +} + +// When can't create review comment, create a fallback comment by adding the location description to the content as a prefix +func GetFallbackReviewCommentContent(content string, location formats.Location) string { + var contentBuilder strings.Builder + contentBuilder.WriteString(MarkdownComment(ReviewCommentId)) + WriteContent(&contentBuilder, getFallbackCommentLocationDescription(location), content) + return contentBuilder.String() +} + +func IsFrogbotComment(content string) bool { + return strings.Contains(content, ReviewCommentId) +} + +func getFallbackCommentLocationDescription(location formats.Location) string { + return fmt.Sprintf("%s\nat %s (line %d)", MarkAsCodeSnippet(location.Snippet), MarkAsQuote(location.File), location.StartLine) +} + +// Summary comment, including banner, footer wrapping the content with a decorator +func GetMainCommentContent(contentForComments []string, issuesExists, isComment bool, writer OutputWriter) (comments []string) { + return ConvertContentToComments(contentForComments, writer, func(commentCount int, content string) string { + if commentCount == 0 { + content = GetPRSummaryMainCommentDecorator(issuesExists, isComment, writer)(commentCount, content) + } + return GetFrogbotCommentBaseDecorator(writer)(commentCount, content) + }) +} + // Adding markdown prefix to identify Frogbot comment and a footer with the link to the documentation func GetFrogbotCommentBaseDecorator(writer OutputWriter) CommentDecorator { return func(_ int, content string) string { @@ -58,15 +109,6 @@ func GetPRSummaryMainCommentDecorator(issuesExists, isComment bool, writer Outpu } } -func GetPRSummaryContent(contentForComments []string, issuesExists, isComment bool, writer OutputWriter) (comments []string) { - return ConvertContentToComments(contentForComments, writer, func(commentCount int, content string) string { - if commentCount == 0 { - content = GetPRSummaryMainCommentDecorator(issuesExists, isComment, writer)(commentCount, content) - } - return GetFrogbotCommentBaseDecorator(writer)(commentCount, content) - }) -} - func getPRSummaryBanner(issuesExists, isComment bool, provider vcsutils.VcsProvider) ImageSource { if !isComment { return fixCVETitleSrc(provider) @@ -77,15 +119,6 @@ func getPRSummaryBanner(issuesExists, isComment bool, provider vcsutils.VcsProvi return PRSummaryCommentTitleSrc(provider) } -// TODO: remove this at the next release, it's not used anymore and replaced by adding ReviewCommentId comment to the content -func IsFrogbotSummaryComment(writer OutputWriter, content string) bool { - client := writer.VcsProvider() - return strings.Contains(content, GetBanner(NoIssuesTitleSrc(client))) || - strings.Contains(content, GetSimplifiedTitle(NoIssuesTitleSrc(client))) || - strings.Contains(content, GetBanner(PRSummaryCommentTitleSrc(client))) || - strings.Contains(content, GetSimplifiedTitle(PRSummaryCommentTitleSrc(client))) -} - func NoIssuesTitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource { if vcsProvider == vcsutils.GitLab { return NoVulnerabilityMrBannerSource @@ -111,76 +144,459 @@ func untitledForJasMsg(writer OutputWriter) string { if writer.AvoidExtraMessages() || writer.IsEntitledForJas() { return "" } - return writer.MarkAsDetails("Note:", 0, fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(jasFeaturesMsgWhenNotEnabled))) + return writer.MarkAsDetails("Note", 0, fmt.Sprintf("\n%s\n%s", SectionDivider(), writer.MarkInCenter(jasFeaturesMsgWhenNotEnabled))) } func footer(writer OutputWriter) string { return fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(CommentGeneratedByFrogbot)) } -func VulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) (content []string) { - if len(vulnerabilities) == 0 { - return []string{} +// Summary content + +func ScanSummaryContent(issues issues.ScansIssuesCollection, context results.ResultContext, includeSecrets bool, writer OutputWriter) string { + if !issues.IssuesExists(includeSecrets) && !issues.HasErrors() { + return "" + } + var contentBuilder strings.Builder + totalIssues := 0 + if context.HasViolationContext() { + totalIssues += issues.GetTotalViolations(includeSecrets) + } + if context.IncludeVulnerabilities { + totalIssues += issues.GetTotalVulnerabilities(includeSecrets) + } + // Title + WriteContent(&contentBuilder, writer.MarkAsTitle(scanSummaryTitle, 2)) + if issues.HasErrors() { + WriteContent(&contentBuilder, MarkAsBullet(fmt.Sprintf("Frogbot attempted to scan for %s but encountered an error.", getResultsContextString(context)))) + return contentBuilder.String() + } else { + WriteContent(&contentBuilder, MarkAsBullet(fmt.Sprintf("Frogbot scanned for %s and found %d issues", getResultsContextString(context), totalIssues))) + } + WriteNewLine(&contentBuilder) + // Create table, a row for each sub scans summary + secretsDetails := "" + if includeSecrets { + secretsDetails = getScanSecurityIssuesDetails(issues, context, utils.SecretsScan, writer) + } + table := NewMarkdownTableWithColumns( + NewMarkdownTableSingleValueColumn("Scan Category", "⚠️", false), + NewMarkdownTableSingleValueColumn("Status", "⚠️", true), + NewMarkdownTableSingleValueColumn("Security Issues", "-", false), + ) + table.AddRow(MarkAsBold("Software Composition Analysis"), getSubScanResultStatus(issues.GetScanStatus(utils.ScaScan)), getScanSecurityIssuesDetails(issues, context, utils.ScaScan, writer)) + table.AddRow(MarkAsBold("Contextual Analysis"), getSubScanResultStatus(issues.GetScanStatus(utils.ContextualAnalysisScan)), "") + table.AddRow(MarkAsBold("Static Application Security Testing (SAST)"), getSubScanResultStatus(issues.GetScanStatus(utils.SastScan)), getScanSecurityIssuesDetails(issues, context, utils.SastScan, writer)) + table.AddRow(MarkAsBold("Secrets"), getSubScanResultStatus(issues.GetScanStatus(utils.SecretsScan)), secretsDetails) + table.AddRow(MarkAsBold("Infrastructure as Code (IaC)"), getSubScanResultStatus(issues.GetScanStatus(utils.IacScan)), getScanSecurityIssuesDetails(issues, context, utils.IacScan, writer)) + WriteContent(&contentBuilder, table.Build()) + return contentBuilder.String() +} + +func getResultsContextString(context results.ResultContext) string { + out := "" + if context.HasViolationContext() { + out += "violations" + } + if context.IncludeVulnerabilities { + if out != "" { + out += " and " + } + out += "vulnerabilities" + } + return out +} + +func getSubScanResultStatus(scanStatusCode *int) string { + if scanStatusCode == nil { + return "ℹ️ Not Scanned" + } + if *scanStatusCode == 0 { + return "✅ Done" + } + return "❌ Failed" +} + +func getScanSecurityIssuesDetails(issues issues.ScansIssuesCollection, context results.ResultContext, scanType utils.SubScanType, writer OutputWriter) string { + if issues.HasErrors() || issues.IsScanNotCompleted(scanType) { + // Failed/Not scanned, no need to show the details + return "" + } + var severityCountMap map[severityutils.Severity]int + countViolations := context.HasViolationContext() + countVulnerabilities := context.IncludeVulnerabilities + switch scanType { + case utils.ScaScan: + severityCountMap = issues.GetScanIssuesSeverityCount(utils.ScaScan, countVulnerabilities, countViolations) + case utils.SastScan: + severityCountMap = issues.GetScanIssuesSeverityCount(utils.SastScan, countVulnerabilities, countViolations) + case utils.SecretsScan: + severityCountMap = issues.GetScanIssuesSeverityCount(utils.SecretsScan, countVulnerabilities, countViolations) + case utils.IacScan: + severityCountMap = issues.GetScanIssuesSeverityCount(utils.IacScan, countVulnerabilities, countViolations) + } + totalIssues := getTotalIssues(severityCountMap) + if totalIssues == 0 { + // No Issues + return "Not Found" + } + var contentBuilder strings.Builder + WriteContent(&contentBuilder, writer.MarkAsDetails(fmt.Sprintf("%d Issues Found", totalIssues), 0, toSeverityDetails(severityCountMap, writer))) + return contentBuilder.String() +} + +func getTotalIssues(severities map[severityutils.Severity]int) (total int) { + for _, count := range severities { + total += count } - content = append(content, writer.MarkAsTitle(vulnerableDependenciesTitle, 2)) - content = append(content, vulnerabilitiesSummaryContent(vulnerabilities, writer)) - content = append(content, vulnerabilityDetailsContent(vulnerabilities, writer)...) return } -func vulnerabilitiesSummaryContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { +func toSeverityDetails(severities map[severityutils.Severity]int, writer OutputWriter) string { var contentBuilder strings.Builder - WriteContent(&contentBuilder, - writer.MarkAsTitle("✍️ Summary", 3), - writer.MarkInCenter(getVulnerabilitiesSummaryTable(vulnerabilities, writer)), - ) + sortedSeverities := []severityutils.Severity{severityutils.Critical, severityutils.High, severityutils.Medium, severityutils.Low, severityutils.Unknown} + for _, severity := range sortedSeverities { + if count, ok := severities[severity]; ok && count > 0 { + if contentBuilder.Len() > 0 { + contentBuilder.WriteString(writer.Separator()) + } + contentBuilder.WriteString(fmt.Sprintf("%s %d %s", writer.SeverityIcon(severity), count, severity.String())) + } + } + return contentBuilder.String() +} + +// SCA (Policy) Violations + +// Summary content for the security violations that we can't yet have location on (SCA, License) +func PolicyViolationsContent(issues issues.ScansIssuesCollection, writer OutputWriter) (policyViolationContent []string) { + if issues.GetTotalScaViolations() == 0 { + return []string{} + } + policyViolationContent = append(policyViolationContent, getSecurityViolationsContent(issues, writer)...) + policyViolationContent = append(policyViolationContent, getLicenseViolationsContent(issues, writer)...) + return ConvertContentToComments(policyViolationContent, writer, getDecoratorWithPolicyViolationTitle(writer)) +} + +func getDecoratorWithPolicyViolationTitle(writer OutputWriter) CommentDecorator { + return func(commentCount int, content string) string { + contentBuilder := strings.Builder{} + // Decorate each part of the split content with a title as prefix and return the content + WriteContent(&contentBuilder, writer.MarkAsTitle(policyViolationTitle, 2)) + WriteContent(&contentBuilder, content) + return contentBuilder.String() + } +} + +// Security Violations + +func getSecurityViolationsContent(issues issues.ScansIssuesCollection, writer OutputWriter) (content []string) { + if len(issues.ScaViolations) == 0 { + return []string{} + } + content = append(content, getSecurityViolationsSummaryTable(issues.ScaViolations, writer)) + content = append(content, getScaSecurityIssueDetailsContent(issues.ScaViolations, true, writer)...) + return ConvertContentToComments(content, writer, getDecoratorWithSecurityViolationTitle(writer)) +} + +func getDecoratorWithSecurityViolationTitle(writer OutputWriter) CommentDecorator { + return func(commentCount int, content string) string { + contentBuilder := strings.Builder{} + // Decorate each part of the split content with a title as prefix and return the content + WriteContent(&contentBuilder, writer.MarkAsTitle(securityViolationTitle, 3)) + WriteContent(&contentBuilder, content) + return contentBuilder.String() + } +} + +func getSecurityViolationsSummaryTable(violations []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { + // Construct table + columns := []string{"Severity", "ID"} + if writer.IsShowingCaColumn() { + columns = append(columns, "Contextual Analysis") + } + table := NewMarkdownTable(append(columns, "Direct Dependencies", "Impacted Dependency", "Watch Name")...).SetDelimiter(writer.Separator()) + if _, ok := writer.(*SimplifiedOutput); ok { + // The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row. + // It means that the first row will show the full details, and the following rows will show only the direct dependency. + // It makes it easier to read the table and less crowded with text in a single cell that could be potentially large. + table.GetColumnInfo("Direct Dependencies").ColumnType = MultiRowColumn + } + // Construct rows + for _, violation := range violations { + row := []CellData{{writer.FormattedSeverity(violation.Severity, violation.Applicable)}, getCveIdsCellData(violation.Cves, violation.IssueId)} + if writer.IsShowingCaColumn() { + row = append(row, NewCellData(violation.Applicable)) + } + row = append(row, + getDirectDependenciesCellData(violation.Components), + NewCellData(results.GetDependencyId(violation.ImpactedDependencyName, violation.ImpactedDependencyVersion)), + NewCellData(violation.Watch), + ) + table.AddRowWithCellData(row...) + } + return writer.MarkInCenter(table.Build()) +} + +// License violations + +func getLicenseViolationsContent(issues issues.ScansIssuesCollection, writer OutputWriter) (content []string) { + if len(issues.LicensesViolations) == 0 { + return []string{} + } + content = append(content, getLicenseViolationsSummaryTable(issues.LicensesViolations, writer)) + content = append(content, getLicenseViolationsDetailsContent(issues.LicensesViolations, writer)...) + return ConvertContentToComments(content, writer, getDecoratorWithLicenseViolationTitle(writer)) +} + +func getDecoratorWithLicenseViolationTitle(writer OutputWriter) CommentDecorator { + return func(commentCount int, content string) string { + contentBuilder := strings.Builder{} + // Decorate each part of the split content with a title as prefix and return the content + WriteContent(&contentBuilder, writer.MarkAsTitle(licenseViolationTitle, 3)) + WriteContent(&contentBuilder, content) + return contentBuilder.String() + } +} + +func getLicenseViolationsSummaryTable(licenses []formats.LicenseViolationRow, writer OutputWriter) string { + table := NewMarkdownTable("Severity", "License", "Direct Dependencies", "Impacted Dependency", "Watch Name").SetDelimiter(writer.Separator()) + if _, ok := writer.(*SimplifiedOutput); ok { + // The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row. + // It means that the first row will show the full details, and the following rows will show only the direct dependency. + // It makes it easier to read the table and less crowded with text in a single cell that could be potentially large. + table.GetColumnInfo("Direct Dependencies").ColumnType = MultiRowColumn + } + for _, license := range licenses { + table.AddRowWithCellData( + NewCellData(writer.FormattedSeverity(license.Severity, "Applicable")), + NewCellData(license.LicenseKey), + getDirectDependenciesCellData(license.Components), + NewCellData(results.GetDependencyId(license.ImpactedDependencyName, license.ImpactedDependencyVersion)), + NewCellData(license.Watch), + ) + } + return writer.MarkInCenter(table.Build()) +} + +func getLicenseViolationsDetailsContent(licenseViolations []formats.LicenseViolationRow, writer OutputWriter) (content []string) { + if len(licenseViolations) == 0 { + return + } + for _, violation := range licenseViolations { + if len(licenseViolations) == 1 { + // No need for
tag if there is only one violation, just show the details + content = append(content, getScaLicenseViolationDetails(violation, writer)) + } else { + // Add wrap the content of each violation in a
tag + content = append(content, "\n"+writer.MarkAsDetails( + getComponentIssueIdentifier(violation.LicenseKey, violation.ImpactedDependencyName, violation.ImpactedDependencyVersion, violation.Watch), + 4, + getScaLicenseViolationDetails(violation, writer), + )) + } + } + // Split content if it exceeds the size limit and decorate each comment with title as prefix + return ConvertContentToComments(content, writer, func(commentCount int, detailsContent string) string { + contentBuilder := strings.Builder{} + WriteContent(&contentBuilder, writer.MarkAsTitle(issuesDetailsSubTitle, 3)) + WriteContent(&contentBuilder, detailsContent) + return contentBuilder.String() + }) +} + +func getScaLicenseViolationDetails(violation formats.LicenseViolationRow, writer OutputWriter) (content string) { + var contentBuilder strings.Builder + // Title + WriteNewLine(&contentBuilder) + WriteContent(&contentBuilder, writer.MarkAsTitle("Violation Details", 3)) + // Details Table + directComponent := []string{} + for _, component := range violation.ImpactedDependencyDetails.Components { + directComponent = append(directComponent, results.GetDependencyId(component.Name, component.Version)) + } + noHeaderTable := NewNoHeaderMarkdownTable(2, false) + + noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Policies:")), NewCellData(violation.Policies...)) + noHeaderTable.AddRow(MarkAsBold("Watch Name:"), violation.Watch) + noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Direct Dependencies:")), NewCellData(directComponent...)) + noHeaderTable.AddRow(MarkAsBold("Impacted Dependency:"), results.GetDependencyId(violation.ImpactedDependencyName, violation.ImpactedDependencyVersion)) + noHeaderTable.AddRow(MarkAsBold("Full Name:"), violation.LicenseName) + + WriteContent(&contentBuilder, noHeaderTable.Build(), "\n") return contentBuilder.String() } +// Sca Vulnerabilities + +func GetVulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) (content []string) { + if len(vulnerabilities) == 0 { + return []string{} + } + content = append(content, writer.MarkInCenter(getVulnerabilitiesSummaryTable(vulnerabilities, writer))) + content = append(content, getScaSecurityIssueDetailsContent(vulnerabilities, false, writer)...) + return ConvertContentToComments(content, writer, getDecoratorWithScaVulnerabilitiesTitle(writer)) +} + +func getDecoratorWithScaVulnerabilitiesTitle(writer OutputWriter) CommentDecorator { + return func(commentCount int, content string) string { + contentBuilder := strings.Builder{} + // Decorate each part of the split content with a title as prefix and return the content + WriteContent(&contentBuilder, writer.MarkAsTitle(vulnerableDependenciesTitle, 3)) + WriteContent(&contentBuilder, content) + return contentBuilder.String() + } +} + func getVulnerabilitiesSummaryTable(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { // Construct table - columns := []string{"SEVERITY"} + columns := []string{"Severity", "ID"} if writer.IsShowingCaColumn() { - columns = append(columns, "CONTEXTUAL ANALYSIS") + columns = append(columns, "Contextual Analysis") } - columns = append(columns, "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY", "FIXED VERSIONS", "CVES") + columns = append(columns, "Direct Dependencies", "Impacted Dependency", "Fixed Versions") table := NewMarkdownTable(columns...).SetDelimiter(writer.Separator()) if _, ok := writer.(*SimplifiedOutput); ok { // The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row. // It means that the first row will show the full details, and the following rows will show only the direct dependency. // It makes it easier to read the table and less crowded with text in a single cell that could be potentially large. - table.GetColumnInfo("DIRECT DEPENDENCIES").ColumnType = MultiRowColumn + table.GetColumnInfo("Direct Dependencies").ColumnType = MultiRowColumn } // Construct rows for _, vulnerability := range vulnerabilities { - row := []CellData{{writer.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)}} + row := []CellData{{writer.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)}, getCveIdsCellData(vulnerability.Cves, vulnerability.IssueId)} if writer.IsShowingCaColumn() { row = append(row, NewCellData(vulnerability.Applicable)) } row = append(row, - getDirectDependenciesCellData("%s:%s", vulnerability.Components), + getDirectDependenciesCellData(vulnerability.Components), NewCellData(fmt.Sprintf("%s %s", vulnerability.ImpactedDependencyName, vulnerability.ImpactedDependencyVersion)), NewCellData(vulnerability.FixedVersions...), - getCveIdsCellData(vulnerability.Cves), ) table.AddRowWithCellData(row...) } return table.Build() } -func getDirectDependenciesCellData(format string, components []formats.ComponentRow) (dependencies CellData) { +// Applicable CVE Evidence + +func ApplicableCveReviewContent(issue issues.ApplicableEvidences, writer OutputWriter) string { + var contentBuilder strings.Builder + WriteContent(&contentBuilder, + writer.MarkAsTitle(contextualAnalysisTitle, 2), + writer.MarkInCenter(GetApplicabilityDescriptionTable(issue.Severity, issue.IssueId, issue.ImpactedDependency, issue.Evidence.Reason, writer)), + writer.MarkAsDetails("Description", 3, "\n"+issue.ScannerDescription+"\n"), + writer.MarkAsDetails("CVE details", 3, "\n"+issue.CveSummary+"\n"), + ) + if len(issue.Remediation) > 0 { + WriteContent(&contentBuilder, writer.MarkAsDetails("Remediation", 3, "\n\n"+issue.Remediation+"\n\n")) + } + return contentBuilder.String() +} + +func GetApplicabilityDescriptionTable(severity, issueId, impactedDependency, finding string, writer OutputWriter) string { + table := NewMarkdownTable("Severity", "ID", "Impacted Dependency", "Finding").AddRow(writer.FormattedSeverity(severity, "Applicable"), issueId, impactedDependency, finding) + return table.Build() +} + +// JAS + +func IacReviewContent(violation bool, writer OutputWriter, issues ...formats.SourceCodeRow) string { + var contentBuilder strings.Builder + WriteContent(&contentBuilder, + writer.MarkAsTitle(fmt.Sprintf("%s %s", iacTitle, getIssueType(violation)), 2), + writer.MarkInCenter(getJasIssueDescriptionTable(writer, issues...)), + getJasFullDescription(violation, writer, getBaseJasDetailsTable, issues...), + ) + return contentBuilder.String() +} + +func SastReviewContent(violation bool, writer OutputWriter, issues ...formats.SourceCodeRow) string { + var contentBuilder strings.Builder + WriteContent(&contentBuilder, + writer.MarkAsTitle(fmt.Sprintf("%s %s", sastTitle, getIssueType(violation)), 2), + writer.MarkInCenter(getJasIssueDescriptionTable(writer, issues...)), + getJasFullDescription(violation, writer, getSastRuleFullDescriptionTable, issues...), + ) + return contentBuilder.String() +} + +func getSastRuleFullDescriptionTable(info formats.ScannerInfo, writer OutputWriter) *MarkdownTableBuilder { + table := getBaseJasDetailsTable(info, writer) + table.AddRow(MarkAsBold("Rule ID:"), info.RuleId) + return table +} + +func SecretReviewContent(violation bool, writer OutputWriter, issues ...formats.SourceCodeRow) string { + var contentBuilder strings.Builder + WriteContent(&contentBuilder, + writer.MarkAsTitle(fmt.Sprintf("%s %s", secretsTitle, getIssueType(violation)), 2), + writer.MarkInCenter(getSecretsDescriptionTable(writer, issues...)), + getJasFullDescription(violation, writer, getSecretsRuleFullDescriptionTable, issues...), + ) + return contentBuilder.String() +} + +func getSecretsDescriptionTable(writer OutputWriter, issues ...formats.SourceCodeRow) string { + // Construct table + table := NewMarkdownTable("Severity", "ID", "Status", "Finding", "Watch Name", "Policies").SetDelimiter(writer.Separator()) + // Hide optional columns if all empty (violations/no status) + table.GetColumnInfo("ID").OmitEmpty = true + table.GetColumnInfo("Status").OmitEmpty = true + table.GetColumnInfo("Watch Name").OmitEmpty = true + table.GetColumnInfo("Policies").OmitEmpty = true + // Construct rows + for _, issue := range issues { + // Determine the issue applicable status + applicability := jasutils.Applicable.String() + status := "" + if issue.Applicability != nil && issue.Applicability.Status != "" { + status = issue.Applicability.Status + if status == jasutils.Inactive.String() { + // Update the applicability status to Not Applicable for Inactive + applicability = jasutils.NotApplicable.String() + } + } + table.AddRowWithCellData( + NewCellData(writer.FormattedSeverity(issue.Severity, applicability)), + NewCellData(issue.IssueId), + NewCellData(status), + NewCellData(issue.Finding), + NewCellData(issue.Watch), + NewCellData(issue.Policies...), + ) + } + return table.Build() +} + +func getSecretsRuleFullDescriptionTable(info formats.ScannerInfo, writer OutputWriter) *MarkdownTableBuilder { + table := getBaseJasDetailsTable(info, writer) + table.AddRow(MarkAsBold("Abbreviation:"), info.RuleId) + return table +} + +// Utilities + +func getIssueType(violation bool) string { + if violation { + return "Violation" + } + return "Vulnerability" +} + +func getDirectDependenciesCellData(components []formats.ComponentRow) (dependencies CellData) { if len(components) == 0 { return NewCellData() } for _, component := range components { - dependencies = append(dependencies, fmt.Sprintf(format, component.Name, component.Version)) + dependencies = append(dependencies, results.GetDependencyId(component.Name, component.Version)) } return } -func getCveIdsCellData(cveRows []formats.CveRow) (ids CellData) { +func getCveIdsCellData(cveRows []formats.CveRow, issueId string) (ids CellData) { if len(cveRows) == 0 { - return NewCellData() + return NewCellData(issueId) } for _, cve := range cveRows { ids = append(ids, cve.Id) @@ -188,218 +604,231 @@ func getCveIdsCellData(cveRows []formats.CveRow) (ids CellData) { return } -type vulnerabilityOrViolationDetails struct { - details string - title string - dependencyName string - dependencyVersion string -} - -func vulnerabilityDetailsContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) (content []string) { - vulnerabilitiesWithDetails := getVulnerabilityWithDetails(vulnerabilities) - if len(vulnerabilitiesWithDetails) == 0 { +func getScaSecurityIssueDetailsContent(issues []formats.VulnerabilityOrViolationRow, violations bool, writer OutputWriter) (content []string) { + issuesWithDetails := getIssuesWithDetails(issues) + if len(issuesWithDetails) == 0 { return } - // Prepare content for each vulnerability details - for i := range vulnerabilitiesWithDetails { - if len(vulnerabilitiesWithDetails) == 1 { - content = append(content, vulnerabilitiesWithDetails[i].details) + for _, issue := range issuesWithDetails { + if len(issues) == 1 { + // No need for
tag if there is only one issue, just show the details + content = append(content, getScaSecurityIssueDetails(issue, violations, writer)) } else { - content = append(content, writer.MarkAsDetails( - fmt.Sprintf(`%s %s %s`, vulnerabilitiesWithDetails[i].title, - vulnerabilitiesWithDetails[i].dependencyName, - vulnerabilitiesWithDetails[i].dependencyVersion), - 4, vulnerabilitiesWithDetails[i].details, + // Add wrap the content of each issue in a
tag + content = append(content, "\n"+writer.MarkAsDetails( + getComponentIssueIdentifier(results.GetIssueIdentifier(issue.Cves, issue.IssueId, ", "), issue.ImpactedDependencyName, issue.ImpactedDependencyVersion, issue.Watch), 4, + getScaSecurityIssueDetails(issue, violations, writer), )) } } - // Split content if it exceeds the size limit and decorate it with title + // Split content if it exceeds the size limit and decorate it with title as prefix return ConvertContentToComments(content, writer, func(commentCount int, detailsContent string) string { contentBuilder := strings.Builder{} - WriteContent(&contentBuilder, writer.MarkAsTitle(vulnerableDependenciesResearchDetailsSubTitle, 3)) + WriteContent(&contentBuilder, writer.MarkAsTitle(issuesDetailsSubTitle, 3)) WriteContent(&contentBuilder, detailsContent) return contentBuilder.String() }) } -func getVulnerabilityWithDetails(vulnerabilities []formats.VulnerabilityOrViolationRow) (vulnerabilitiesWithDetails []vulnerabilityOrViolationDetails) { - for i := range vulnerabilities { - vulDescriptionContent := createVulnerabilityResearchDescription(&vulnerabilities[i]) - if vulDescriptionContent == "" { - // No content - continue +func getIssuesWithDetails(issues []formats.VulnerabilityOrViolationRow) (filter []formats.VulnerabilityOrViolationRow) { + for i := range issues { + if issues[i].JfrogResearchInformation != nil || issues[i].Summary != "" { + filter = append(filter, issues[i]) } - vulnerabilitiesWithDetails = append(vulnerabilitiesWithDetails, vulnerabilityOrViolationDetails{ - details: vulDescriptionContent, - title: getVulnerabilityDescriptionIdentifier(vulnerabilities[i].Cves, vulnerabilities[i].IssueId), - dependencyName: vulnerabilities[i].ImpactedDependencyName, - dependencyVersion: vulnerabilities[i].ImpactedDependencyVersion, - }) } return } -func createVulnerabilityResearchDescription(vulnerability *formats.VulnerabilityOrViolationRow) string { - var descriptionBuilder strings.Builder - vulnResearch := vulnerability.JfrogResearchInformation - if vulnResearch == nil { - vulnResearch = &formats.JfrogResearchInformation{Details: vulnerability.Summary} - } else if vulnResearch.Details == "" { - vulnResearch.Details = vulnerability.Summary +func getComponentIssueIdentifier(key, compName, version, watch string) (id string) { + parts := []string{} + if key != "" { + parts = append(parts, fmt.Sprintf("[ %s ]", key)) } + parts = append(parts, compName, version) + if watch != "" { + parts = append(parts, fmt.Sprintf("(%s)", watch)) + } + return strings.Join(parts, " ") +} - if vulnResearch.Details != "" { - WriteContent(&descriptionBuilder, MarkAsBold("Description:"), vulnResearch.Details) +func getScaSecurityIssueDetails(issue formats.VulnerabilityOrViolationRow, violations bool, writer OutputWriter) (content string) { + var contentBuilder strings.Builder + // Title + WriteNewLine(&contentBuilder) + WriteContent(&contentBuilder, writer.MarkAsTitle(fmt.Sprintf("%s Details", getIssueType(violations)), 3)) + // Details Table + directComponent := []string{} + for _, component := range issue.ImpactedDependencyDetails.Components { + directComponent = append(directComponent, results.GetDependencyId(component.Name, component.Version)) } - if vulnResearch.Remediation != "" { - if vulnResearch.Details != "" { - WriteNewLine(&descriptionBuilder) - } - WriteContent(&descriptionBuilder, MarkAsBold("Remediation:"), vulnResearch.Remediation) + noHeaderTable := NewNoHeaderMarkdownTable(2, false) + if len(issue.Policies) > 0 { + noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Policies:")), NewCellData(issue.Policies...)) } - return descriptionBuilder.String() -} + if issue.Watch != "" { + noHeaderTable.AddRow(MarkAsBold("Watch Name:"), issue.Watch) + } + if issue.JfrogResearchInformation != nil && issue.JfrogResearchInformation.Severity != "" { + severity := severityutils.Severity(issue.JfrogResearchInformation.Severity) + noHeaderTable.AddRow(MarkAsBold("Jfrog Research Severity:"), fmt.Sprintf("%s %s", writer.SeverityIcon(severity), severity.String())) + } + if issue.Applicable != "" { + noHeaderTable.AddRow(MarkAsBold("Contextual Analysis:"), issue.Applicable) + } + noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Direct Dependencies:")), NewCellData(directComponent...)) + noHeaderTable.AddRow(MarkAsBold("Impacted Dependency:"), results.GetDependencyId(issue.ImpactedDependencyName, issue.ImpactedDependencyVersion)) + noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("Fixed Versions:")), NewCellData(issue.FixedVersions...)) -func getVulnerabilityDescriptionIdentifier(cveRows []formats.CveRow, xrayId string) string { - identifier := results.GetIssueIdentifier(cveRows, xrayId, ", ") - if identifier == "" { - return "" + cvss := []string{} + for _, cve := range issue.Cves { + cvss = append(cvss, cve.CvssV3) } - return fmt.Sprintf("[ %s ]", identifier) -} + noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("CVSS V3:")), NewCellData(cvss...)) + WriteContent(&contentBuilder, noHeaderTable.Build()) -func LicensesContent(licenses []formats.LicenseRow, writer OutputWriter) string { - if len(licenses) == 0 { - return "" + // Summary + summary := issue.Summary + if issue.JfrogResearchInformation != nil && issue.JfrogResearchInformation.Summary != "" { + summary = issue.JfrogResearchInformation.Summary } - // Title - var contentBuilder strings.Builder - WriteContent(&contentBuilder, writer.MarkAsTitle("⚖️ Violated Licenses", 2)) - // Content - table := NewMarkdownTable("SEVERITY", "LICENSE", "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY").SetDelimiter(writer.Separator()) - for _, license := range licenses { - table.AddRowWithCellData( - NewCellData(license.Severity), - NewCellData(license.LicenseKey), - getDirectDependenciesCellData("%s %s", license.Components), - NewCellData(fmt.Sprintf("%s %s", license.ImpactedDependencyName, license.ImpactedDependencyVersion)), - ) + if summary != "" { + WriteNewLine(&contentBuilder) + WriteContent(&contentBuilder, summary) } - WriteContent(&contentBuilder, writer.MarkInCenter(table.Build())) - return contentBuilder.String() -} -// For review comment Frogbot creates on Scan PR -func GenerateReviewCommentContent(content string, writer OutputWriter) string { - var contentBuilder strings.Builder - contentBuilder.WriteString(MarkdownComment(ReviewCommentId)) - customCommentTitle := writer.PullRequestCommentTitle() - if customCommentTitle != "" { - WriteContent(&contentBuilder, writer.MarkAsTitle(MarkAsBold(customCommentTitle), 2)) + // Jfrog Research Details + if issue.JfrogResearchInformation == nil || (issue.JfrogResearchInformation.Details == "" && issue.JfrogResearchInformation.Remediation == "") { + return contentBuilder.String() } - WriteContent(&contentBuilder, content, footer(writer)) - return contentBuilder.String() -} + WriteNewLine(&contentBuilder) + WriteContent(&contentBuilder, writer.MarkAsTitle(jfrogResearchDetailsSubTitle, 3)) -// When can't create review comment, create a fallback comment by adding the location description to the content as a prefix -func GetFallbackReviewCommentContent(content string, location formats.Location, writer OutputWriter) string { - var contentBuilder strings.Builder - contentBuilder.WriteString(MarkdownComment(ReviewCommentId)) - WriteContent(&contentBuilder, getFallbackCommentLocationDescription(location), content) - return contentBuilder.String() -} - -func IsFrogbotComment(content string) bool { - return strings.Contains(content, ReviewCommentId) -} + if issue.JfrogResearchInformation.Details != "" { + WriteNewLine(&contentBuilder) + WriteContent(&contentBuilder, MarkAsBold("Description:"), issue.JfrogResearchInformation.Details) + } + if issue.JfrogResearchInformation.Remediation != "" { + WriteNewLine(&contentBuilder) + WriteContent(&contentBuilder, MarkAsBold("Remediation:"), issue.JfrogResearchInformation.Remediation) + } -func getFallbackCommentLocationDescription(location formats.Location) string { - return fmt.Sprintf("%s\nat %s (line %d)", MarkAsCodeSnippet(location.Snippet), MarkAsQuote(location.File), location.StartLine) + return contentBuilder.String() + "\n" } -func GetApplicabilityDescriptionTable(severity, cve, impactedDependency, finding string, writer OutputWriter) string { - table := NewMarkdownTable("Severity", "Impacted Dependency", "Finding", "CVE").AddRow(writer.FormattedSeverity(severity, "Applicable"), impactedDependency, finding, cve) +func getJasIssueDescriptionTable(writer OutputWriter, issues ...formats.SourceCodeRow) string { + // Construct table + table := NewMarkdownTable("Severity", "ID", "Finding", "Watch Name", "Policies").SetDelimiter(writer.Separator()) + // Hide optional columns if all empty (not violations) + table.GetColumnInfo("ID").OmitEmpty = true + table.GetColumnInfo("Watch Name").OmitEmpty = true + table.GetColumnInfo("Policies").OmitEmpty = true + // Construct rows + for _, issue := range issues { + table.AddRowWithCellData( + NewCellData(writer.FormattedSeverity(issue.Severity, "Applicable")), + NewCellData(issue.IssueId), + NewCellData(issue.Finding), + NewCellData(issue.Watch), + NewCellData(issue.Policies...), + ) + } return table.Build() } -func ApplicableCveReviewContent(severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string, writer OutputWriter) string { +// For Jas we show description for each unique rule +func getJasFullDescription(violations bool, writer OutputWriter, generateRuleTable func(formats.ScannerInfo, OutputWriter) *MarkdownTableBuilder, issues ...formats.SourceCodeRow) string { + // Group by scanner info + rulesInfo, codeFlows := groupIssuesByScanner(issues...) + // Write the details for each rule var contentBuilder strings.Builder - WriteContent(&contentBuilder, - writer.MarkAsTitle(contextualAnalysisTitle, 2), - writer.MarkInCenter(GetApplicabilityDescriptionTable(severity, cve, impactedDependency, finding, writer)), - writer.MarkAsDetails("Description", 3, fullDetails), - writer.MarkAsDetails("CVE details", 3, cveDetails), - ) - - if len(remediation) > 0 { - WriteContent(&contentBuilder, writer.MarkAsDetails("Remediation", 3, remediation)) + for _, info := range rulesInfo { + var scannerCodeFlows [][]formats.Location + if v, ok := codeFlows[info.RuleId]; ok { + scannerCodeFlows = v + } + title := "Full description" + if len(rulesInfo) > 1 { + title = getJasDetailsIdentifier(info) + } + WriteContent(&contentBuilder, writer.MarkAsDetails(title, 3, getJasRuleFullDescription(violations, info.ScannerDescription, generateRuleTable(info, writer), writer, scannerCodeFlows...))) } return contentBuilder.String() } -func getJasDescriptionTable(severity, finding string, writer OutputWriter) string { - return NewMarkdownTable("Severity", "Finding").AddRow(writer.FormattedSeverity(severity, jasutils.Applicable.String()), finding).Build() -} - -func getSecretsDescriptionTable(severity, finding, status string, writer OutputWriter) string { - columns := []string{"Severity", "Finding"} - applicability := jasutils.Applicable.String() - if status != "" { - columns = append(columns, "Status") - if status == jasutils.Inactive.String() { - applicability = jasutils.NotApplicable.String() +func groupIssuesByScanner(issues ...formats.SourceCodeRow) (rulesInfo []formats.ScannerInfo, codeFlows map[string][][]formats.Location) { + rulesInfoMap := map[string]formats.ScannerInfo{} + codeFlows = map[string][][]formats.Location{} + for _, issue := range issues { + if _, ok := rulesInfoMap[issue.RuleId]; ok { + codeFlows[issue.RuleId] = append(codeFlows[issue.RuleId], issue.CodeFlow...) + continue } - return NewMarkdownTable(columns...).AddRow(writer.FormattedSeverity(severity, applicability), finding, status).Build() + rulesInfoMap[issue.RuleId] = issue.ScannerInfo + codeFlows[issue.RuleId] = issue.CodeFlow } - return NewMarkdownTable(columns...).AddRow(writer.FormattedSeverity(severity, applicability), finding).Build() -} - -func SecretReviewContent(severity, finding, fullDetails, applicability string, writer OutputWriter) string { - var contentBuilder strings.Builder - WriteContent(&contentBuilder, - writer.MarkAsTitle(secretsTitle, 2), - writer.MarkInCenter(getSecretsDescriptionTable(severity, finding, applicability, writer)), - writer.MarkAsDetails("Full description", 3, fullDetails), - ) - return contentBuilder.String() + rulesInfo = maps.Values(rulesInfoMap) + // Sort by rule id + sort.Slice(rulesInfo, func(i, j int) bool { + return rulesInfo[i].RuleId < rulesInfo[j].RuleId + }) + return } -func IacReviewContent(severity, finding, fullDetails string, writer OutputWriter) string { - var contentBuilder strings.Builder - WriteContent(&contentBuilder, - writer.MarkAsTitle(iacTitle, 2), - writer.MarkInCenter(getJasDescriptionTable(severity, finding, writer)), - writer.MarkAsDetails("Full description", 3, fullDetails), - ) - return contentBuilder.String() +func getJasDetailsIdentifier(info formats.ScannerInfo) string { + id := info.RuleId + if info.ScannerShortDescription != "" { + id = info.ScannerShortDescription + } + return fmt.Sprintf("[ %s ]", id) } -func SastReviewContent(severity, finding, fullDetails string, codeFlows [][]formats.Location, writer OutputWriter) string { +func getJasRuleFullDescription(violation bool, scannerDescription string, issueDescTable *MarkdownTableBuilder, writer OutputWriter, codeFlows ...[]formats.Location) string { var contentBuilder strings.Builder - WriteContent(&contentBuilder, - writer.MarkAsTitle(sastTitle, 2), - writer.MarkInCenter(getJasDescriptionTable(severity, finding, writer)), - writer.MarkAsDetails("Full description", 3, fullDetails), - ) - + // Separator + WriteNewLine(&contentBuilder) + // Write the vulnerability/violation details + WriteContent(&contentBuilder, writer.MarkAsTitle(fmt.Sprintf("%s Details", getIssueType(violation)), 3)) + if issueDescTable != nil && issueDescTable.HasContent() { + WriteContent(&contentBuilder, issueDescTable.Build()) + // Separator + WriteNewLine(&contentBuilder) + } + // Write the description + WriteContent(&contentBuilder, scannerDescription, "\n") + // Write the code flows if exists if len(codeFlows) > 0 { - WriteContent(&contentBuilder, writer.MarkAsDetails("Code Flows", 3, sastCodeFlowsReviewContent(codeFlows, writer))) + WriteContent(&contentBuilder, codeFlowsReviewContent(codeFlows, writer)) } return contentBuilder.String() } -func sastCodeFlowsReviewContent(codeFlows [][]formats.Location, writer OutputWriter) string { +func codeFlowsReviewContent(codeFlows [][]formats.Location, writer OutputWriter) string { + if len(codeFlows) == 0 { + return "" + } var contentBuilder strings.Builder for _, flow := range codeFlows { - WriteContent(&contentBuilder, writer.MarkAsDetails("Vulnerable data flow analysis result", 4, sastDataFlowLocationsReviewContent(flow))) + WriteContent(&contentBuilder, writer.MarkAsDetails("Vulnerable data flow analysis result", 4, dataFlowLocationsReviewContent(flow))) } - return contentBuilder.String() + return writer.MarkAsDetails("Code Flows", 3, contentBuilder.String()) } -func sastDataFlowLocationsReviewContent(flow []formats.Location) string { +func dataFlowLocationsReviewContent(flow []formats.Location) string { var contentBuilder strings.Builder - for _, location := range flow { + for i, location := range flow { + if i == 0 { + WriteNewLine(&contentBuilder) + } WriteContent(&contentBuilder, fmt.Sprintf("%s %s (at %s line %d)\n", "↘️", MarkAsQuote(location.Snippet), location.File, location.StartLine)) } return contentBuilder.String() } + +func getBaseJasDetailsTable(ruleInfo formats.ScannerInfo, writer OutputWriter) *MarkdownTableBuilder { + noHeaderTable := NewNoHeaderMarkdownTable(2, false).SetDelimiter(writer.Separator()) + // General CWE attribute if exists + if len(ruleInfo.Cwe) > 0 { + noHeaderTable.AddRowWithCellData(NewCellData(MarkAsBold("CWE:")), NewCellData(ruleInfo.Cwe...)) + } + return noHeaderTable +} diff --git a/utils/outputwriter/outputcontent_test.go b/utils/outputwriter/outputcontent_test.go index 51945f453..29222ef0c 100644 --- a/utils/outputwriter/outputcontent_test.go +++ b/utils/outputwriter/outputcontent_test.go @@ -4,14 +4,18 @@ import ( "path/filepath" "testing" + "github.com/jfrog/frogbot/v2/utils/issues" "github.com/jfrog/froggit-go/vcsutils" + "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" + xrayApi "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" ) -func TestGetPRSummaryContent(t *testing.T) { +func TestGetMainCommentContent(t *testing.T) { testCases := []struct { name string cases []OutputTestCase @@ -19,7 +23,7 @@ func TestGetPRSummaryContent(t *testing.T) { isComment bool }{ { - name: "Summary comment No issues found", + name: "Main comment No issues found", issuesExists: false, isComment: true, cases: []OutputTestCase{ @@ -76,7 +80,7 @@ func TestGetPRSummaryContent(t *testing.T) { }, }, { - name: "Summary comment Found issues", + name: "Main comment Found issues", issuesExists: true, isComment: true, cases: []OutputTestCase{ @@ -185,7 +189,7 @@ func TestGetPRSummaryContent(t *testing.T) { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { expectedOutput := GetExpectedTestOutput(t, test) - output := GetPRSummaryContent([]string{MarkAsCodeSnippet("some content")}, tc.issuesExists, tc.isComment, test.writer) + output := GetMainCommentContent([]string{MarkAsCodeSnippet("some content")}, tc.issuesExists, tc.isComment, test.writer) assert.Len(t, output, 1) assert.Equal(t, expectedOutput, output[0]) }) @@ -193,6 +197,154 @@ func TestGetPRSummaryContent(t *testing.T) { } } +func TestScanSummaryContent(t *testing.T) { + testScanStatus := formats.ScanStatus{ + ScaStatusCode: utils.NewIntPtr(0), + ApplicabilityStatusCode: utils.NewIntPtr(0), + SastStatusCode: utils.NewIntPtr(0), + SecretsStatusCode: utils.NewIntPtr(0), + } + testIssues := issues.ScansIssuesCollection{ + ScaVulnerabilities: []formats.VulnerabilityOrViolationRow{ + {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Critical"}}}, + {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "High"}}}, + {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "High"}}}, + {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Medium"}}}, + {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Low"}}}, + {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Unknown"}}}, + }, + ScaViolations: []formats.VulnerabilityOrViolationRow{ + {ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Critical"}}}, + }, + LicensesViolations: []formats.LicenseViolationRow{ + {LicenseRow: formats.LicenseRow{ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "High"}}}}, + {LicenseRow: formats.LicenseRow{ImpactedDependencyDetails: formats.ImpactedDependencyDetails{SeverityDetails: formats.SeverityDetails{Severity: "Medium"}}}}, + }, + SecretsVulnerabilities: []formats.SourceCodeRow{ + {SeverityDetails: formats.SeverityDetails{Severity: "High"}}, + {SeverityDetails: formats.SeverityDetails{Severity: "High"}}, + }, + SastVulnerabilities: []formats.SourceCodeRow{ + {SeverityDetails: formats.SeverityDetails{Severity: "High"}}, + {SeverityDetails: formats.SeverityDetails{Severity: "High"}}, + {SeverityDetails: formats.SeverityDetails{Severity: "Low"}}, + }, + SastViolations: []formats.SourceCodeRow{{SeverityDetails: formats.SeverityDetails{Severity: "High"}}}, + } + + testCases := []struct { + name string + includeSecrets bool + scanStatus formats.ScanStatus + context results.ResultContext + issues issues.ScansIssuesCollection + cases []OutputTestCase + }{ + { + name: "No issues", + issues: issues.ScansIssuesCollection{}, + scanStatus: testScanStatus, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutput: []string{""}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: []string{""}, + }, + }, + }, + { + name: "Vulnerabilities", + issues: testIssues, + scanStatus: testScanStatus, + context: results.ResultContext{IncludeVulnerabilities: true}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_standard.md")}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_simplified.md")}, + }, + }, + }, + { + name: "Violations", + issues: testIssues, + scanStatus: testScanStatus, + context: results.ResultContext{Watches: []string{"watch"}}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_violation_standard.md")}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_violation_simplified.md")}, + }, + }, + }, + { + name: "Violations and Vulnerabilities", + issues: testIssues, + scanStatus: testScanStatus, + context: results.ResultContext{GitRepoHttpsCloneUrl: "url", PlatformWatches: &xrayApi.ResourcesWatchesBody{GitRepositoryWatches: []string{"watch"}}, IncludeVulnerabilities: true}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_both_standard.md")}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_both_simplified.md")}, + }, + }, + }, + { + name: "with errors", + issues: issues.ScansIssuesCollection{}, + scanStatus: formats.ScanStatus{ + IacStatusCode: utils.NewIntPtr(33), + }, + context: results.ResultContext{GitRepoHttpsCloneUrl: "url", PlatformWatches: &xrayApi.ResourcesWatchesBody{GitRepositoryWatches: []string{"watch"}}}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_error_standard.md")}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "summary", "summary_error_simplified.md")}, + }, + }, + }, + } + + for _, tc := range testCases { + for _, test := range tc.cases { + t.Run(tc.name+"_"+test.name, func(t *testing.T) { + expectedOutput := GetExpectedTestOutput(t, test) + tc.issues.ScanStatus = tc.scanStatus + output := ScanSummaryContent(tc.issues, tc.context, tc.includeSecrets, test.writer) + assert.Equal(t, expectedOutput, output) + }) + } + } +} + func TestVulnerabilitiesContent(t *testing.T) { testCases := []struct { name string @@ -287,96 +439,22 @@ func TestVulnerabilitiesContent(t *testing.T) { }, }, { - name: "multiple Vulnerabilities with Contextual Analysis", - vulnerabilities: []formats.VulnerabilityOrViolationRow{ - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(severityutils.Critical, jasutils.NotApplicable, false), - ImpactedDependencyName: "impacted", - ImpactedDependencyVersion: "3.0.0", - Components: []formats.ComponentRow{ - {Name: "dep1", Version: "1.0.0"}, - {Name: "dep2", Version: "2.0.0"}, - }, - }, - Applicable: "Not Applicable", - FixedVersions: []string{"4.0.0", "5.0.0"}, - Cves: []formats.CveRow{{Id: "CVE-1111-11111", Applicability: &formats.Applicability{Status: "Not Applicable"}}}, - }, - { - Summary: "Summary XRAY-122345", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(severityutils.High, jasutils.ApplicabilityUndetermined, false), - ImpactedDependencyName: "github.com/nats-io/nats-streaming-server", - ImpactedDependencyVersion: "v0.21.0", - Components: []formats.ComponentRow{ - { - Name: "github.com/nats-io/nats-streaming-server", - Version: "v0.21.0", - }, - }, - }, - Applicable: "Undetermined", - FixedVersions: []string{"[0.24.1]"}, - IssueId: "XRAY-122345", - JfrogResearchInformation: &formats.JfrogResearchInformation{ - Remediation: "some remediation", - }, - Cves: []formats.CveRow{{}}, - }, - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(severityutils.Medium, jasutils.Applicable, false), - ImpactedDependencyName: "component-D", - ImpactedDependencyVersion: "v0.21.0", - Components: []formats.ComponentRow{ - { - Name: "component-D", - Version: "v0.21.0", - }, - }, - }, - Applicable: "Applicable", - FixedVersions: []string{"[0.24.3]"}, - JfrogResearchInformation: &formats.JfrogResearchInformation{ - Remediation: "some remediation", - }, - Cves: []formats.CveRow{ - {Id: "CVE-2022-26652"}, - {Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable"}}, - }, - }, - { - Summary: "Summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: severityutils.GetAsDetails(severityutils.Low, jasutils.ApplicabilityUndetermined, false), - ImpactedDependencyName: "github.com/mholt/archiver/v3", - ImpactedDependencyVersion: "v3.5.1", - Components: []formats.ComponentRow{ - { - Name: "github.com/mholt/archiver/v3", - Version: "v3.5.1", - }, - }, - }, - Applicable: "Undetermined", - Cves: []formats.CveRow{}, - }, - }, + name: "multiple Vulnerabilities with Contextual Analysis", + vulnerabilities: getTestScaIssues(false), cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{MarkdownOutput{showCaColumn: true}}, + writer: &StandardOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true}}, + writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_simplified.md")}, }, { name: "Split Standard output", - writer: &StandardOutput{MarkdownOutput{showCaColumn: true, descriptionSizeLimit: 1720, commentSizeLimit: 1720}}, + writer: &StandardOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true, descriptionSizeLimit: 1720, commentSizeLimit: 1720}}, expectedOutputPath: []string{ filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_standard_split1.md"), filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_standard_split2.md"), @@ -384,7 +462,7 @@ func TestVulnerabilitiesContent(t *testing.T) { }, { name: "Split Simplified output", - writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, descriptionSizeLimit: 1000, commentSizeLimit: 1000}}, + writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true, descriptionSizeLimit: 1000, commentSizeLimit: 2000}}, expectedOutputPath: []string{ filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_simplified_split1.md"), filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_simplified_split2.md"), @@ -392,15 +470,63 @@ func TestVulnerabilitiesContent(t *testing.T) { }, }, }, + } + for _, tc := range testCases { + for _, test := range tc.cases { + t.Run(tc.name+"_"+test.name, func(t *testing.T) { + expectedOutput := GetExpectedTestCaseOutput(t, test) + output := ConvertContentToComments(GetVulnerabilitiesContent(tc.vulnerabilities, test.writer), test.writer) + assert.Len(t, output, len(expectedOutput)) + assert.ElementsMatch(t, expectedOutput, output) + }) + } + } +} + +func TestSecurityViolationsContent(t *testing.T) { + testCases := []struct { + name string + issues issues.ScansIssuesCollection + cases []OutputTestCase + }{ { - name: "Split vulnerabilities content", + name: "No security violations", + issues: issues.ScansIssuesCollection{}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutput: []string{""}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: []string{""}, + }, + }, + }, + { + name: "Security violations", + issues: issues.ScansIssuesCollection{ScaViolations: getTestScaIssues(true)}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "security", "security_violation_standard.md")}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true, hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "security", "security_violation_simplified.md")}, + }, + }, }, } for _, tc := range testCases { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { expectedOutput := GetExpectedTestCaseOutput(t, test) - output := ConvertContentToComments(VulnerabilitiesContent(tc.vulnerabilities, test.writer), test.writer) + output := ConvertContentToComments(getSecurityViolationsContent(tc.issues, test.writer), test.writer) assert.Len(t, output, len(expectedOutput)) assert.ElementsMatch(t, expectedOutput, output) }) @@ -408,74 +534,173 @@ func TestVulnerabilitiesContent(t *testing.T) { } } +func getTestScaIssues(violations bool) []formats.VulnerabilityOrViolationRow { + issues := []formats.VulnerabilityOrViolationRow{ + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severityutils.Critical, jasutils.NotApplicable, false), + ImpactedDependencyName: "impacted", + ImpactedDependencyVersion: "3.0.0", + Components: []formats.ComponentRow{ + {Name: "dep1", Version: "1.0.0"}, + {Name: "dep2", Version: "2.0.0"}, + }, + }, + Applicable: "Not Applicable", + FixedVersions: []string{"4.0.0", "5.0.0"}, + Cves: []formats.CveRow{{Id: "CVE-1111-11111", Applicability: &formats.Applicability{Status: "Not Applicable"}}}, + }, + { + Summary: "Summary XRAY-122345", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severityutils.High, jasutils.ApplicabilityUndetermined, false), + ImpactedDependencyName: "github.com/nats-io/nats-streaming-server", + ImpactedDependencyVersion: "v0.21.0", + Components: []formats.ComponentRow{ + { + Name: "github.com/nats-io/nats-streaming-server", + Version: "v0.21.0", + }, + }, + }, + Applicable: "Undetermined", + FixedVersions: []string{"[0.24.1]"}, + IssueId: "XRAY-122345", + JfrogResearchInformation: &formats.JfrogResearchInformation{ + Remediation: "some remediation", + }, + Cves: []formats.CveRow{}, + }, + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severityutils.Medium, jasutils.Applicable, false), + ImpactedDependencyName: "component-D", + ImpactedDependencyVersion: "v0.21.0", + Components: []formats.ComponentRow{ + { + Name: "component-D", + Version: "v0.21.0", + }, + }, + }, + Applicable: "Applicable", + FixedVersions: []string{"[0.24.3]"}, + JfrogResearchInformation: &formats.JfrogResearchInformation{ + Remediation: "some remediation", + }, + Cves: []formats.CveRow{ + {Id: "CVE-2022-26652"}, + {Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable"}}, + }, + }, + { + Summary: "Summary", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: severityutils.GetAsDetails(severityutils.Low, jasutils.ApplicabilityUndetermined, false), + ImpactedDependencyName: "github.com/mholt/archiver/v3", + ImpactedDependencyVersion: "v3.5.1", + Components: []formats.ComponentRow{ + { + Name: "github.com/mholt/archiver/v3", + Version: "v3.5.1", + }, + }, + }, + Applicable: "Undetermined", + Cves: []formats.CveRow{}, + }, + } + if violations { + for _, issue := range issues { + issue.ViolationContext = formats.ViolationContext{ + Watch: "watch", + Policies: []string{"policy1", "policy2"}, + } + } + } + return issues +} + func TestLicensesContent(t *testing.T) { testCases := []struct { name string - licenses []formats.LicenseRow + licenses []formats.LicenseViolationRow cases []OutputTestCase }{ { name: "No license violations", - licenses: []formats.LicenseRow{}, + licenses: []formats.LicenseViolationRow{}, cases: []OutputTestCase{ { name: "Standard output", writer: &StandardOutput{}, - expectedOutput: []string{""}, + expectedOutput: []string{}, }, { name: "Simplified output", writer: &SimplifiedOutput{}, - expectedOutput: []string{""}, + expectedOutput: []string{}, }, }, }, { name: "License violations", - licenses: []formats.LicenseRow{ - { - LicenseKey: "License1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - - Components: []formats.ComponentRow{{Name: "Comp1", Version: "1.0"}}, - ImpactedDependencyName: "Dep1", - ImpactedDependencyVersion: "2.0", - SeverityDetails: formats.SeverityDetails{ - Severity: "High", + licenses: []formats.LicenseViolationRow{ + { + LicenseRow: formats.LicenseRow{ + LicenseKey: "License1", + LicenseName: "License1 full name", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{{Name: "Comp1", Version: "1.0"}}, + ImpactedDependencyName: "Dep1", + ImpactedDependencyVersion: "2.0", + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + }, }, }, + ViolationContext: formats.ViolationContext{ + Watch: "watch", + Policies: []string{"policy1", "policy2"}, + }, }, { - LicenseKey: "License2", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{ - { - Name: "root", - Version: "1.0.0", + LicenseRow: formats.LicenseRow{ + LicenseKey: "License2", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{ + { + Name: "root", + Version: "1.0.0", + }, + { + Name: "minimatch", + Version: "1.2.3", + }, }, - { - Name: "minimatch", - Version: "1.2.3", + ImpactedDependencyName: "Dep2", + ImpactedDependencyVersion: "3.0", + SeverityDetails: formats.SeverityDetails{ + Severity: "High", }, }, - ImpactedDependencyName: "Dep2", - ImpactedDependencyVersion: "3.0", - SeverityDetails: formats.SeverityDetails{ - Severity: "High", - }, + }, + ViolationContext: formats.ViolationContext{ + Watch: "watch2", + Policies: []string{"policy3"}, }, }, }, cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{}, - expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "license", "license_violation_standard.md")}, + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "license", "license_violation_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{}, - expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "license", "license_violation_simplified.md")}, + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testSummaryCommentDir, "violations", "license", "license_violation_simplified.md")}, }, }, }, @@ -483,7 +708,8 @@ func TestLicensesContent(t *testing.T) { for _, tc := range testCases { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { - assert.Equal(t, GetExpectedTestOutput(t, test), LicensesContent(tc.licenses, test.writer)) + expectedOutput := GetExpectedTestCaseOutput(t, test) + assert.Equal(t, expectedOutput, PolicyViolationsContent(issues.ScansIssuesCollection{LicensesViolations: tc.licenses}, test.writer)) }) } } @@ -567,7 +793,7 @@ func TestGenerateReviewComment(t *testing.T) { expectedOutput := GetExpectedTestOutput(t, test) output := GenerateReviewCommentContent(content, test.writer) if tc.location != nil { - output = GetFallbackReviewCommentContent(content, *tc.location, test.writer) + output = GetFallbackReviewCommentContent(content, *tc.location) } assert.Equal(t, expectedOutput, output) }) @@ -578,39 +804,48 @@ func TestGenerateReviewComment(t *testing.T) { func TestApplicableReviewContent(t *testing.T) { testCases := []struct { name string + issue issues.ApplicableEvidences severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string cases []OutputTestCase }{ { - name: "Applicable CVE review comment content", - severity: "Critical", - finding: "The vulnerable function flask.Flask.run is called", - fullDetails: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.", - cve: "CVE-2022-29361", - cveDetails: "cveDetails", - impactedDependency: "werkzeug:1.0.1", - remediation: "some remediation", + name: "Applicable CVE review comment content", + issue: issues.ApplicableEvidences{ + Severity: "Critical", + IssueId: "CVE-2022-29361", + ScannerDescription: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.", + CveSummary: "cveDetails", + ImpactedDependency: "werkzeug:1.0.1", + Remediation: "some remediation", + Evidence: formats.Evidence{ + Reason: "The vulnerable function flask.Flask.run is called", + }, + }, cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{}, + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "applicable", "applicable_review_content_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{}, + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "applicable", "applicable_review_content_simplified.md")}, }, }, }, { - name: "No remediation", - severity: "Critical", - finding: "The vulnerable function flask.Flask.run is called", - fullDetails: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.", - cve: "CVE-2022-29361", - cveDetails: "cveDetails", - impactedDependency: "werkzeug:1.0.1", + name: "No remediation and internet connection", + issue: issues.ApplicableEvidences{ + Severity: "Critical", + IssueId: "CVE-2022-29361", + ScannerDescription: "The scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.", + CveSummary: "cveDetails", + ImpactedDependency: "werkzeug:1.0.1", + Evidence: formats.Evidence{ + Reason: "The vulnerable function flask.Flask.run is called", + }, + }, cases: []OutputTestCase{ { name: "Standard output", @@ -630,7 +865,7 @@ func TestApplicableReviewContent(t *testing.T) { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { expectedOutput := GetExpectedTestOutput(t, test) - assert.Equal(t, expectedOutput, ApplicableCveReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.cve, tc.cveDetails, tc.impactedDependency, tc.remediation, test.writer)) + assert.Equal(t, expectedOutput, ApplicableCveReviewContent(tc.issue, test.writer)) }) } } @@ -638,54 +873,124 @@ func TestApplicableReviewContent(t *testing.T) { func TestSecretsReviewContent(t *testing.T) { testCases := []struct { - name string - severity, finding, fullDetails, status string - cases []OutputTestCase + name string + issues []formats.SourceCodeRow + cases []OutputTestCase }{ { - name: "Secret review comment content", - severity: "Medium", - finding: "Secret keys were found", - fullDetails: "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", + name: "Secret review comment content", + issues: []formats.SourceCodeRow{{ + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Finding: "Secret keys were found", + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule-id", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Scanner Short Description", + }, + }}, cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{}, + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_no_ca_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{}, + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_no_ca_simplified.md")}, }, }, }, { - name: "Secret review comment content with applicability status", - severity: "Medium", - status: "Active", - finding: "Secret keys were found", - fullDetails: "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", + name: "Secret review comment content with applicability status", + issues: []formats.SourceCodeRow{{ + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Applicability: &formats.Applicability{Status: jasutils.Active.String()}, + Finding: "Secret keys were found", + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule-id", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Scanner Short Description", + }, + }}, cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{}, + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{}, + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_review_content_simplified.md")}, }, }, }, + { + name: "Secrets violation review comment content with applicability status", + issues: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Applicability: &formats.Applicability{Status: jasutils.Active.String()}, + Finding: "Secret keys were found", + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule-id", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Scanner Short Description", + }, + ViolationContext: formats.ViolationContext{ + Watch: "jas-watch", + IssueId: "secret-violation-id", + Policies: []string{"policy1"}, + }, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "Critical"}, + Applicability: &formats.Applicability{Status: jasutils.Inactive.String()}, + Finding: "Secret keys were found", + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule-id", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Scanner Short Description", + }, + ViolationContext: formats.ViolationContext{ + Watch: "jas-watch2", + IssueId: "secret-violation-id-2", + Policies: []string{"policy1", "policy2"}, + }, + }, + }, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_violation_review_content_standard.md")}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "secrets", "secret_violation_review_content_simplified.md")}, + }, + }, + }, } for _, tc := range testCases { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { expectedOutput := GetExpectedTestOutput(t, test) - assert.Equal(t, expectedOutput, SecretReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.status, test.writer)) + violations := false + for _, issue := range tc.issues { + if issue.Watch != "" { + violations = true + break + } + } + assert.Equal(t, expectedOutput, SecretReviewContent(violations, test.writer, tc.issues...)) }) } } @@ -693,35 +998,77 @@ func TestSecretsReviewContent(t *testing.T) { func TestIacReviewContent(t *testing.T) { testCases := []struct { - name string - severity, finding, fullDetails string - cases []OutputTestCase + name string + issues []formats.SourceCodeRow + cases []OutputTestCase }{ { - name: "Iac review comment content", - severity: "Medium", - finding: "Missing auto upgrade was detected", - fullDetails: "Resource `google_container_node_pool` should have `management.auto_upgrade=true`\n\nVulnerable example - \n```\nresource \"google_container_node_pool\" \"vulnerable_example\" {\n management {\n auto_upgrade = false\n }\n}\n```\n", + name: "Iac review comment content", + issues: []formats.SourceCodeRow{{ + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + Finding: "Missing auto upgrade was detected", + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule-id", + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Scanner Short Description", + }, + }}, cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{}, + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_review_content_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{}, + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_review_content_simplified.md")}, }, }, }, + { + name: "Iac violation review comment content", + issues: []formats.SourceCodeRow{{ + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + Finding: "Missing auto upgrade was detected", + ScannerInfo: formats.ScannerInfo{ + RuleId: "rule-id", + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Scanner Short Description", + }, + ViolationContext: formats.ViolationContext{ + IssueId: "iac-violation-id", + Watch: "jas-watch", + Policies: []string{"policy1", "policy2"}, + }, + }}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_violation_review_content_standard.md")}, + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "iac", "iac_violation_review_content_simplified.md")}, + }, + }, + }, } for _, tc := range testCases { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { expectedOutput := GetExpectedTestOutput(t, test) - assert.Equal(t, expectedOutput, IacReviewContent(tc.severity, tc.finding, tc.fullDetails, test.writer)) + violations := false + for _, issue := range tc.issues { + if issue.Watch != "" { + violations = true + break + } + } + assert.Equal(t, expectedOutput, IacReviewContent(violations, test.writer, tc.issues...)) }) } } @@ -729,84 +1076,194 @@ func TestIacReviewContent(t *testing.T) { func TestSastReviewContent(t *testing.T) { testCases := []struct { - name string - severity string - finding string - fullDetails string - codeFlows [][]formats.Location - cases []OutputTestCase + name string + issues []formats.SourceCodeRow + cases []OutputTestCase }{ { - name: "Sast review comment content", - severity: "Low", - finding: "Stack Trace Exposure", - fullDetails: "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output.", - codeFlows: [][]formats.Location{ + name: "No code flows (no internet connection)", + issues: []formats.SourceCodeRow{{ + SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + Finding: "Found a Use of Insecure Random", + ScannerInfo: formats.ScannerInfo{ + RuleId: "js-insecure-random", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Use of Insecure Random", + }, + }}, + cases: []OutputTestCase{ { - { - File: "file2", - StartLine: 1, - StartColumn: 2, - EndLine: 3, - EndColumn: 4, - Snippet: "other-snippet", - }, - { - File: "file", - StartLine: 0, - StartColumn: 0, - EndLine: 0, - EndColumn: 0, - Snippet: "snippet", - }, + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_standard.md")}, }, { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_simplified.md")}, + }, + }, + }, + { + name: "Sast review comment content", + issues: []formats.SourceCodeRow{{ + SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + Finding: "Found a Use of Insecure Random", + ScannerInfo: formats.ScannerInfo{ + RuleId: "js-insecure-random", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Use of Insecure Random", + }, + CodeFlow: [][]formats.Location{ { - File: "file", - StartLine: 10, - StartColumn: 20, - EndLine: 10, - EndColumn: 30, - Snippet: "a-snippet", + { + File: "file2", + StartLine: 1, + StartColumn: 2, + EndLine: 3, + EndColumn: 4, + Snippet: "other-snippet", + }, + { + File: "file", + StartLine: 0, + StartColumn: 0, + EndLine: 0, + EndColumn: 0, + Snippet: "snippet", + }, }, { - File: "file", - StartLine: 0, - StartColumn: 0, - EndLine: 0, - EndColumn: 0, - Snippet: "snippet", + { + File: "file", + StartLine: 10, + StartColumn: 20, + EndLine: 10, + EndColumn: 30, + Snippet: "a-snippet", + }, + { + File: "file", + StartLine: 0, + StartColumn: 0, + EndLine: 0, + EndColumn: 0, + Snippet: "snippet", + }, }, }, - }, + }}, cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{}, + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{}, + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_simplified.md")}, }, }, }, { - name: "No code flows", - severity: "Low", - finding: "Stack Trace Exposure", - fullDetails: "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output.", + name: "Sast violation review comment content", + issues: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{Severity: "Low"}, + Finding: "Found a Use of Insecure Random", + ScannerInfo: formats.ScannerInfo{ + RuleId: "js-insecure-random", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Use of Insecure Random", + }, + ViolationContext: formats.ViolationContext{ + IssueId: "sast-violation-id", + Watch: "jas-watch", + Policies: []string{"policy1", "policy2"}, + }, + CodeFlow: [][]formats.Location{ + { + { + File: "file2", + StartLine: 1, + StartColumn: 2, + EndLine: 3, + EndColumn: 4, + Snippet: "other-snippet", + }, + { + File: "file", + StartLine: 0, + StartColumn: 0, + EndLine: 0, + EndColumn: 0, + Snippet: "snippet", + }, + }, + { + { + File: "file", + StartLine: 10, + StartColumn: 20, + EndLine: 10, + EndColumn: 30, + Snippet: "a-snippet", + }, + { + File: "file", + StartLine: 0, + StartColumn: 0, + EndLine: 0, + EndColumn: 0, + Snippet: "snippet", + }, + }, + }, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Finding: "Found a Use of Insecure Random", + ScannerInfo: formats.ScannerInfo{ + RuleId: "js-insecure-random", + Cwe: []string{"CWE-798", "CWE-799"}, + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Use of Insecure Random", + }, + ViolationContext: formats.ViolationContext{ + IssueId: "sast-violation-id-2", + Watch: "jas-watch2", + Policies: []string{"policy3"}, + }, + }, + { + SeverityDetails: formats.SeverityDetails{Severity: "High"}, + Finding: "Found An Express Not Using Helmet", + ScannerInfo: formats.ScannerInfo{ + RuleId: "js-express-without-helmet", + ScannerDescription: "Scanner Description....", + ScannerShortDescription: "Express Not Using Helmet", + }, + ViolationContext: formats.ViolationContext{ + IssueId: "sast-violation-id-3", + Watch: "jas-watch2", + Policies: []string{"policy3"}, + }, + }, + }, cases: []OutputTestCase{ { name: "Standard output", - writer: &StandardOutput{}, - expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_standard.md")}, + writer: &StandardOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_violation_review_content_standard.md")}, }, { name: "Simplified output", - writer: &SimplifiedOutput{}, - expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_simplified.md")}, + writer: &SimplifiedOutput{MarkdownOutput{hasInternetConnection: true}}, + expectedOutputPath: []string{filepath.Join(testReviewCommentDir, "sast", "sast_violation_review_content_simplified.md")}, }, }, }, @@ -816,7 +1273,14 @@ func TestSastReviewContent(t *testing.T) { for _, test := range tc.cases { t.Run(tc.name+"_"+test.name, func(t *testing.T) { expectedOutput := GetExpectedTestOutput(t, test) - assert.Equal(t, expectedOutput, SastReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.codeFlows, test.writer)) + violations := false + for _, issue := range tc.issues { + if issue.Watch != "" { + violations = true + break + } + } + assert.Equal(t, expectedOutput, SastReviewContent(violations, test.writer, tc.issues...)) }) } } diff --git a/utils/outputwriter/outputwriter.go b/utils/outputwriter/outputwriter.go index 865c0ed02..841ae2eb2 100644 --- a/utils/outputwriter/outputwriter.go +++ b/utils/outputwriter/outputwriter.go @@ -6,6 +6,7 @@ import ( "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/froggit-go/vcsutils" + "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -105,6 +106,7 @@ type OutputWriter interface { VcsProvider() vcsutils.VcsProvider SetVcsProvider(provider vcsutils.VcsProvider) // Markdown interface + SeverityIcon(severity severityutils.Severity) string FormattedSeverity(severity, applicability string) string Separator() string MarkInCenter(content string) string @@ -219,6 +221,10 @@ func MarkAsLink(content, link string) string { return fmt.Sprintf("[%s](%s)", content, link) } +func MarkAsBullet(content string) string { + return fmt.Sprintf("- %s", content) +} + func SectionDivider() string { return "\n---" } diff --git a/utils/outputwriter/simplifiedoutput.go b/utils/outputwriter/simplifiedoutput.go index 114e19630..3f5ff524e 100644 --- a/utils/outputwriter/simplifiedoutput.go +++ b/utils/outputwriter/simplifiedoutput.go @@ -3,6 +3,8 @@ package outputwriter import ( "fmt" "strings" + + "github.com/jfrog/jfrog-cli-security/utils/severityutils" ) const ( @@ -17,6 +19,10 @@ func (smo *SimplifiedOutput) Separator() string { return simpleSeparator } +func (smo *SimplifiedOutput) SeverityIcon(severity severityutils.Severity) string { + return severityutils.GetSeverityIcon(severity) +} + func (smo *SimplifiedOutput) FormattedSeverity(severity, _ string) string { return severity } @@ -30,12 +36,16 @@ func (smo *SimplifiedOutput) MarkInCenter(content string) string { } func (smo *SimplifiedOutput) MarkAsDetails(summary string, subTitleDepth int, content string) string { - return fmt.Sprintf("%s\n%s", smo.MarkAsTitle(summary, subTitleDepth), content) + delimiter := "\n" + if subTitleDepth == 0 { + delimiter = ": " + } + return fmt.Sprintf("%s%s%s", smo.MarkAsTitle(summary, subTitleDepth), delimiter, content) } func (smo *SimplifiedOutput) MarkAsTitle(title string, subTitleDepth int) string { if subTitleDepth == 0 { - return fmt.Sprintf("%s\n%s\n%s", SectionDivider(), title, SectionDivider()) + return title } return fmt.Sprintf("%s\n%s %s\n%s", SectionDivider(), strings.Repeat("#", subTitleDepth), title, SectionDivider()) } diff --git a/utils/outputwriter/simplifiedoutput_test.go b/utils/outputwriter/simplifiedoutput_test.go index 6dc860cfb..5642dbb9d 100644 --- a/utils/outputwriter/simplifiedoutput_test.go +++ b/utils/outputwriter/simplifiedoutput_test.go @@ -126,11 +126,11 @@ func TestSimpleMarkAsDetails(t *testing.T) { subTitleDepth int }{ { - name: "empty", - summary: "", + name: "inline", + summary: "title", subTitleDepth: 0, - content: "", - expectedOutput: "\n---\n\n\n---\n", + content: "details", + expectedOutput: "title: details", }, { name: "empty content", @@ -184,10 +184,10 @@ func TestSimpleMarkAsTitle(t *testing.T) { subTitleDepth int }{ { - name: "empty", - title: "", + name: "inline", + title: "title", subTitleDepth: 0, - expectedOutput: "\n---\n\n\n---", + expectedOutput: "title", }, { name: "Main title", diff --git a/utils/outputwriter/standardoutput.go b/utils/outputwriter/standardoutput.go index 2a1a50aa3..7143f72b7 100644 --- a/utils/outputwriter/standardoutput.go +++ b/utils/outputwriter/standardoutput.go @@ -3,6 +3,8 @@ package outputwriter import ( "fmt" "strings" + + "github.com/jfrog/jfrog-cli-security/utils/severityutils" ) type StandardOutput struct { @@ -13,7 +15,18 @@ func (so *StandardOutput) Separator() string { return "
" } +func (so *StandardOutput) SeverityIcon(severity severityutils.Severity) string { + if !so.hasInternetConnection { + return severityutils.GetSeverityIcon(severity) + } + return getSmallSeverityTag(IconName(severity)) + +} + func (so *StandardOutput) FormattedSeverity(severity, applicability string) string { + if !so.hasInternetConnection { + return severity + } return fmt.Sprintf("%s%8s", getSeverityTag(IconName(severity), applicability), severity) } @@ -28,14 +41,11 @@ func (so *StandardOutput) MarkInCenter(content string) string { return GetMarkdownCenterTag(content) } -func (so *StandardOutput) MarkAsDetails(summary string, subTitleDepth int, content string) string { +func (so *StandardOutput) MarkAsDetails(summary string, _ int, content string) string { if summary != "" { - summary = fmt.Sprintf(" %s \n", summary) - } - if subTitleDepth > 0 { - summary += "
\n" + summary = fmt.Sprintf("%s", summary) } - return fmt.Sprintf("
\n%s\n%s\n\n
\n", summary, content) + return fmt.Sprintf("
%s%s
", summary, content) } func (so *StandardOutput) MarkAsTitle(title string, subTitleDepth int) string { diff --git a/utils/outputwriter/standardoutput_test.go b/utils/outputwriter/standardoutput_test.go index 133c01905..f3f235901 100644 --- a/utils/outputwriter/standardoutput_test.go +++ b/utils/outputwriter/standardoutput_test.go @@ -52,27 +52,42 @@ func TestStandardSeparator(t *testing.T) { func TestStandardFormattedSeverity(t *testing.T) { testCases := []struct { - name string - severity string - applicability string - expectedOutput string + name string + severity string + applicability string + expectedOutput string + internetConnection bool }{ + { + name: "Applicable severity", + severity: "Low", + applicability: "Applicable", + internetConnection: true, + expectedOutput: "![low](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low", + }, + { + name: "Not applicable severity", + severity: "Medium", + applicability: "Not Applicable", + internetConnection: true, + expectedOutput: "![medium (not applicable)](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableMedium.png)
Medium", + }, { name: "Applicable severity", severity: "Low", applicability: "Applicable", - expectedOutput: "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low", + expectedOutput: "Low", }, { name: "Not applicable severity", severity: "Medium", applicability: "Not Applicable", - expectedOutput: "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableMedium.png)
Medium", + expectedOutput: "Medium", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - smo := &StandardOutput{} + smo := &StandardOutput{MarkdownOutput: MarkdownOutput{hasInternetConnection: tc.internetConnection}} assert.Equal(t, tc.expectedOutput, smo.FormattedSeverity(tc.severity, tc.applicability)) }) } @@ -161,42 +176,42 @@ func TestStandardMarkAsDetails(t *testing.T) { summary: "", subTitleDepth: 0, content: "", - expectedOutput: "
\n\n\n\n
\n", + expectedOutput: "

", }, { name: "empty content", summary: "summary", subTitleDepth: 1, content: "", - expectedOutput: "
\n summary \n
\n\n\n\n
\n", + expectedOutput: "
summary
", }, { name: "empty summary", summary: "", subTitleDepth: 0, content: "content", - expectedOutput: "
\n\ncontent\n\n
\n", + expectedOutput: "
content
", }, { name: "Main details", summary: "summary", subTitleDepth: 1, content: "content", - expectedOutput: "
\n summary \n
\n\ncontent\n\n
\n", + expectedOutput: "
summarycontent
", }, { name: "Sub details", summary: "summary", subTitleDepth: 2, content: "content", - expectedOutput: "
\n summary \n
\n\ncontent\n\n
\n", + expectedOutput: "
summarycontent
", }, { name: "Sub sub details", summary: "summary", subTitleDepth: 3, content: "content", - expectedOutput: "
\n summary \n
\n\ncontent\n\n
\n", + expectedOutput: "
summarycontent
", }, } for _, tc := range testCases { diff --git a/utils/params.go b/utils/params.go index ad71a894f..cbc6cfe96 100644 --- a/utils/params.go +++ b/utils/params.go @@ -285,10 +285,11 @@ func (s *Scan) setDefaultsIfNeeded() (err error) { } type JFrogPlatform struct { - XrayVersion string - XscVersion string - Watches []string `yaml:"watches,omitempty"` - JFrogProjectKey string `yaml:"jfrogProjectKey,omitempty"` + XrayVersion string + XscVersion string + Watches []string `yaml:"watches,omitempty"` + IncludeVulnerabilities bool `yaml:"includeVulnerabilities,omitempty"` + JFrogProjectKey string `yaml:"jfrogProjectKey,omitempty"` } func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) { @@ -298,7 +299,6 @@ func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) { return } } - if jp.JFrogProjectKey == "" { if err = readParamFromEnv(jfrogProjectEnv, &jp.JFrogProjectKey); err != nil && !e.IsMissingEnvErr(err) { return @@ -306,6 +306,11 @@ func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) { // We don't want to return an error from this function if the error is of type ErrMissingEnv because JFrogPlatform environment variables are not mandatory. err = nil } + if !jp.IncludeVulnerabilities { + if jp.IncludeVulnerabilities, err = getBoolEnv(IncludeVulnerabilitiesEnv, false); err != nil { + return + } + } return } @@ -329,6 +334,19 @@ type Git struct { UseLocalRepository bool } +func (g *Git) GetRepositoryHttpsCloneUrl(gitClient vcsclient.VcsClient) (string, error) { + if g.RepositoryCloneUrl != "" { + return g.RepositoryCloneUrl, nil + } + // If the repository clone URL is not cached, we fetch it from the VCS provider + repositoryInfo, err := gitClient.GetRepositoryInfo(context.Background(), g.RepoOwner, g.RepoName) + if err != nil { + return "", fmt.Errorf("failed to fetch the repository clone URL. %s", err.Error()) + } + g.RepositoryCloneUrl = repositoryInfo.CloneInfo.HTTP + return g.RepositoryCloneUrl, nil +} + func (g *Git) setDefaultsIfNeeded(gitParamsFromEnv *Git, commandName string) (err error) { g.RepoOwner = gitParamsFromEnv.RepoOwner g.GitProvider = gitParamsFromEnv.GitProvider @@ -815,7 +833,6 @@ func getConfigProfileIfExistsAndValid(xrayVersion, xscVersion string, jfrogServe log.Debug(fmt.Sprintf("Configuration Profile usage is disabled. All configurations will be derived from environment variables and files.\nTo enable a Configuration Profile, please set %s to TRUE", JfrogUseConfigProfileEnv)) return } - // Attempt to get the config profile by profile's name profileName := getTrimmedEnv(JfrogConfigProfileEnv) if profileName != "" { @@ -826,14 +843,10 @@ func getConfigProfileIfExistsAndValid(xrayVersion, xscVersion string, jfrogServe err = verifyConfigProfileValidity(configProfile) return } - // Getting repository's url in order to get repository HTTP url - repositoryInfo, err := gitClient.GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName) - if err != nil { - return nil, "", err + if repoCloneUrl, err = gitParams.GetRepositoryHttpsCloneUrl(gitClient); err != nil { + return } - repoCloneUrl = repositoryInfo.CloneInfo.HTTP - // Attempt to get a config profile associated with the repo URL log.Debug(fmt.Sprintf("Configuration profile was requested. Searching profile associated to repository '%s'", jfrogServer.Url)) if configProfile, err = xsc.GetConfigProfileByUrl(xrayVersion, jfrogServer, repoCloneUrl); err != nil || configProfile == nil { diff --git a/utils/scandetails.go b/utils/scandetails.go index 1de80e0ba..0dd75d135 100644 --- a/utils/scandetails.go +++ b/utils/scandetails.go @@ -3,7 +3,6 @@ package utils import ( "context" "fmt" - "os" "path/filepath" "time" @@ -11,20 +10,19 @@ import ( "github.com/jfrog/froggit-go/vcsclient" "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" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/severityutils" - "github.com/jfrog/jfrog-cli-security/utils/xray/scangraph" "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/jfrog/jfrog-client-go/xray/services" + xscservices "github.com/jfrog/jfrog-client-go/xsc/services" ) type ScanDetails struct { *Project *Git - *services.XrayGraphScanParams + + *xscservices.XscGitInfoContext *config.ServerDetails client vcsclient.VcsClient failOnInstallationErrors bool @@ -35,13 +33,24 @@ type ScanDetails struct { baseBranch string configProfile *clientservices.ConfigProfile allowPartialResults bool - StartTime time.Time + + results.ResultContext + MultiScanId string + XrayVersion string + XscVersion string + StartTime time.Time } func NewScanDetails(client vcsclient.VcsClient, server *config.ServerDetails, git *Git) *ScanDetails { return &ScanDetails{client: client, ServerDetails: server, Git: git} } +func (sc *ScanDetails) SetJfrogVersions(xrayVersion, xscVersion string) *ScanDetails { + sc.XrayVersion = xrayVersion + sc.XscVersion = xscVersion + return sc +} + func (sc *ScanDetails) SetDisableJas(disable bool) *ScanDetails { sc.disableJas = disable return sc @@ -57,8 +66,8 @@ func (sc *ScanDetails) SetProject(project *Project) *ScanDetails { return sc } -func (sc *ScanDetails) SetXrayGraphScanParams(watches []string, jfrogProjectKey string, includeLicenses bool) *ScanDetails { - sc.XrayGraphScanParams = createXrayScanParams(watches, jfrogProjectKey, includeLicenses) +func (sc *ScanDetails) SetResultsContext(httpCloneUrl string, watches []string, jfrogProjectKey string, includeVulnerabilities, includeLicenses bool) *ScanDetails { + sc.ResultContext = audit.CreateAuditResultsContext(sc.ServerDetails, sc.XrayVersion, watches, sc.RepoPath, jfrogProjectKey, httpCloneUrl, includeVulnerabilities, includeLicenses) return sc } @@ -137,43 +146,6 @@ func (sc *ScanDetails) AllowPartialResults() bool { return sc.allowPartialResults } -func (sc *ScanDetails) CreateCommonGraphScanParams() *scangraph.CommonGraphScanParams { - commonParams := &scangraph.CommonGraphScanParams{ - RepoPath: sc.RepoPath, - Watches: sc.Watches, - ScanType: sc.ScanType, - } - if sc.ProjectKey == "" { - commonParams.ProjectKey = os.Getenv(coreutils.Project) - } else { - commonParams.ProjectKey = sc.ProjectKey - } - commonParams.IncludeVulnerabilities = sc.IncludeVulnerabilities - commonParams.IncludeLicenses = sc.IncludeLicenses - return commonParams -} - -func (sc *ScanDetails) HasViolationContext() bool { - return sc.ProjectKey != "" || len(sc.Watches) > 0 || sc.RepoPath != "" -} - -func createXrayScanParams(watches []string, project string, includeLicenses bool) (params *services.XrayGraphScanParams) { - params = &services.XrayGraphScanParams{ - ScanType: services.Dependency, - IncludeLicenses: includeLicenses, - } - if len(watches) > 0 { - params.Watches = watches - return - } - if project != "" { - params.ProjectKey = project - return - } - params.IncludeVulnerabilities = true - return -} - func (sc *ScanDetails) RunInstallAndAudit(workDirs ...string) (auditResults *results.SecurityCommandResults) { auditBasicParams := (&utils.AuditBasicParams{}). SetXrayVersion(sc.XrayVersion). @@ -198,7 +170,7 @@ func (sc *ScanDetails) RunInstallAndAudit(workDirs ...string) (auditResults *res SetMinSeverityFilter(sc.MinSeverityFilter()). SetFixableOnly(sc.FixableOnly()). SetGraphBasicParams(auditBasicParams). - SetCommonGraphScanParams(sc.CreateCommonGraphScanParams()). + SetResultsContext(sc.ResultContext). SetConfigProfile(sc.configProfile). SetMultiScanId(sc.MultiScanId). SetStartTime(sc.StartTime) @@ -220,7 +192,7 @@ func (sc *ScanDetails) SetXscGitInfoContext(scannedBranch, gitProject string, cl // ScannedBranch - name of the branch we are scanning. // GitProject - [Optional] relevant for azure repos and Bitbucket server. // Client vscClient -func (sc *ScanDetails) createGitInfoContext(scannedBranch, gitProject string, client vcsclient.VcsClient) (gitInfo *services.XscGitInfoContext, err error) { +func (sc *ScanDetails) createGitInfoContext(scannedBranch, gitProject string, client vcsclient.VcsClient) (gitInfo *xscservices.XscGitInfoContext, err error) { latestCommit, err := client.GetLatestCommit(context.Background(), sc.RepoOwner, sc.RepoName, scannedBranch) if err != nil { return nil, fmt.Errorf("failed getting latest commit, repository: %s, branch: %s. error: %s ", sc.RepoName, scannedBranch, err.Error()) @@ -229,17 +201,17 @@ func (sc *ScanDetails) createGitInfoContext(scannedBranch, gitProject string, cl if gitProject == "" { gitProject = sc.RepoOwner } - gitInfo = &services.XscGitInfoContext{ + gitInfo = &xscservices.XscGitInfoContext{ // Use Clone URLs as Repo Url, on browsers it will redirect to repository URLS. - GitRepoUrl: sc.Git.RepositoryCloneUrl, - GitRepoName: sc.RepoName, - GitProvider: sc.GitProvider.String(), - GitProject: gitProject, - BranchName: scannedBranch, - LastCommit: latestCommit.Url, - CommitHash: latestCommit.Hash, - CommitMessage: latestCommit.Message, - CommitAuthor: latestCommit.AuthorName, + GitRepoHttpsCloneUrl: sc.Git.RepositoryCloneUrl, + GitRepoName: sc.RepoName, + GitProvider: sc.Git.GitProvider.String(), + GitProject: gitProject, + BranchName: scannedBranch, + LastCommitUrl: latestCommit.Url, + LastCommitHash: latestCommit.Hash, + LastCommitMessage: latestCommit.Message, + LastCommitAuthor: latestCommit.AuthorName, } return } diff --git a/utils/scandetails_test.go b/utils/scandetails_test.go index 7791fc7d1..36f486e8f 100644 --- a/utils/scandetails_test.go +++ b/utils/scandetails_test.go @@ -1,34 +1,11 @@ package utils import ( - "github.com/stretchr/testify/assert" "path/filepath" "testing" -) - -func TestCreateXrayScanParams(t *testing.T) { - // Project - scanDetails := &ScanDetails{} - scanDetails.SetXrayGraphScanParams(nil, "", false) - assert.Empty(t, scanDetails.Watches) - assert.Equal(t, "", scanDetails.ProjectKey) - assert.True(t, scanDetails.IncludeVulnerabilities) - assert.False(t, scanDetails.IncludeLicenses) - - // Watches - scanDetails.SetXrayGraphScanParams([]string{"watch-1", "watch-2"}, "", false) - assert.Equal(t, []string{"watch-1", "watch-2"}, scanDetails.Watches) - assert.Equal(t, "", scanDetails.ProjectKey) - assert.False(t, scanDetails.IncludeVulnerabilities) - assert.False(t, scanDetails.IncludeLicenses) - // Project - scanDetails.SetXrayGraphScanParams(nil, "project", true) - assert.Empty(t, scanDetails.Watches) - assert.Equal(t, "project", scanDetails.ProjectKey) - assert.False(t, scanDetails.IncludeVulnerabilities) - assert.True(t, scanDetails.IncludeLicenses) -} + "github.com/stretchr/testify/assert" +) func TestGetFullPathWorkingDirs(t *testing.T) { sampleProject := Project{ diff --git a/utils/utils.go b/utils/utils.go index 08d8cf35c..388484533 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -13,6 +13,7 @@ import ( "strings" "sync" + "github.com/jfrog/frogbot/v2/utils/issues" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/gofrog/version" "github.com/jfrog/jfrog-cli-core/v2/common/commands" @@ -222,8 +223,8 @@ func VulnerabilityDetailsToMD5Hash(vulnerabilities ...formats.VulnerabilityOrVio return hex.EncodeToString(hash.Sum(nil)), nil } -func UploadSarifResultsToGithubSecurityTab(scanResults *results.SecurityCommandResults, repo *Repository, branch string, client vcsclient.VcsClient, hasViolationContext bool) error { - report, err := GenerateFrogbotSarifReport(scanResults, scanResults.HasMultipleTargets(), hasViolationContext, repo.AllowedLicenses) +func UploadSarifResultsToGithubSecurityTab(scanResults *results.SecurityCommandResults, repo *Repository, branch string, client vcsclient.VcsClient) error { + report, err := GenerateFrogbotSarifReport(scanResults, repo.AllowedLicenses) if err != nil { return err } @@ -235,11 +236,10 @@ func UploadSarifResultsToGithubSecurityTab(scanResults *results.SecurityCommandR return nil } -func GenerateFrogbotSarifReport(extendedResults *results.SecurityCommandResults, isMultipleRoots, hasViolationContext bool, allowedLicenses []string) (string, error) { +func GenerateFrogbotSarifReport(extendedResults *results.SecurityCommandResults, allowedLicenses []string) (string, error) { convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ - IncludeVulnerabilities: true, - HasViolationContext: hasViolationContext, - IsMultipleRoots: &isMultipleRoots, + IncludeVulnerabilities: extendedResults.IncludesVulnerabilities(), + HasViolationContext: extendedResults.HasViolationContext(), AllowedLicenses: allowedLicenses, }) sarifReport, err := convertor.ConvertToSarif(extendedResults) @@ -336,11 +336,15 @@ func GetVulnerabiltiesUniqueID(vulnerability formats.VulnerabilityOrViolationRow len(vulnerability.FixedVersions) > 0) } -func ConvertSarifPathsToRelative(issues *IssuesCollection, workingDirs ...string) { - convertSarifPathsInCveApplicability(issues.Vulnerabilities, workingDirs...) - convertSarifPathsInIacs(issues.Iacs, workingDirs...) - convertSarifPathsInSecrets(issues.Secrets, workingDirs...) - convertSarifPathsInSast(issues.Sast, workingDirs...) +func ConvertSarifPathsToRelative(issues *issues.ScansIssuesCollection, workingDirs ...string) { + convertSarifPathsInCveApplicability(issues.ScaVulnerabilities, workingDirs...) + convertSarifPathsInIacs(issues.IacVulnerabilities, workingDirs...) + convertSarifPathsInSecrets(issues.SecretsVulnerabilities, workingDirs...) + convertSarifPathsInSast(issues.SastVulnerabilities, workingDirs...) + convertSarifPathsInCveApplicability(issues.ScaViolations, workingDirs...) + convertSarifPathsInIacs(issues.IacViolations, workingDirs...) + convertSarifPathsInSecrets(issues.SecretsViolations, workingDirs...) + convertSarifPathsInSast(issues.SastViolations, workingDirs...) } func convertSarifPathsInCveApplicability(vulnerabilities []formats.VulnerabilityOrViolationRow, workingDirs ...string) {