diff --git a/scanpullrequest/scanallpullrequests.go b/scanpullrequest/scanallpullrequests.go index 573094a5a..51f5f0008 100644 --- a/scanpullrequest/scanallpullrequests.go +++ b/scanpullrequest/scanallpullrequests.go @@ -4,9 +4,10 @@ import ( "context" "errors" "fmt" + "github.com/jfrog/frogbot/utils" + "github.com/jfrog/frogbot/utils/outputwriter" "github.com/jfrog/jfrog-client-go/utils/log" - "strings" "github.com/jfrog/froggit-go/vcsclient" ) @@ -64,18 +65,14 @@ func shouldScanPullRequest(repo utils.Repository, client vcsclient.VcsClient, pr for _, comment := range pullRequestsComments { // If this a 're-scan' request comment - if isFrogbotRescanComment(comment.Content) { + if utils.IsFrogbotRescanComment(comment.Content) { return true, nil } // if this is a Frogbot 'scan results' comment and not 're-scan' request comment, do not scan this pull request. - if repo.OutputWriter.IsFrogbotResultComment(comment.Content) { + if outputwriter.IsFrogbotSummaryComment(repo.OutputWriter, comment.Content) { return false, nil } } // This is a new pull request, and it therefore should be scanned. return true, nil } - -func isFrogbotRescanComment(comment string) bool { - return strings.Contains(strings.ToLower(strings.TrimSpace(comment)), utils.RescanRequestComment) -} diff --git a/scanpullrequest/scanallpullrequests_test.go b/scanpullrequest/scanallpullrequests_test.go index 6d4b3b203..3a2fde292 100644 --- a/scanpullrequest/scanallpullrequests_test.go +++ b/scanpullrequest/scanallpullrequests_test.go @@ -16,16 +16,19 @@ import ( "time" ) -var gitParams = &utils.Repository{ - OutputWriter: &outputwriter.SimplifiedOutput{}, - Params: utils.Params{ - Git: utils.Git{ - RepoOwner: "repo-owner", - Branches: []string{"master"}, - RepoName: "repo-name", +var ( + gitParams = &utils.Repository{ + OutputWriter: &outputwriter.SimplifiedOutput{}, + Params: utils.Params{ + Git: utils.Git{ + RepoOwner: "repo-owner", + Branches: []string{"master"}, + RepoName: "repo-name", + }, }, - }, -} + } + allPrIntegrationPath = filepath.Join(outputwriter.TestMessagesDir, "integration") +) type MockParams struct { repoName string @@ -140,13 +143,13 @@ func TestScanAllPullRequestsMultiRepo(t *testing.T) { err := scanAllPullRequestsCmd.Run(configAggregator, client) if assert.NoError(t, err) { assert.Len(t, frogbotMessages, 4) - expectedMessage := "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n
\n\n| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](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 |\n\n
\n\n## šŸ”¬ Research Details\n\n\n**Description:**\n[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.\n\n\nAn incomplete fix for [CVE-2020-7598](https://nvd.nist.gov/vuln/detail/CVE-2020-7598) partially blocked prototype pollution attacks. Researchers discovered that it does not check for constructor functions which means they can be overridden. This behavior can be triggered easily when using it insecurely (which is the common usage). For example:\n```\nvar argv = parse(['--_.concat.constructor.prototype.y', '123']);\nt.equal((function(){}).foo, undefined);\nt.equal(argv.y, undefined);\n```\nIn this example, `prototype.y` is assigned with `123` which will be derived to every newly created object. \n\nThis vulnerability can be triggered when the attacker-controlled input is parsed using Minimist without any validation. As always with prototype pollution, the impact depends on the code that follows the attack, but denial of service is almost always guaranteed.\n**Remediation:**\n##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
" + expectedMessage := outputwriter.GetOutputFromFile(t, filepath.Join(allPrIntegrationPath, "test_proj_with_vulnerability_standard.md")) assert.Equal(t, expectedMessage, frogbotMessages[0]) - expectedMessage = "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
" + expectedMessage = outputwriter.GetPRSummaryContentNoIssues(t, outputwriter.TestSummaryCommentDir, true, false) assert.Equal(t, expectedMessage, frogbotMessages[1]) - expectedMessage = "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n
\n\n| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Undetermined | pip-example:1.2.3 | pyjwt:1.7.1 | [2.4.0] | CVE-2022-29217 |\n\n
\n\n## šŸ”¬ Research Details\n\n\n**Description:**\n[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**.\n\nThe PyJWT library uses the signature-verification algorithm that is specified in the JWT token (that is completely attacker-controlled), however - it requires the validating application to pass an `algorithms` kwarg that specifies the expected algorithms in order to avoid key confusion. Unfortunately - a non-default value `algorithms=jwt.algorithms.get_default_algorithms()` exists that allows all algorithms.\nThe PyJWT library also tries to mitigate key confusions in this case, by making sure that public keys are not used as an HMAC secret. For example, HMAC secrets that begin with `-----BEGIN PUBLIC KEY-----` are rejected when encoding a JWT.\n\nIt has been discovered that due to missing key-type checks, in cases where -\n1. The vulnerable application expects to receive a JWT signed with an Elliptic-Curve key (one of the algorithms `ES256`, `ES384`, `ES512`, `EdDSA`)\n2. The vulnerable application decodes the JWT token using the non-default kwarg `algorithms=jwt.algorithms.get_default_algorithms()` (or alternatively, `algorithms` contain both an HMAC-based algorithm and an EC-based algorithm)\n\nAn attacker can create an HMAC-signed (ex. `HS256`) JWT token, using the (well-known!) EC public key as the HMAC key. The validating application will accept this JWT token as a valid token.\n\nFor example, an application might have planned to validate an `EdDSA`-signed token that was generated as follows -\n```python\n# Making a good jwt token that should work by signing it with the private key\nencoded_good = jwt.encode({\"test\": 1234}, priv_key_bytes, algorithm=\"EdDSA\")\n```\nAn attacker in posession of the public key can generate an `HMAC`-signed token to confuse PyJWT - \n```python\n# Using HMAC with the public key to trick the receiver to think that the public key is a HMAC secret\nencoded_bad = jwt.encode({\"test\": 1234}, pub_key_bytes, algorithm=\"HS256\")\n```\n\nThe following vulnerable `decode` call will accept BOTH of the above tokens as valid - \n```\ndecoded = jwt.decode(encoded_good, pub_key_bytes, \nalgorithms=jwt.algorithms.get_default_algorithms())\n```\n**Remediation:**\n##### Development mitigations\n\nUse a specific algorithm instead of `jwt.algorithms.get_default_algorithms`.\nFor example, replace the following call - \n`jwt.decode(encoded_jwt, pub_key_bytes, algorithms=jwt.algorithms.get_default_algorithms())`\nWith -\n`jwt.decode(encoded_jwt, pub_key_bytes, algorithms=[\"ES256\"])`\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
" + expectedMessage = outputwriter.GetOutputFromFile(t, filepath.Join(allPrIntegrationPath, "test_proj_pip_with_vulnerability.md")) assert.Equal(t, expectedMessage, frogbotMessages[2]) - expectedMessage = "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
" + expectedMessage = outputwriter.GetPRSummaryContentNoIssues(t, outputwriter.TestSummaryCommentDir, true, false) assert.Equal(t, expectedMessage, frogbotMessages[3]) } } @@ -182,9 +185,9 @@ func TestScanAllPullRequests(t *testing.T) { err := scanAllPullRequestsCmd.Run(paramsAggregator, client) assert.NoError(t, err) assert.Len(t, frogbotMessages, 2) - expectedMessage := "**šŸšØ Frogbot scanned this pull request and found the below:**\n\n---\n## šŸ“¦ Vulnerable Dependencies\n---\n\n### āœļø Summary\n\n| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| Critical | Not Applicable | minimist:1.2.5 | minimist:1.2.5 | [0.2.4], [1.2.6] | CVE-2021-44906 |\n\n---\n## šŸ”¬ Research Details\n---\n\n\n#### [ CVE-2021-44906 ] minimist 1.2.5\n\n\n**Description:**\n[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.\n\n\nAn incomplete fix for [CVE-2020-7598](https://nvd.nist.gov/vuln/detail/CVE-2020-7598) partially blocked prototype pollution attacks. Researchers discovered that it does not check for constructor functions which means they can be overridden. This behavior can be triggered easily when using it insecurely (which is the common usage). For example:\n```\nvar argv = parse(['--_.concat.constructor.prototype.y', '123']);\nt.equal((function(){}).foo, undefined);\nt.equal(argv.y, undefined);\n```\nIn this example, `prototype.y` is assigned with `123` which will be derived to every newly created object. \n\nThis vulnerability can be triggered when the attacker-controlled input is parsed using Minimist without any validation. As always with prototype pollution, the impact depends on the code that follows the attack, but denial of service is almost always guaranteed.\n**Remediation:**\n##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.\n\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)" + expectedMessage := outputwriter.GetOutputFromFile(t, filepath.Join(allPrIntegrationPath, "test_proj_with_vulnerability_simplified.md")) assert.Equal(t, expectedMessage, frogbotMessages[0]) - expectedMessage = "**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.** \n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)" + expectedMessage = outputwriter.GetPRSummaryContentNoIssues(t, outputwriter.TestSummaryCommentDir, true, true) assert.Equal(t, expectedMessage, frogbotMessages[1]) } diff --git a/scanpullrequest/scanpullrequest.go b/scanpullrequest/scanpullrequest.go index 0dbff7131..0a0a01f76 100644 --- a/scanpullrequest/scanpullrequest.go +++ b/scanpullrequest/scanpullrequest.go @@ -4,9 +4,10 @@ import ( "context" "errors" "fmt" - "golang.org/x/exp/slices" "os" + "golang.org/x/exp/slices" + "github.com/jfrog/frogbot/utils" "github.com/jfrog/froggit-go/vcsclient" "github.com/jfrog/froggit-go/vcsutils" @@ -104,6 +105,8 @@ func scanPullRequest(repo *utils.Repository, client vcsclient.VcsClient) (err er return } } + + // Handle PR comments for scan output if err = utils.HandlePullRequestCommentsAfterScan(issues, repo, client, int(pullRequestDetails.ID)); err != nil { return } diff --git a/scanpullrequest/scanpullrequest_test.go b/scanpullrequest/scanpullrequest_test.go index d5e567910..9ecd68ffe 100644 --- a/scanpullrequest/scanpullrequest_test.go +++ b/scanpullrequest/scanpullrequest_test.go @@ -502,7 +502,6 @@ func TestGetAllIssues(t *testing.T) { EntitledForJas: true, }, } - expectedOutput := &utils.IssuesCollection{ Vulnerabilities: []formats.VulnerabilityOrViolationRow{ { @@ -809,13 +808,10 @@ func createGitLabHandler(t *testing.T, projectName string) http.HandlerFunc { assert.NotEmpty(t, buf.String()) var expectedResponse []byte - switch { - case strings.Contains(projectName, "multi-dir"): - expectedResponse, err = os.ReadFile(filepath.Join("..", "expectedResponseMultiDir.json")) - case strings.Contains(projectName, "pip"): - expectedResponse, err = os.ReadFile(filepath.Join("..", "expectedResponsePip.json")) - default: - expectedResponse, err = os.ReadFile(filepath.Join("..", "expectedResponse.json")) + if strings.Contains(projectName, "multi-dir") { + expectedResponse = outputwriter.GetJsonBodyOutputFromFile(t, filepath.Join("..", "expected_response_multi_dir.md")) + } else { + expectedResponse = outputwriter.GetJsonBodyOutputFromFile(t, filepath.Join("..", "expected_response.md")) } assert.NoError(t, err) assert.JSONEq(t, string(expectedResponse), buf.String()) diff --git a/scanrepository/scanrepository.go b/scanrepository/scanrepository.go index c5797d1fa..d2dee2ad5 100644 --- a/scanrepository/scanrepository.go +++ b/scanrepository/scanrepository.go @@ -363,7 +363,7 @@ func (cfp *ScanRepositoryCmd) preparePullRequestDetails(vulnerabilitiesDetails . return cfp.gitManager.GenerateAggregatedPullRequestTitle(cfp.projectTech), "", nil } vulnerabilitiesRows := utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilitiesDetails) - prBody = cfp.OutputWriter.VulnerabilitiesTitle(false) + "\n" + cfp.OutputWriter.VulnerabilitiesContent(vulnerabilitiesRows) + cfp.OutputWriter.UntitledForJasMsg() + cfp.OutputWriter.Footer() + prBody = utils.GenerateFixPullRequestDetails(vulnerabilitiesRows, cfp.OutputWriter) if cfp.aggregateFixes { var scanHash string if scanHash, err = utils.VulnerabilityDetailsToMD5Hash(vulnerabilitiesRows...); err != nil { diff --git a/scanrepository/scanrepository_test.go b/scanrepository/scanrepository_test.go index f975d99eb..909d95406 100644 --- a/scanrepository/scanrepository_test.go +++ b/scanrepository/scanrepository_test.go @@ -569,85 +569,7 @@ func TestUpdatePackageToFixedVersion(t *testing.T) { func TestGetRemoteBranchScanHash(t *testing.T) { prBody := ` -[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerMR.png)](https://github.com/jfrog/frogbot#readme) -## šŸ“¦ Vulnerable Dependencies - -### āœļø Summary - -
- -| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | -| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | $\color{}{\textsf{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/applicableHighSeverity.png)
High | $\color{}{\textsf{Undetermined}}$ |github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3:v3.5.1 | | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | $\color{}{\textsf{Undetermined}}$ |github.com/nats-io/nats-streaming-server:v0.21.0 | github.com/nats-io/nats-streaming-server:v0.21.0 | [0.24.3] | - -
- -## šŸ‘‡ Details - - -
- github.com/nats-io/nats-streaming-server v0.21.0 -
- -- **Severity** šŸ”„ High -- **Contextual Analysis:** $\color{}{\textsf{Undetermined}}$ -- **Package Name:** github.com/nats-io/nats-streaming-server -- **Current Version:** v0.21.0 -- **Fixed Version:** [0.24.1] -- **CVEs:** CVE-2022-24450 - - -
- - -
- github.com/mholt/archiver/v3 v3.5.1 -
- -- **Severity** šŸ”„ High -- **Contextual Analysis:** $\color{}{\textsf{Undetermined}}$ -- **Package Name:** github.com/mholt/archiver/v3 -- **Current Version:** v3.5.1 - - -
- - -
- github.com/nats-io/nats-streaming-server v0.21.0 -
- -- **Severity** šŸŽƒ Medium -- **Contextual Analysis:** $\color{}{\textsf{Undetermined}}$ -- **Package Name:** github.com/nats-io/nats-streaming-server -- **Current Version:** v0.21.0 -- **Fixed Version:** [0.24.3] -- **CVEs:** CVE-2022-26652 - - -
- - -## šŸ› ļø Infrastructure as Code - -
- - -| SEVERITY | FILE | LINE:COLUMN | FINDING | -| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | test.js | 1:20 | kms_key_id='' was detected | -| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | test2.js | 4:30 | Deprecated TLS version was detected | - -
- - -
- -[JFrog Frogbot](https://github.com/jfrog/frogbot#readme) - -
+a body [Comment]: <> (Checksum: myhash4321) ` @@ -679,7 +601,7 @@ func TestPreparePullRequestDetails(t *testing.T) { SuggestedFixedVersion: "1.0.0", }, } - expectedPrBody := "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n
\n\n\n| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | | package1:1.0.0 | 1.0.0
2.0.0 | CVE-2022-1234 |\n\n
\n\n## šŸ”¬ Research Details\n\n\n**Description:**\nsummary\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
" + expectedPrBody := utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) prTitle, prBody, err := cfp.preparePullRequestDetails(vulnerabilities...) assert.NoError(t, err) assert.Equal(t, "[šŸø Frogbot] Update version of package1 to 1.0.0", prTitle) @@ -698,13 +620,13 @@ func TestPreparePullRequestDetails(t *testing.T) { SuggestedFixedVersion: "2.0.0", }) cfp.aggregateFixes = true - expectedPrBody = "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n
\n\n\n| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | | package1:1.0.0 | 1.0.0
2.0.0 | CVE-2022-1234 |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableCriticalSeverity.png)
Critical | | package2:2.0.0 | 2.0.0
3.0.0 | CVE-2022-4321 |\n\n
\n\n## šŸ”¬ Research Details\n\n
\n [ CVE-2022-1234 ] package1 1.0.0 \n
\n\n**Description:**\nsummary\n\n\n
\n\n\n
\n [ CVE-2022-4321 ] package2 2.0.0 \n
\n\n**Description:**\nsummary\n\n\n
\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
\n\n[comment]: <> (Checksum: bec823edaceb5d0478b789798e819bde)\n" + expectedPrBody = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) + outputwriter.MarkdownComment("Checksum: bec823edaceb5d0478b789798e819bde") prTitle, prBody, err = cfp.preparePullRequestDetails(vulnerabilities...) assert.NoError(t, err) assert.Equal(t, cfp.gitManager.GenerateAggregatedPullRequestTitle([]coreutils.Technology{}), prTitle) assert.Equal(t, expectedPrBody, prBody) cfp.OutputWriter = &outputwriter.SimplifiedOutput{} - expectedPrBody = "**šŸšØ This automated pull request was created by Frogbot and fixes the below:**\n\n\n---\n## šŸ“¦ Vulnerable Dependencies\n---\n\n### āœļø Summary\n\n\n| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| High | | package1:1.0.0 | 1.0.0, 2.0.0 | CVE-2022-1234 |\n| Critical | | package2:2.0.0 | 2.0.0, 3.0.0 | CVE-2022-4321 |\n\n---\n## šŸ”¬ Research Details\n---\n\n\n#### [ CVE-2022-1234 ] package1 1.0.0\n\n\n**Description:**\nsummary\n\n\n#### [ CVE-2022-4321 ] package2 2.0.0\n\n\n**Description:**\nsummary\n\n\n\n---\n**Frogbot** also supports **Contextual Analysis, Secret Detection and IaC Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/xray/) package, which isn't enabled on your system.\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n[comment]: <> (Checksum: bec823edaceb5d0478b789798e819bde)\n" + expectedPrBody = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) + outputwriter.MarkdownComment("Checksum: bec823edaceb5d0478b789798e819bde") prTitle, prBody, err = cfp.preparePullRequestDetails(vulnerabilities...) assert.NoError(t, err) assert.Equal(t, cfp.gitManager.GenerateAggregatedPullRequestTitle([]coreutils.Technology{}), prTitle) diff --git a/testdata/messages/integration/test_proj_pip_with_vulnerability.md b/testdata/messages/integration/test_proj_pip_with_vulnerability.md new file mode 100644 index 000000000..a9c1b05d0 --- /dev/null +++ b/testdata/messages/integration/test_proj_pip_with_vulnerability.md @@ -0,0 +1,63 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +## šŸ“¦ Vulnerable Dependencies +### āœļø Summary +
+ +| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Undetermined | pip-example:1.2.3 | pyjwt 1.7.1 | [2.4.0] | CVE-2022-29217 | + +
+ +### šŸ”¬ 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**. + +The PyJWT library uses the signature-verification algorithm that is specified in the JWT token (that is completely attacker-controlled), however - it requires the validating application to pass an `algorithms` kwarg that specifies the expected algorithms in order to avoid key confusion. Unfortunately - a non-default value `algorithms=jwt.algorithms.get_default_algorithms()` exists that allows all algorithms. +The PyJWT library also tries to mitigate key confusions in this case, by making sure that public keys are not used as an HMAC secret. For example, HMAC secrets that begin with `-----BEGIN PUBLIC KEY-----` are rejected when encoding a JWT. + +It has been discovered that due to missing key-type checks, in cases where - +1. The vulnerable application expects to receive a JWT signed with an Elliptic-Curve key (one of the algorithms `ES256`, `ES384`, `ES512`, `EdDSA`) +2. The vulnerable application decodes the JWT token using the non-default kwarg `algorithms=jwt.algorithms.get_default_algorithms()` (or alternatively, `algorithms` contain both an HMAC-based algorithm and an EC-based algorithm) + +An attacker can create an HMAC-signed (ex. `HS256`) JWT token, using the (well-known!) EC public key as the HMAC key. The validating application will accept this JWT token as a valid token. + +For example, an application might have planned to validate an `EdDSA`-signed token that was generated as follows - +```python +# Making a good jwt token that should work by signing it with the private key +encoded_good = jwt.encode({"test": 1234}, priv_key_bytes, algorithm="EdDSA") +``` +An attacker in posession of the public key can generate an `HMAC`-signed token to confuse PyJWT - +```python +# Using HMAC with the public key to trick the receiver to think that the public key is a HMAC secret +encoded_bad = jwt.encode({"test": 1234}, pub_key_bytes, algorithm="HS256") +``` + +The following vulnerable `decode` call will accept BOTH of the above tokens as valid - +``` +decoded = jwt.decode(encoded_good, pub_key_bytes, +algorithms=jwt.algorithms.get_default_algorithms()) +``` + +**Remediation:** +##### Development mitigations + +Use a specific algorithm instead of `jwt.algorithms.get_default_algorithms`. +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"])` + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/integration/test_proj_with_vulnerability_simplified.md b/testdata/messages/integration/test_proj_with_vulnerability_simplified.md new file mode 100644 index 000000000..fff0f3372 --- /dev/null +++ b/testdata/messages/integration/test_proj_with_vulnerability_simplified.md @@ -0,0 +1,42 @@ +**šŸšØ Frogbot scanned this pull request and found the below:** + + +--- +## šŸ“¦ Vulnerable Dependencies + +--- + +--- +### āœļø Summary + +--- +| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Critical | Not Applicable | minimist:1.2.5 | minimist 1.2.5 | [0.2.4], [1.2.6] | CVE-2021-44906 | + +--- +### šŸ”¬ 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. + + +An incomplete fix for [CVE-2020-7598](https://nvd.nist.gov/vuln/detail/CVE-2020-7598) partially blocked prototype pollution attacks. Researchers discovered that it does not check for constructor functions which means they can be overridden. This behavior can be triggered easily when using it insecurely (which is the common usage). For example: +``` +var argv = parse(['--_.concat.constructor.prototype.y', '123']); +t.equal((function(){}).foo, undefined); +t.equal(argv.y, undefined); +``` +In this example, `prototype.y` is assigned with `123` which will be derived to every newly created object. + +This vulnerability can be triggered when the attacker-controlled input is parsed using Minimist without any validation. As always with prototype pollution, the impact depends on the code that follows the attack, but denial of service is almost always guaranteed. + +**Remediation:** +##### Development mitigations + +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://github.com/jfrog/frogbot#readme) \ 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 new file mode 100644 index 000000000..79d592593 --- /dev/null +++ b/testdata/messages/integration/test_proj_with_vulnerability_standard.md @@ -0,0 +1,44 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +## šŸ“¦ Vulnerable Dependencies +### āœļø Summary +
+ +| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | + +
+ +### šŸ”¬ 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. + + +An incomplete fix for [CVE-2020-7598](https://nvd.nist.gov/vuln/detail/CVE-2020-7598) partially blocked prototype pollution attacks. Researchers discovered that it does not check for constructor functions which means they can be overridden. This behavior can be triggered easily when using it insecurely (which is the common usage). For example: +``` +var argv = parse(['--_.concat.constructor.prototype.y', '123']); +t.equal((function(){}).foo, undefined); +t.equal(argv.y, undefined); +``` +In this example, `prototype.y` is assigned with `123` which will be derived to every newly created object. + +This vulnerability can be triggered when the attacker-controlled input is parsed using Minimist without any validation. As always with prototype pollution, the impact depends on the code that follows the attack, but denial of service is almost always guaranteed. + +**Remediation:** +##### Development mitigations + +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://github.com/jfrog/frogbot#readme) + +
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 new file mode 100644 index 000000000..a372250ce --- /dev/null +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_simplified.md @@ -0,0 +1,21 @@ + + +--- +## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability + +--- +| Severity | Impacted Dependency | Finding | CVE | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Critical | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | CVE-2022-29361 | + +--- +### 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 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 new file mode 100644 index 000000000..610aeac07 --- /dev/null +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_no_remediation_standard.md @@ -0,0 +1,25 @@ + +## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability +
+ +| Severity | Impacted Dependency | Finding | CVE | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | + +
+ +
+ 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 + +
diff --git a/testdata/messages/reviewcomment/applicable/applicable_review_content_simplified.md b/testdata/messages/reviewcomment/applicable/applicable_review_content_simplified.md new file mode 100644 index 000000000..3e04f85ba --- /dev/null +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_simplified.md @@ -0,0 +1,27 @@ + + +--- +## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability + +--- +| Severity | Impacted Dependency | Finding | CVE | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| Critical | werkzeug:1.0.1 | The vulnerable function flask.Flask.run is called | CVE-2022-29361 | + +--- +### 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 diff --git a/testdata/messages/reviewcomment/applicable/applicable_review_content_standard.md b/testdata/messages/reviewcomment/applicable/applicable_review_content_standard.md new file mode 100644 index 000000000..6cc4fe622 --- /dev/null +++ b/testdata/messages/reviewcomment/applicable/applicable_review_content_standard.md @@ -0,0 +1,33 @@ + +## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability +
+ +| Severity | Impacted Dependency | Finding | CVE | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | + +
+ +
+ 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 + +
diff --git a/testdata/messages/reviewcomment/iac/iac_review_content_simplified.md b/testdata/messages/reviewcomment/iac/iac_review_content_simplified.md new file mode 100644 index 000000000..c526eb6f4 --- /dev/null +++ b/testdata/messages/reviewcomment/iac/iac_review_content_simplified.md @@ -0,0 +1,24 @@ + + +--- +## šŸ› ļø Infrastructure as Code Vulnerability + +--- +| Severity | Finding | +| :---------------------: | :-----------------------------------: | +| 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 + } +} +``` diff --git a/testdata/messages/reviewcomment/iac/iac_review_content_standard.md b/testdata/messages/reviewcomment/iac/iac_review_content_standard.md new file mode 100644 index 000000000..7c96b0682 --- /dev/null +++ b/testdata/messages/reviewcomment/iac/iac_review_content_standard.md @@ -0,0 +1,27 @@ + +## šŸ› ļø Infrastructure as Code Vulnerability +
+ +| Severity | Finding | +| :---------------------: | :-----------------------------------: | +| ![](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` + +Vulnerable example - +``` +resource "google_container_node_pool" "vulnerable_example" { + management { + auto_upgrade = false + } +} +``` + + +
diff --git a/testdata/messages/reviewcomment/review_comment_fallback_simplified.md b/testdata/messages/reviewcomment/review_comment_fallback_simplified.md new file mode 100644 index 000000000..117e0f3c0 --- /dev/null +++ b/testdata/messages/reviewcomment/review_comment_fallback_simplified.md @@ -0,0 +1,15 @@ + + +[comment]: <> (FrogbotReviewComment) + +``` +snippet +``` +at `file` (line 11) + +``` +some review content +``` + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ No newline at end of file diff --git a/testdata/messages/reviewcomment/review_comment_fallback_standard.md b/testdata/messages/reviewcomment/review_comment_fallback_standard.md new file mode 100644 index 000000000..742987b5d --- /dev/null +++ b/testdata/messages/reviewcomment/review_comment_fallback_standard.md @@ -0,0 +1,19 @@ + + +[comment]: <> (FrogbotReviewComment) + +``` +snippet +``` +at `file` (line 11) + +``` +some review content +``` + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/reviewcomment/review_comment_simplified.md b/testdata/messages/reviewcomment/review_comment_simplified.md new file mode 100644 index 000000000..8dcff0ed6 --- /dev/null +++ b/testdata/messages/reviewcomment/review_comment_simplified.md @@ -0,0 +1,11 @@ + + +[comment]: <> (FrogbotReviewComment) + + +``` +some review content +``` + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ No newline at end of file diff --git a/testdata/messages/reviewcomment/review_comment_standard.md b/testdata/messages/reviewcomment/review_comment_standard.md new file mode 100644 index 000000000..c9ef167d8 --- /dev/null +++ b/testdata/messages/reviewcomment/review_comment_standard.md @@ -0,0 +1,15 @@ + + +[comment]: <> (FrogbotReviewComment) + + +``` +some review content +``` + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
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 new file mode 100644 index 000000000..73567b6bd --- /dev/null +++ b/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_simplified.md @@ -0,0 +1,21 @@ + + +--- +## šŸŽÆ Static Application Security Testing (SAST) Vulnerability + +--- +| Severity | Finding | +| :---------------------: | :-----------------------------------: | +| Low | Stack Trace Exposure | + +--- +### 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 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 new file mode 100644 index 000000000..eb2cffdbc --- /dev/null +++ b/testdata/messages/reviewcomment/sast/sast_review_content_no_code_flow_standard.md @@ -0,0 +1,23 @@ + +## šŸŽÆ Static Application Security Testing (SAST) Vulnerability +
+ +| Severity | Finding | +| :---------------------: | :-----------------------------------: | +| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Stack Trace Exposure | + +
+ +
+ 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. + +
diff --git a/testdata/messages/reviewcomment/sast/sast_review_content_simplified.md b/testdata/messages/reviewcomment/sast/sast_review_content_simplified.md new file mode 100644 index 000000000..366c7c4c2 --- /dev/null +++ b/testdata/messages/reviewcomment/sast/sast_review_content_simplified.md @@ -0,0 +1,46 @@ + + +--- +## šŸŽÆ Static Application Security Testing (SAST) Vulnerability + +--- +| Severity | Finding | +| :---------------------: | :-----------------------------------: | +| Low | Stack Trace Exposure | + +--- +### 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. + +--- +### 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_review_content_standard.md b/testdata/messages/reviewcomment/sast/sast_review_content_standard.md new file mode 100644 index 000000000..6794f48e9 --- /dev/null +++ b/testdata/messages/reviewcomment/sast/sast_review_content_standard.md @@ -0,0 +1,55 @@ + +## šŸŽÆ Static Application Security Testing (SAST) Vulnerability +
+ +| Severity | Finding | +| :---------------------: | :-----------------------------------: | +| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Stack Trace Exposure | + +
+ +
+ 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. + +
+ +
+ 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/summarycomment/license/license_violation_simplified.md b/testdata/messages/summarycomment/license/license_violation_simplified.md new file mode 100644 index 000000000..2f8ffa7ba --- /dev/null +++ b/testdata/messages/summarycomment/license/license_violation_simplified.md @@ -0,0 +1,10 @@ + + +--- +## āš–ļø Violated Licenses + +--- +| LICENSE | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | +| License1 | Comp1 1.0 | Dep1 2.0 | +| 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 new file mode 100644 index 000000000..75e420599 --- /dev/null +++ b/testdata/messages/summarycomment/license/license_violation_standard.md @@ -0,0 +1,10 @@ + +## āš–ļø Violated Licenses +
+ +| LICENSE | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | +| License1 | Comp1 1.0 | Dep1 2.0 | +| License2 | root 1.0.0
minimatch 1.2.3 | Dep2 3.0 | + +
diff --git a/testdata/messages/summarycomment/structure/fix_mr_entitled.md b/testdata/messages/summarycomment/structure/fix_mr_entitled.md new file mode 100644 index 000000000..8c9401e14 --- /dev/null +++ b/testdata/messages/summarycomment/structure/fix_mr_entitled.md @@ -0,0 +1,18 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerMR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +some content +``` + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/fix_mr_not_entitled.md b/testdata/messages/summarycomment/structure/fix_mr_not_entitled.md new file mode 100644 index 000000000..bd7c7e4db --- /dev/null +++ b/testdata/messages/summarycomment/structure/fix_mr_not_entitled.md @@ -0,0 +1,25 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerMR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +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. + +
+ + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/fix_pr_entitled.md b/testdata/messages/summarycomment/structure/fix_pr_entitled.md new file mode 100644 index 000000000..ace38b27e --- /dev/null +++ b/testdata/messages/summarycomment/structure/fix_pr_entitled.md @@ -0,0 +1,18 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +some content +``` + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/fix_pr_not_entitled.md b/testdata/messages/summarycomment/structure/fix_pr_not_entitled.md new file mode 100644 index 000000000..3f7deda93 --- /dev/null +++ b/testdata/messages/summarycomment/structure/fix_pr_not_entitled.md @@ -0,0 +1,25 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +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. + +
+ + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/fix_simplified_entitled.md b/testdata/messages/summarycomment/structure/fix_simplified_entitled.md new file mode 100644 index 000000000..dc3f6188e --- /dev/null +++ b/testdata/messages/summarycomment/structure/fix_simplified_entitled.md @@ -0,0 +1,9 @@ +**šŸšØ This automated pull request was created by Frogbot and fixes the below:** + +``` +some content +``` + + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ No newline at end of file diff --git a/testdata/messages/summarycomment/structure/fix_simplified_not_entitled.md b/testdata/messages/summarycomment/structure/fix_simplified_not_entitled.md new file mode 100644 index 000000000..dead95a2c --- /dev/null +++ b/testdata/messages/summarycomment/structure/fix_simplified_not_entitled.md @@ -0,0 +1,11 @@ +**šŸšØ This automated pull request was created by Frogbot and fixes the below:** + +``` +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. + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ No newline at end of file diff --git a/testdata/messages/summarycomment/structure/summary_comment_mr_issues_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_mr_issues_entitled.md new file mode 100644 index 000000000..2a3feeeb7 --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_mr_issues_entitled.md @@ -0,0 +1,18 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerMR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +some content +``` + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/summary_comment_mr_issues_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_mr_issues_not_entitled.md new file mode 100644 index 000000000..908833538 --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_mr_issues_not_entitled.md @@ -0,0 +1,25 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerMR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +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. + +
+ + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/summary_comment_mr_no_issues_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_mr_no_issues_entitled.md new file mode 100644 index 000000000..cba88c8be --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_mr_no_issues_entitled.md @@ -0,0 +1,14 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerMR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/novulnerabilitiesMR.md b/testdata/messages/summarycomment/structure/summary_comment_mr_no_issues_not_entitled.md old mode 100755 new mode 100644 similarity index 56% rename from testdata/messages/novulnerabilitiesMR.md rename to testdata/messages/summarycomment/structure/summary_comment_mr_no_issues_not_entitled.md index 2f719f82e..ee997a3fe --- a/testdata/messages/novulnerabilitiesMR.md +++ b/testdata/messages/summarycomment/structure/summary_comment_mr_no_issues_not_entitled.md @@ -6,15 +6,16 @@ --- -
+
-**Frogbot** also supports **Contextual Analysis, Secret Detection and IaC Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/xray/) package, which isn't enabled on your system. +**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.
+ --- -
+
[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) -
\ No newline at end of file +
diff --git a/testdata/messages/summarycomment/structure/summary_comment_pr_issues_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_pr_issues_entitled.md new file mode 100644 index 000000000..25df27b7c --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_pr_issues_entitled.md @@ -0,0 +1,18 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +some content +``` + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/summary_comment_pr_issues_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_pr_issues_not_entitled.md new file mode 100644 index 000000000..98f70eebb --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_pr_issues_not_entitled.md @@ -0,0 +1,25 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +``` +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. + +
+ + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/summarycomment/structure/summary_comment_pr_no_issues_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_pr_no_issues_entitled.md new file mode 100644 index 000000000..452f6dd08 --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_pr_no_issues_entitled.md @@ -0,0 +1,14 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/messages/novulnerabilities.md b/testdata/messages/summarycomment/structure/summary_comment_pr_no_issues_not_entitled.md old mode 100755 new mode 100644 similarity index 56% rename from testdata/messages/novulnerabilities.md rename to testdata/messages/summarycomment/structure/summary_comment_pr_no_issues_not_entitled.md index 51aa43540..9807b4034 --- a/testdata/messages/novulnerabilities.md +++ b/testdata/messages/summarycomment/structure/summary_comment_pr_no_issues_not_entitled.md @@ -6,15 +6,16 @@ --- -
+
-**Frogbot** also supports **Contextual Analysis, Secret Detection and IaC Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/xray/) package, which isn't enabled on your system. +**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.
+ --- -
+
[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) -
\ No newline at end of file +
diff --git a/testdata/messages/summarycomment/structure/summary_comment_simplified_issues_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_simplified_issues_entitled.md new file mode 100644 index 000000000..d12ded940 --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_simplified_issues_entitled.md @@ -0,0 +1,9 @@ +**šŸšØ Frogbot scanned this pull request and found the below:** + +``` +some content +``` + + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ No newline at end of file diff --git a/testdata/messages/summarycomment/structure/summary_comment_simplified_issues_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_simplified_issues_not_entitled.md new file mode 100644 index 000000000..77eb27844 --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_simplified_issues_not_entitled.md @@ -0,0 +1,11 @@ +**šŸšØ Frogbot scanned this pull request and found the below:** + +``` +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. + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ No newline at end of file diff --git a/testdata/messages/summarycomment/structure/summary_comment_simplified_no_issues_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_simplified_no_issues_entitled.md new file mode 100644 index 000000000..cd3afb8fe --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_simplified_no_issues_entitled.md @@ -0,0 +1,5 @@ +**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.** + + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ No newline at end of file diff --git a/testdata/messages/summarycomment/structure/summary_comment_simplified_no_issues_not_entitled.md b/testdata/messages/summarycomment/structure/summary_comment_simplified_no_issues_not_entitled.md new file mode 100644 index 000000000..7b7cfdd40 --- /dev/null +++ b/testdata/messages/summarycomment/structure/summary_comment_simplified_no_issues_not_entitled.md @@ -0,0 +1,7 @@ +**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.** + +--- +**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. + +--- +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) \ 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 new file mode 100644 index 000000000..1f39c3932 --- /dev/null +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_simplified.md @@ -0,0 +1,14 @@ + + +--- +## šŸ“¦ Vulnerable Dependencies + +--- + +--- +### āœļø Summary + +--- +| 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 | \ 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 new file mode 100644 index 000000000..8fa3327c6 --- /dev/null +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_no_details_standard.md @@ -0,0 +1,10 @@ + +## šŸ“¦ Vulnerable Dependencies +### āœļø Summary +
+ +| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | + +
diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md new file mode 100644 index 000000000..8de1d30fe --- /dev/null +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_simplified.md @@ -0,0 +1,24 @@ + + +--- +## šŸ“¦ Vulnerable Dependencies + +--- + +--- +### āœļø Summary + +--- +| 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 + +--- +**Description:** +Research CVE-2022-26652 details + +**Remediation:** +some remediation \ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md new file mode 100644 index 000000000..121f04dac --- /dev/null +++ b/testdata/messages/summarycomment/vulnerabilities/one_vulnerability_standard.md @@ -0,0 +1,17 @@ + +## šŸ“¦ Vulnerable Dependencies +### āœļø Summary +
+ +| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | + +
+ +### šŸ”¬ Research Details +**Description:** +Research CVE-2022-26652 details + +**Remediation:** +some remediation \ No newline at end of file diff --git a/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md new file mode 100644 index 000000000..ef8bafb51 --- /dev/null +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_simplified.md @@ -0,0 +1,49 @@ + + +--- +## šŸ“¦ Vulnerable Dependencies + +--- + +--- +### āœļø Summary + +--- +| 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 + +--- + +**Description:** +Summary XRAY-122345 + +**Remediation:** +some remediation + +--- +#### [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 + +--- + +**Remediation:** +some remediation + +--- +#### github.com/mholt/archiver/v3 v3.5.1 + +--- + +**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 new file mode 100644 index 000000000..95e8435e4 --- /dev/null +++ b/testdata/messages/summarycomment/vulnerabilities/vulnerabilities_standard.md @@ -0,0 +1,52 @@ + +## šŸ“¦ Vulnerable Dependencies +### āœļø Summary +
+ +| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | - | - | + +
+ +
+ šŸ”¬ Research Details +
+ +
+ [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 +
+ + +**Description:** +Summary XRAY-122345 + +**Remediation:** +some remediation + +
+ +
+ [ CVE-2022-26652, CVE-2023-4321 ] component-D v0.21.0 +
+ + +**Remediation:** +some remediation + +
+ +
+ github.com/mholt/archiver/v3 v3.5.1 +
+ + +**Description:** +Summary + +
+ +
diff --git a/testdata/scanpullrequest/expectedResponse.json b/testdata/scanpullrequest/expectedResponse.json deleted file mode 100755 index ded9c6bdf..000000000 --- a/testdata/scanpullrequest/expectedResponse.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "body": "\u003cdiv align='center'\u003e\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n\u003c/div\u003e\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n\u003cdiv align=\"center\"\u003e\n\n| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableCritical.png)\u003cbr\u003eCritical | Not Applicable | minimist:1.2.5 | minimist:1.2.5 | [0.2.4]\u003cbr\u003e[1.2.6] | CVE-2021-44906 |\n\n\u003c/div\u003e\n\n## šŸ”¬ Research Details\n\n\n**Description:**\n[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.\n\n\nAn incomplete fix for [CVE-2020-7598](https://nvd.nist.gov/vuln/detail/CVE-2020-7598) partially blocked prototype pollution attacks. Researchers discovered that it does not check for constructor functions which means they can be overridden. This behavior can be triggered easily when using it insecurely (which is the common usage). For example:\n```\nvar argv = parse(['--_.concat.constructor.prototype.y', '123']);\nt.equal((function(){}).foo, undefined);\nt.equal(argv.y, undefined);\n```\nIn this example, `prototype.y` is assigned with `123` which will be derived to every newly created object. \n\nThis vulnerability can be triggered when the attacker-controlled input is parsed using Minimist without any validation. As always with prototype pollution, the impact depends on the code that follows the attack, but denial of service is almost always guaranteed.\n**Remediation:**\n##### Development mitigations\n\nAdd the `Object.freeze(Object.prototype);` directive once at the beginning of your main JS source code file (ex. `index.js`), preferably after all your `require` directives. This will prevent any changes to the prototype object, thus completely negating prototype pollution attacks.\n\n\n---\n\u003cdiv align=\"center\"\u003e\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n\u003c/div\u003e" -} \ No newline at end of file diff --git a/testdata/scanpullrequest/expectedResponseMultiDir.json b/testdata/scanpullrequest/expectedResponseMultiDir.json deleted file mode 100755 index 626ca5f06..000000000 --- a/testdata/scanpullrequest/expectedResponseMultiDir.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "body": "\u003cdiv align='center'\u003e\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n\u003c/div\u003e\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n\u003cdiv align=\"center\"\u003e\n\n| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableHigh.png)\u003cbr\u003e High | Not Applicable | minimatch:3.0.4 | minimatch:3.0.4 | [3.0.5] | CVE-2022-3517 |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)\u003cbr\u003e High | Undetermined | pyjwt:1.7.1 | pyjwt:1.7.1 | [2.4.0] | CVE-2022-29217 |\n\n\u003c/div\u003e\n\n## šŸ”¬ Research Details\n\n\u003cdetails\u003e\n\u003csummary\u003e \u003cb\u003e[ CVE-2022-3517 ] minimatch 3.0.4\u003c/b\u003e \u003c/summary\u003e\n\u003cbr\u003e\n\n**Description:**\nA 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.\n\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e \u003cb\u003e[ CVE-2022-29217 ] pyjwt 1.7.1\u003c/b\u003e \u003c/summary\u003e\n\u003cbr\u003e\n\n**Description:**\n[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**.\n\nThe PyJWT library uses the signature-verification algorithm that is specified in the JWT token (that is completely attacker-controlled), however - it requires the validating application to pass an `algorithms` kwarg that specifies the expected algorithms in order to avoid key confusion. Unfortunately - a non-default value `algorithms=jwt.algorithms.get_default_algorithms()` exists that allows all algorithms.\nThe PyJWT library also tries to mitigate key confusions in this case, by making sure that public keys are not used as an HMAC secret. For example, HMAC secrets that begin with `-----BEGIN PUBLIC KEY-----` are rejected when encoding a JWT.\n\nIt has been discovered that due to missing key-type checks, in cases where -\n1. The vulnerable application expects to receive a JWT signed with an Elliptic-Curve key (one of the algorithms `ES256`, `ES384`, `ES512`, `EdDSA`)\n2. The vulnerable application decodes the JWT token using the non-default kwarg `algorithms=jwt.algorithms.get_default_algorithms()` (or alternatively, `algorithms` contain both an HMAC-based algorithm and an EC-based algorithm)\n\nAn attacker can create an HMAC-signed (ex. `HS256`) JWT token, using the (well-known!) EC public key as the HMAC key. The validating application will accept this JWT token as a valid token.\n\nFor example, an application might have planned to validate an `EdDSA`-signed token that was generated as follows -\n```python\n# Making a good jwt token that should work by signing it with the private key\nencoded_good = jwt.encode({\"test\": 1234}, priv_key_bytes, algorithm=\"EdDSA\")\n```\nAn attacker in posession of the public key can generate an `HMAC`-signed token to confuse PyJWT - \n```python\n# Using HMAC with the public key to trick the receiver to think that the public key is a HMAC secret\nencoded_bad = jwt.encode({\"test\": 1234}, pub_key_bytes, algorithm=\"HS256\")\n```\n\nThe following vulnerable `decode` call will accept BOTH of the above tokens as valid - \n```\ndecoded = jwt.decode(encoded_good, pub_key_bytes, \nalgorithms=jwt.algorithms.get_default_algorithms())\n```\n**Remediation:**\n##### Development mitigations\n\nUse a specific algorithm instead of `jwt.algorithms.get_default_algorithms`.\nFor example, replace the following call - \n`jwt.decode(encoded_jwt, pub_key_bytes, algorithms=jwt.algorithms.get_default_algorithms())`\nWith -\n`jwt.decode(encoded_jwt, pub_key_bytes, algorithms=[\"ES256\"])`\n\n\n\u003c/details\u003e\n\n\n---\n\u003cdiv align=\"center\"\u003e\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n\u003c/div\u003e" -} \ No newline at end of file diff --git a/testdata/scanpullrequest/expectedResponsePip.json b/testdata/scanpullrequest/expectedResponsePip.json deleted file mode 100755 index e9e14af3c..000000000 --- a/testdata/scanpullrequest/expectedResponsePip.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "body": "[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n[What is Frogbot?](https://github.com/jfrog/frogbot#readme)\n\n| SEVERITY | DIRECT DEPENDENCIES | DIRECT DEPENDENCIES VERSIONS | IMPACTED DEPENDENCY NAME | IMPACTED DEPENDENCY VERSION | FIXED VERSIONS | CVE\n:--: | -- | -- | -- | -- | :--: | --\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/highSeverity.png)\u003cbr\u003e High | pyjwt | 1.7.1 | pyjwt | 1.7.1 | [2.4.0] | CVE-2022-29217 " -} \ No newline at end of file diff --git a/testdata/scanpullrequest/expected_response.md b/testdata/scanpullrequest/expected_response.md new file mode 100644 index 000000000..79d592593 --- /dev/null +++ b/testdata/scanpullrequest/expected_response.md @@ -0,0 +1,44 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +## šŸ“¦ Vulnerable Dependencies +### āœļø Summary +
+ +| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | + +
+ +### šŸ”¬ 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. + + +An incomplete fix for [CVE-2020-7598](https://nvd.nist.gov/vuln/detail/CVE-2020-7598) partially blocked prototype pollution attacks. Researchers discovered that it does not check for constructor functions which means they can be overridden. This behavior can be triggered easily when using it insecurely (which is the common usage). For example: +``` +var argv = parse(['--_.concat.constructor.prototype.y', '123']); +t.equal((function(){}).foo, undefined); +t.equal(argv.y, undefined); +``` +In this example, `prototype.y` is assigned with `123` which will be derived to every newly created object. + +This vulnerability can be triggered when the attacker-controlled input is parsed using Minimist without any validation. As always with prototype pollution, the impact depends on the code that follows the attack, but denial of service is almost always guaranteed. + +**Remediation:** +##### Development mitigations + +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://github.com/jfrog/frogbot#readme) + +
diff --git a/testdata/scanpullrequest/expected_response_multi_dir.md b/testdata/scanpullrequest/expected_response_multi_dir.md new file mode 100644 index 000000000..d64ee0d8f --- /dev/null +++ b/testdata/scanpullrequest/expected_response_multi_dir.md @@ -0,0 +1,87 @@ +
+ +[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme) + +
+ + +## šŸ“¦ Vulnerable Dependencies +### āœļø Summary +
+ +| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES | +| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: | +| ![](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 | Undetermined | pyjwt:1.7.1 | pyjwt 1.7.1 | [2.4.0] | CVE-2022-29217 | + +
+ +
+ šŸ”¬ Research Details +
+ +
+ [ 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. + +
+ +
+ [ CVE-2022-29217 ] pyjwt 1.7.1 +
+ + +**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**. + +The PyJWT library uses the signature-verification algorithm that is specified in the JWT token (that is completely attacker-controlled), however - it requires the validating application to pass an `algorithms` kwarg that specifies the expected algorithms in order to avoid key confusion. Unfortunately - a non-default value `algorithms=jwt.algorithms.get_default_algorithms()` exists that allows all algorithms. +The PyJWT library also tries to mitigate key confusions in this case, by making sure that public keys are not used as an HMAC secret. For example, HMAC secrets that begin with `-----BEGIN PUBLIC KEY-----` are rejected when encoding a JWT. + +It has been discovered that due to missing key-type checks, in cases where - +1. The vulnerable application expects to receive a JWT signed with an Elliptic-Curve key (one of the algorithms `ES256`, `ES384`, `ES512`, `EdDSA`) +2. The vulnerable application decodes the JWT token using the non-default kwarg `algorithms=jwt.algorithms.get_default_algorithms()` (or alternatively, `algorithms` contain both an HMAC-based algorithm and an EC-based algorithm) + +An attacker can create an HMAC-signed (ex. `HS256`) JWT token, using the (well-known!) EC public key as the HMAC key. The validating application will accept this JWT token as a valid token. + +For example, an application might have planned to validate an `EdDSA`-signed token that was generated as follows - +```python +# Making a good jwt token that should work by signing it with the private key +encoded_good = jwt.encode({"test": 1234}, priv_key_bytes, algorithm="EdDSA") +``` +An attacker in posession of the public key can generate an `HMAC`-signed token to confuse PyJWT - +```python +# Using HMAC with the public key to trick the receiver to think that the public key is a HMAC secret +encoded_bad = jwt.encode({"test": 1234}, pub_key_bytes, algorithm="HS256") +``` + +The following vulnerable `decode` call will accept BOTH of the above tokens as valid - +``` +decoded = jwt.decode(encoded_good, pub_key_bytes, +algorithms=jwt.algorithms.get_default_algorithms()) +``` + +**Remediation:** +##### Development mitigations + +Use a specific algorithm instead of `jwt.algorithms.get_default_algorithms`. +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"])` + +
+ +
+ + + +--- +
+ +[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme) + +
diff --git a/utils/reviewcomment.go b/utils/comment.go similarity index 78% rename from utils/reviewcomment.go rename to utils/comment.go index a306934d2..92da746fc 100644 --- a/utils/reviewcomment.go +++ b/utils/comment.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "strings" "github.com/jfrog/frogbot/utils/outputwriter" @@ -16,14 +17,16 @@ type ReviewCommentType string type ReviewComment struct { Location formats.Location - CommentInfo vcsclient.PullRequestComment Type ReviewCommentType + CommentInfo vcsclient.PullRequestComment } const ( ApplicableComment ReviewCommentType = "Applicable" IacComment ReviewCommentType = "Iac" SastComment ReviewCommentType = "Sast" + + RescanRequestComment = "rescan" ) func HandlePullRequestCommentsAfterScan(issues *IssuesCollection, repo *Repository, client vcsclient.VcsClient, pullRequestID int) (err error) { @@ -42,7 +45,7 @@ func HandlePullRequestCommentsAfterScan(issues *IssuesCollection, repo *Reposito } // Add summary (SCA, license) scan comment - if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, createPullRequestComment(issues, repo.OutputWriter), pullRequestID); err != nil { + if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, generatePullRequestSummaryComment(issues, repo.OutputWriter), pullRequestID); err != nil { err = errors.New("couldn't add pull request comment: " + err.Error()) return } @@ -67,7 +70,7 @@ func DeleteExistingPullRequestComments(repository *Repository, client vcsclient. commentsToDelete := getFrogbotReviewComments(comments) // Previous Summary comments for _, comment := range comments { - if repository.OutputWriter.IsFrogbotResultComment(comment.Content) { + if outputwriter.IsFrogbotSummaryComment(repository.OutputWriter, comment.Content) { commentsToDelete = append(commentsToDelete, comment) } } @@ -82,18 +85,34 @@ func DeleteExistingPullRequestComments(repository *Repository, client vcsclient. return err } -func createPullRequestComment(issues *IssuesCollection, writer outputwriter.OutputWriter) string { - if !issues.IssuesExists() { - return writer.NoVulnerabilitiesTitle() + writer.UntitledForJasMsg() + writer.Footer() +func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) string { + return outputwriter.GetPRSummaryContent(outputwriter.VulnerabilitiesContent(vulnerabilities, writer), true, false, writer) +} + +func generatePullRequestSummaryComment(issuesCollection *IssuesCollection, writer outputwriter.OutputWriter) string { + issuesExists := issuesCollection.IssuesExists() + content := strings.Builder{} + if issuesExists { + content.WriteString(outputwriter.VulnerabilitiesContent(issuesCollection.Vulnerabilities, writer)) + content.WriteString(outputwriter.LicensesContent(issuesCollection.Licenses, writer)) } - comment := strings.Builder{} - comment.WriteString(writer.VulnerabilitiesTitle(true)) - comment.WriteString(writer.VulnerabilitiesContent(issues.Vulnerabilities)) - comment.WriteString(writer.LicensesContent(issues.Licenses)) - comment.WriteString(writer.UntitledForJasMsg()) - comment.WriteString(writer.Footer()) + return outputwriter.GetPRSummaryContent(content.String(), issuesExists, true, writer) +} - return comment.String() +func IsFrogbotRescanComment(comment string) bool { + return strings.Contains(strings.ToLower(comment), RescanRequestComment) +} + +func GetSortedPullRequestComments(client vcsclient.VcsClient, repoOwner, repoName string, prID int) ([]vcsclient.CommentInfo, error) { + pullRequestsComments, err := client.ListPullRequestComments(context.Background(), repoOwner, repoName, prID) + if err != nil { + return nil, err + } + // Sort the comment according to time created, the newest comment should be the first one. + sort.Slice(pullRequestsComments, func(i, j int) bool { + return pullRequestsComments[i].Created.After(pullRequestsComments[j].Created) + }) + return pullRequestsComments, nil } func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.VcsClient, issues *IssuesCollection) (err error) { @@ -106,7 +125,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, getRegularCommentContent(comment), pullRequestID); err != nil { + if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, outputwriter.GetFallbackReviewCommentContent(comment.CommentInfo.Content, comment.Location, repo.OutputWriter), pullRequestID); err != nil { err = errors.New("couldn't add pull request comment, fallback to comment: " + err.Error()) return } @@ -135,7 +154,7 @@ func DeleteExistingPullRequestReviewComments(repo *Repository, pullRequestID int func getFrogbotReviewComments(existingComments []vcsclient.CommentInfo) (reviewComments []vcsclient.CommentInfo) { for _, comment := range existingComments { - if strings.Contains(comment.Content, outputwriter.ReviewCommentId) { + if outputwriter.IsFrogbotReviewComment(comment.Content) { log.Debug("Deleting comment id:", comment.ID) reviewComments = append(reviewComments, comment) } @@ -143,10 +162,6 @@ func getFrogbotReviewComments(existingComments []vcsclient.CommentInfo) (reviewC return } -func getRegularCommentContent(comment ReviewComment) string { - return outputwriter.MarkdownComment(outputwriter.ReviewCommentId) + outputwriter.GetLocationDescription(comment.Location) + comment.CommentInfo.Content -} - func getNewReviewComments(repo *Repository, issues *IssuesCollection) (commentsToAdd []ReviewComment) { writer := repo.OutputWriter @@ -162,7 +177,6 @@ func getNewReviewComments(repo *Repository, issues *IssuesCollection) (commentsT for _, iac := range issues.Iacs { commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, iac.Location, generateSourceCodeReviewContent(IacComment, iac, writer))) } - for _, sast := range issues.Sast { commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, sast.Location, generateSourceCodeReviewContent(SastComment, sast, writer))) } @@ -188,7 +202,7 @@ func generateApplicabilityReviewContent(issue formats.Evidence, relatedCve forma if relatedVulnerability.JfrogResearchInformation != nil { remediation = relatedVulnerability.JfrogResearchInformation.Remediation } - return outputwriter.GenerateReviewCommentContent(writer.ApplicableCveReviewContent( + return outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent( relatedVulnerability.Severity, issue.Reason, relatedCve.Applicability.ScannerDescription, @@ -196,23 +210,26 @@ func generateApplicabilityReviewContent(issue formats.Evidence, relatedCve forma relatedVulnerability.Summary, fmt.Sprintf("%s:%s", relatedVulnerability.ImpactedDependencyName, relatedVulnerability.ImpactedDependencyVersion), remediation, + writer, ), writer) } func generateSourceCodeReviewContent(commentType ReviewCommentType, issue formats.SourceCodeRow, writer outputwriter.OutputWriter) (content string) { switch commentType { case IacComment: - return outputwriter.GenerateReviewCommentContent(writer.IacReviewContent( + return outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent( issue.Severity, issue.Finding, issue.ScannerDescription, + writer, ), writer) case SastComment: - return outputwriter.GenerateReviewCommentContent(writer.SastReviewContent( + return outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent( issue.Severity, issue.Finding, issue.ScannerDescription, issue.CodeFlow, + writer, ), writer) } return diff --git a/utils/comment_test.go b/utils/comment_test.go new file mode 100644 index 000000000..c0b910bac --- /dev/null +++ b/utils/comment_test.go @@ -0,0 +1,239 @@ +package utils + +import ( + "testing" + + "github.com/jfrog/frogbot/utils/outputwriter" + "github.com/jfrog/froggit-go/vcsclient" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" + "github.com/stretchr/testify/assert" +) + +func TestGetFrogbotReviewComments(t *testing.T) { + testCases := []struct { + name string + existingComments []vcsclient.CommentInfo + expectedOutput []vcsclient.CommentInfo + }{ + { + name: "No frogbot comments", + existingComments: []vcsclient.CommentInfo{ + {Content: outputwriter.FrogbotTitlePrefix}, + {Content: "some comment text" + outputwriter.MarkdownComment("with hidden comment")}, + {Content: outputwriter.CommentGeneratedByFrogbot}, + }, + expectedOutput: []vcsclient.CommentInfo{}, + }, + { + name: "With frogbot comments", + existingComments: []vcsclient.CommentInfo{ + {Content: outputwriter.FrogbotTitlePrefix}, + {Content: outputwriter.MarkdownComment(outputwriter.ReviewCommentId) + "A Frogbot review comment"}, + {Content: "some comment text" + outputwriter.MarkdownComment("with hidden comment")}, + {Content: outputwriter.ReviewCommentId}, + {Content: outputwriter.CommentGeneratedByFrogbot}, + }, + expectedOutput: []vcsclient.CommentInfo{ + {Content: outputwriter.MarkdownComment(outputwriter.ReviewCommentId) + "A Frogbot review comment"}, + {Content: outputwriter.ReviewCommentId}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output := getFrogbotReviewComments(tc.existingComments) + assert.ElementsMatch(t, tc.expectedOutput, output) + }) + } +} + +func TestGetNewReviewComments(t *testing.T) { + repo := &Repository{OutputWriter: &outputwriter.StandardOutput{}} + testCases := []struct { + name string + issues *IssuesCollection + expectedOutput []ReviewComment + }{ + { + name: "No issues for review comments", + issues: &IssuesCollection{ + Vulnerabilities: []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: coreutils.Npm, + }, + }, + Secrets: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + Finding: "Secret", + Location: formats.Location{ + File: "index.js", + StartLine: 5, + StartColumn: 6, + EndLine: 7, + EndColumn: 8, + Snippet: "access token exposed", + }, + }, + }, + }, + expectedOutput: []ReviewComment{}, + }, + { + name: "With issues for review comments", + issues: &IssuesCollection{ + Vulnerabilities: []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", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, + Technology: coreutils.Npm, + }, + }, + Iacs: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + Finding: "Missing auto upgrade was detected", + Location: formats.Location{ + File: "file1", + StartLine: 1, + StartColumn: 10, + EndLine: 2, + EndColumn: 11, + Snippet: "aws-violation", + }, + }, + }, + Sast: []formats.SourceCodeRow{ + { + SeverityDetails: formats.SeverityDetails{ + Severity: "High", + SeverityNumValue: 13, + }, + Finding: "XSS Vulnerability", + Location: formats.Location{ + File: "file1", + StartLine: 1, + StartColumn: 10, + EndLine: 2, + EndColumn: 11, + Snippet: "snippet", + }, + }, + }, + }, + expectedOutput: []ReviewComment{ + { + Location: formats.Location{ + File: "file1", + StartLine: 1, + StartColumn: 10, + EndLine: 2, + EndColumn: 11, + Snippet: "snippet", + }, + Type: ApplicableComment, + CommentInfo: vcsclient.PullRequestComment{ + CommentInfo: vcsclient.CommentInfo{ + Content: outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent("Low", "", "", "CVE-2023-4321", "summary-2", "component-C:", "", repo.OutputWriter), repo.OutputWriter), + }, + PullRequestDiff: vcsclient.PullRequestDiff{ + OriginalFilePath: "file1", + OriginalStartLine: 1, + OriginalStartColumn: 10, + OriginalEndLine: 2, + OriginalEndColumn: 11, + NewFilePath: "file1", + NewStartLine: 1, + NewStartColumn: 10, + NewEndLine: 2, + NewEndColumn: 11, + }, + }, + }, + { + Location: formats.Location{ + File: "file1", + StartLine: 1, + StartColumn: 10, + EndLine: 2, + EndColumn: 11, + Snippet: "aws-violation", + }, + Type: IacComment, + CommentInfo: vcsclient.PullRequestComment{ + CommentInfo: vcsclient.CommentInfo{ + Content: outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent("High", "Missing auto upgrade was detected", "", repo.OutputWriter), repo.OutputWriter), + }, + PullRequestDiff: vcsclient.PullRequestDiff{ + OriginalFilePath: "file1", + OriginalStartLine: 1, + OriginalStartColumn: 10, + OriginalEndLine: 2, + OriginalEndColumn: 11, + NewFilePath: "file1", + NewStartLine: 1, + NewStartColumn: 10, + NewEndLine: 2, + NewEndColumn: 11, + }, + }, + }, + { + Location: formats.Location{ + File: "file1", + StartLine: 1, + StartColumn: 10, + EndLine: 2, + EndColumn: 11, + Snippet: "snippet", + }, + Type: SastComment, + CommentInfo: vcsclient.PullRequestComment{ + CommentInfo: vcsclient.CommentInfo{ + Content: outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent("High", "XSS Vulnerability", "", [][]formats.Location{}, repo.OutputWriter), repo.OutputWriter), + }, + PullRequestDiff: vcsclient.PullRequestDiff{ + OriginalFilePath: "file1", + OriginalStartLine: 1, + OriginalStartColumn: 10, + OriginalEndLine: 2, + OriginalEndColumn: 11, + NewFilePath: "file1", + NewStartLine: 1, + NewStartColumn: 10, + NewEndLine: 2, + NewEndColumn: 11, + }, + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output := getNewReviewComments(repo, tc.issues) + assert.ElementsMatch(t, tc.expectedOutput, output) + }) + } +} diff --git a/utils/consts.go b/utils/consts.go index 18b55be01..ce0583c45 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -16,9 +16,6 @@ const ( BitbucketServer vcsProvider = "bitbucketServer" AzureRepos vcsProvider = "azureRepos" - // Frogbot comments - RescanRequestComment = "rescan" - // JFrog platform environment variables JFrogUserEnv = "JF_USER" JFrogUrlEnv = "JF_URL" diff --git a/utils/outputwriter/icons.go b/utils/outputwriter/icons.go index 9f7a6a4b5..c13725857 100644 --- a/utils/outputwriter/icons.go +++ b/utils/outputwriter/icons.go @@ -65,22 +65,21 @@ func getApplicableIconTags(iconName IconName) string { } func GetBanner(banner ImageSource) string { - formattedBanner := "[" + GetIconTag(banner) + "](https://github.com/jfrog/frogbot#readme)" - return fmt.Sprintf("
\n\n%s\n\n
\n\n", formattedBanner) + return GetMarkdownCenterTag(MarkAsLink(GetIconTag(banner), "https://github.com/jfrog/frogbot#readme")) } func GetIconTag(imageSource ImageSource) string { - return fmt.Sprintf("![](%s)", baseResourceUrl+imageSource) + return fmt.Sprintf("!%s", MarkAsLink("", fmt.Sprintf("%s%s", baseResourceUrl, imageSource))) } func GetSimplifiedTitle(is ImageSource) string { switch is { case NoVulnerabilityPrBannerSource: - return "**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.** \n" + return MarkAsBold("šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.") case VulnerabilitiesPrBannerSource: - return "**šŸšØ Frogbot scanned this pull request and found the below:**\n" + return MarkAsBold("šŸšØ Frogbot scanned this pull request and found the below:") case VulnerabilitiesFixPrBannerSource: - return "**šŸšØ This automated pull request was created by Frogbot and fixes the below:**\n" + return MarkAsBold("šŸšØ This automated pull request was created by Frogbot and fixes the below:") default: return "" } diff --git a/utils/outputwriter/icons_test.go b/utils/outputwriter/icons_test.go index 4630efe6a..3546058f5 100644 --- a/utils/outputwriter/icons_test.go +++ b/utils/outputwriter/icons_test.go @@ -23,17 +23,17 @@ func TestGetSeverityTagNotApplicable(t *testing.T) { } func TestGetVulnerabilitiesBanners(t *testing.T) { - assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n", GetBanner(NoVulnerabilityPrBannerSource)) - assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n", GetBanner(VulnerabilitiesPrBannerSource)) - assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n", GetBanner(VulnerabilitiesMrBannerSource)) - assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n", GetBanner(NoVulnerabilityMrBannerSource)) - assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n", GetBanner(VulnerabilitiesFixMrBannerSource)) - assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n", GetBanner(VulnerabilitiesFixPrBannerSource)) + assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", GetBanner(NoVulnerabilityPrBannerSource)) + assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", GetBanner(VulnerabilitiesPrBannerSource)) + assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", GetBanner(VulnerabilitiesMrBannerSource)) + assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", GetBanner(NoVulnerabilityMrBannerSource)) + assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", GetBanner(VulnerabilitiesFixMrBannerSource)) + assert.Equal(t, "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", GetBanner(VulnerabilitiesFixPrBannerSource)) } func TestGetSimplifiedTitle(t *testing.T) { - assert.Equal(t, "**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.** \n", GetSimplifiedTitle(NoVulnerabilityPrBannerSource)) - assert.Equal(t, "**šŸšØ Frogbot scanned this pull request and found the below:**\n", GetSimplifiedTitle(VulnerabilitiesPrBannerSource)) - assert.Equal(t, "**šŸšØ This automated pull request was created by Frogbot and fixes the below:**\n", GetSimplifiedTitle(VulnerabilitiesFixPrBannerSource)) + assert.Equal(t, "**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.**", GetSimplifiedTitle(NoVulnerabilityPrBannerSource)) + assert.Equal(t, "**šŸšØ Frogbot scanned this pull request and found the below:**", GetSimplifiedTitle(VulnerabilitiesPrBannerSource)) + assert.Equal(t, "**šŸšØ This automated pull request was created by Frogbot and fixes the below:**", GetSimplifiedTitle(VulnerabilitiesFixPrBannerSource)) assert.Equal(t, "", GetSimplifiedTitle("none")) } diff --git a/utils/outputwriter/markdowntable.go b/utils/outputwriter/markdowntable.go new file mode 100644 index 000000000..9ca52fb8b --- /dev/null +++ b/utils/outputwriter/markdowntable.go @@ -0,0 +1,224 @@ +package outputwriter + +import ( + "fmt" + "strings" +) + +const ( + tableRowFirstColumnSeparator = "| :---------------------: |" + tableRowColumnSeparator = " :-----------------------------------: |" + cellFirstCellPlaceholder = "| %s |" + cellCellPlaceholder = " %s |" + cellDefaultValue = "-" + + // (Default value for columns) If more than one value exists in a cell, the values will be separated by the delimiter. + SeparatorDelimited MarkdownColumnType = "single" + // If more than one value exists in a cell, for each value a new row will be created. + // The first row will contain the other columns values, and the rest of the rows will contain the values of the multi value column only. + // Only works if exists up to one MultiRowColumn in the table. + MultiRowColumn MarkdownColumnType = "multi" +) + +// Create a markdown table using the provided columns and rows, and construct a markdown string with the table's content. +// Each cell in the table can contain no values (represented as column default value), single or multiple values (separated by the table delimiter). +type MarkdownTableBuilder struct { + columns []*MarkdownColumn + delimiter string + rows [][]CellData +} + +type MarkdownColumnType string + +type MarkdownColumn struct { + Name string + ColumnType MarkdownColumnType + DefaultValue string +} + +// CellData represents the data of a cell in the markdown table. Each cell can contain multiple values. +type CellData []string + +func NewCellData(values ...string) CellData { + if len(values) == 0 { + // In markdown table, empty cell = cell with no values = cell with one empty value + return CellData{""} + } + return values +} + +// Create a markdown table builder with the provided columns. +func NewMarkdownTable(columns ...string) *MarkdownTableBuilder { + columnsInfo := []*MarkdownColumn{} + for _, column := range columns { + columnsInfo = append(columnsInfo, &MarkdownColumn{Name: column, ColumnType: SeparatorDelimited, DefaultValue: cellDefaultValue}) + } + return &MarkdownTableBuilder{columns: columnsInfo, delimiter: simpleSeparator} +} + +// 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 +} + +// Get the column information output controller by the provided name. +func (t *MarkdownTableBuilder) GetColumnInfo(name string) *MarkdownColumn { + for _, column := range t.columns { + if column.Name == name { + return column + } + } + return nil +} + +// Add a row to the markdown table, each value will be added to the corresponding column. +// Use to add row with single value columns only. +func (t *MarkdownTableBuilder) AddRow(values ...string) *MarkdownTableBuilder { + if len(values) == 0 { + return t + } + cellData := []CellData{} + for _, value := range values { + cellData = append(cellData, NewCellData(value)) + } + return t.AddRowWithCellData(cellData...) +} + +// Add a row to the markdown table, each value will be added to the corresponding column. +// Use to add row with multiple value columns. +func (t *MarkdownTableBuilder) AddRowWithCellData(values ...CellData) *MarkdownTableBuilder { + if len(values) == 0 { + return t + } + nColumns := len(t.columns) + row := make([]CellData, nColumns) + + for c := 0; c < nColumns; c++ { + if c < len(values) { + row[c] = values[c] + } else { + row[c] = NewCellData() + } + } + + t.rows = append(t.rows, row) + return t +} + +func (t *MarkdownTableBuilder) Build() string { + if len(t.columns) == 0 { + return "" + } + var tableBuilder strings.Builder + // Header + for c, column := range t.columns { + if c == 0 { + tableBuilder.WriteString(fmt.Sprintf(cellFirstCellPlaceholder, column.Name)) + } else { + tableBuilder.WriteString(fmt.Sprintf(cellCellPlaceholder, column.Name)) + } + } + tableBuilder.WriteString("\n") + // Separator + for c := range t.columns { + if c == 0 { + tableBuilder.WriteString(tableRowFirstColumnSeparator) + } else { + tableBuilder.WriteString(tableRowColumnSeparator) + } + } + // Content + for _, row := range t.rows { + tableBuilder.WriteString(t.getRowContent(row)) + } + return tableBuilder.String() +} + +func (t *MarkdownTableBuilder) getRowContent(row []CellData) string { + if columnIndex, multiValueColumn := t.getMultiValueColumn(); multiValueColumn != nil && len(row[columnIndex]) > 1 { + return t.getMultiValueRowsContent(row, columnIndex) + } + return t.getSeparatorDelimitedRowContent(row) +} + +func (t *MarkdownTableBuilder) getMultiValueColumn() (int, *MarkdownColumn) { + for i, column := range t.columns { + if column.ColumnType == MultiRowColumn { + return i, column + } + } + return -1, nil +} + +func (t *MarkdownTableBuilder) getMultiValueRowsContent(row []CellData, multiValueColumnIndex int) string { + var rowBuilder strings.Builder + firstRow := true + for _, value := range row[multiValueColumnIndex] { + // Add row for each value in the multi values column + if len(value) == 0 { + continue + } + content := []string{} + for column, cell := range row { + if column == multiValueColumnIndex { + // Multi values column separated by different rows, add the specific value for this row + content = append(content, value) + } else { + if firstRow { + // First row contains the other columns values as well + content = append(content, t.getCellContent(cell, t.columns[column].DefaultValue)) + } else { + // Rest of the rows contains only the multi values column value + content = append(content, " ") + } + } + } + firstRow = false + rowBuilder.WriteString(buildRowContent(content...)) + } + return rowBuilder.String() +} + +func (t *MarkdownTableBuilder) getSeparatorDelimitedRowContent(row []CellData) string { + content := []string{} + for column, columnInfo := range t.columns { + content = append(content, t.getCellContent(row[column], columnInfo.DefaultValue)) + } + return buildRowContent(content...) +} + +func buildRowContent(content ...string) string { + if len(content) == 0 { + return "" + } + var rowBuilder strings.Builder + rowBuilder.WriteString("\n") + for c, cell := range content { + if c == 0 { + rowBuilder.WriteString(fmt.Sprintf("| %s |", cell)) + } else { + rowBuilder.WriteString(fmt.Sprintf(" %s |", cell)) + } + } + return rowBuilder.String() +} + +func (t *MarkdownTableBuilder) getCellContent(data CellData, defaultValue string) string { + if len(data) == 0 { + return defaultValue + } + var cellBuilder strings.Builder + for _, value := range data { + value = strings.TrimSpace(value) + if value == "" { + continue + } + cellBuilder.WriteString(fmt.Sprintf("%s%s", value, t.delimiter)) + } + value := strings.TrimSuffix(cellBuilder.String(), t.delimiter) + if value == "" { + return defaultValue + } + return value +} diff --git a/utils/outputwriter/markdowntable_test.go b/utils/outputwriter/markdowntable_test.go new file mode 100644 index 000000000..556b6e1d9 --- /dev/null +++ b/utils/outputwriter/markdowntable_test.go @@ -0,0 +1,220 @@ +package outputwriter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMarkdownTableContent(t *testing.T) { + testCases := []struct { + name string + columns []string + rows [][]string + expectedOutput [][]CellData + }{ + { + name: "Empty", + columns: []string{}, + rows: [][]string{}, + expectedOutput: [][]CellData{}, + }, + { + name: "No rows", + columns: []string{"col1"}, + rows: [][]string{}, + expectedOutput: [][]CellData{}, + }, + { + name: "Same number of columns", + columns: []string{"col1", "col2", "col3"}, + rows: [][]string{ + {"row1col1", "row1col2", "row1col3"}, + {"row2col1", "row2col2", "row2col3"}, + {"row3col1", "row3col2", "row3col3"}, + }, + expectedOutput: [][]CellData{ + {{"row1col1"}, {"row1col2"}, {"row1col3"}}, + {{"row2col1"}, {"row2col2"}, {"row2col3"}}, + {{"row3col1"}, {"row3col2"}, {"row3col3"}}, + }, + }, + { + name: "Different number of columns", + columns: []string{"col1", "col2", "col3"}, + rows: [][]string{ + {"row1col1", "row1col2", ""}, + {"row2col1", "row2col2"}, + {"row3col1", "", "row3col3", "row3col4"}, + {"row4col1"}, + }, + expectedOutput: [][]CellData{ + {{"row1col1"}, {"row1col2"}, {""}}, + {{"row2col1"}, {"row2col2"}, {""}}, + {{"row3col1"}, {""}, {"row3col3"}}, + {{"row4col1"}, {""}, {""}}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + table := NewMarkdownTable(tc.columns...) + for _, row := range tc.rows { + table.AddRow(row...) + } + assert.Len(t, table.rows, len(tc.expectedOutput)) + for i, row := range table.rows { + assertRow(t, row, tc.expectedOutput[i], len(tc.columns)) + } + }) + } +} + +func assertRow(t *testing.T, actual []CellData, expected []CellData, expectedNumberColumns int) { + assert.Len(t, actual, expectedNumberColumns) + for i, cell := range actual { + assert.Len(t, cell, len(expected[i])) + for j, value := range cell { + assert.Equal(t, expected[i][j], value) + } + } +} + +func TestMarkdownTableBuild(t *testing.T) { + testCases := []struct { + name string + expectedOutput string + columns []string + rows [][]string + }{ + { + name: "Empty", + columns: []string{}, + rows: [][]string{}, + expectedOutput: "", + }, + { + name: "No rows", + columns: []string{"col1"}, + rows: [][]string{}, + expectedOutput: "| col1 |\n" + tableRowFirstColumnSeparator, + }, + { + name: "Same number of columns", + columns: []string{"col1", "col2"}, + rows: [][]string{ + {"row1col1", "row1col2"}, + {"row2col1", "row2col2"}, + {"row3col1", "row3col2"}, + }, + expectedOutput: "| col1 | col2 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + ` +| row1col1 | row1col2 | +| row2col1 | row2col2 | +| row3col1 | row3col2 |`, + }, + { + name: "Different number of columns", + columns: []string{"col1", "col2", "col3"}, + rows: [][]string{ + {"row1col1", "row1col2", ""}, + {"row2col1", "row2col2"}, + {}, + {"row3col1", "", "row3col3", "row3col4"}, + {"row4col1"}, + {"row5col1", "row5col2", "row5col3"}, + }, + expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` +| row1col1 | row1col2 | - | +| row2col1 | row2col2 | - | +| row3col1 | - | row3col3 | +| row4col1 | - | - | +| row5col1 | row5col2 | row5col3 |`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + table := NewMarkdownTable(tc.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 + expectedOutput string + columns []MarkdownColumn + rows [][]CellData + }{ + { + name: "Empty on multi value column", + columns: []MarkdownColumn{{Name: "col1", ColumnType: MultiRowColumn}, {Name: "col2"}, {Name: "col3"}}, + rows: [][]CellData{ + {{""}, {"row1col2"}, {"row1col3"}}, + }, + expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` +| - | row1col2 | row1col3 |`, + }, + { + name: "One value on multi value column", + columns: []MarkdownColumn{{Name: "col1"}, {Name: "col2"}, {Name: "col3", ColumnType: MultiRowColumn}}, + rows: [][]CellData{ + {{"row1col1"}, {"row1col2"}, {"row1col3"}}, + {{"row2col1"}, {"row2col2"}, {"row2col3"}}, + }, + expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` +| row1col1 | row1col2 | row1col3 | +| row2col1 | row2col2 | row2col3 |`, + }, + { + name: "Multiple values on separator delimited column", + columns: []MarkdownColumn{{Name: "col1"}, {Name: "col2"}, {Name: "col3"}}, + rows: [][]CellData{ + {{"row1col1"}, {""}, {"row1col3"}}, + {{"row2col1"}, {"row2col2"}, {"row2col3val1", "row2col3val2"}}, + {{"row3col1"}, {"row3col2val1", "row3col2val2", "row3col2val3"}, {"row3col3"}}, + }, + expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` +| row1col1 | - | row1col3 | +| row2col1 | row2col2 | row2col3val1, row2col3val2 | +| row3col1 | row3col2val1, row3col2val2, row3col2val3 | row3col3 |`, + }, + { + name: "Multiple values on multi row column", + columns: []MarkdownColumn{{Name: "col1"}, {Name: "col2", ColumnType: MultiRowColumn}, {Name: "col3"}}, + rows: [][]CellData{ + {{"row1col1"}, {""}, {"row1col3"}}, + {{"row2col1"}, {"row2col2"}, {"row2col3val1", "row2col3val2"}}, + {{"row3col1"}, {"row3col2val1", "row3col2val2", "row3col2val3"}, {"row3col3"}}, + }, + expectedOutput: "| col1 | col2 | col3 |\n" + tableRowFirstColumnSeparator + tableRowColumnSeparator + tableRowColumnSeparator + ` +| row1col1 | - | row1col3 | +| row2col1 | row2col2 | row2col3val1, row2col3val2 | +| row3col1 | row3col2val1 | row3col3 | +| | row3col2val2 | | +| | row3col2val3 | |`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + columns := []string{} + for _, column := range tc.columns { + columns = append(columns, column.Name) + } + table := NewMarkdownTable(columns...) + for _, column := range tc.columns { + if column.ColumnType == MultiRowColumn { + table.GetColumnInfo(column.Name).ColumnType = MultiRowColumn + } + } + for _, row := range tc.rows { + table.AddRowWithCellData(row...) + } + assert.Equal(t, tc.expectedOutput, table.Build()) + }) + } +} diff --git a/utils/outputwriter/outputcontent.go b/utils/outputwriter/outputcontent.go new file mode 100644 index 000000000..8d533efa9 --- /dev/null +++ b/utils/outputwriter/outputcontent.go @@ -0,0 +1,324 @@ +package outputwriter + +import ( + "fmt" + "strings" + + "github.com/jfrog/froggit-go/vcsutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" + xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" +) + +const ( + FrogbotTitlePrefix = "[šŸø Frogbot]" + ReviewCommentId = "FrogbotReviewComment" + + vulnerableDependenciesTitle = "šŸ“¦ Vulnerable Dependencies" + vulnerableDependenciesResearchDetailsSubTitle = "šŸ”¬ Research Details" + + contextualAnalysisTitle = "šŸ“¦šŸ” Contextual Analysis CVE Vulnerability" + iacTitle = "šŸ› ļø Infrastructure as Code Vulnerability" + sastTitle = "šŸŽÆ Static Application Security Testing (SAST) Vulnerability" +) + +var ( + CommentGeneratedByFrogbot = MarkAsLink("šŸø JFrog Frogbot", "https://github.com/jfrog/frogbot#readme") + 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." +) + +func GetPRSummaryContent(content string, issuesExists, isComment bool, writer OutputWriter) string { + comment := strings.Builder{} + comment.WriteString(writer.Image(getPRSummaryBanner(issuesExists, isComment, writer.VcsProvider()))) + if issuesExists { + WriteContent(&comment, content) + } + WriteContent(&comment, + untitledForJasMsg(writer), + footer(writer), + ) + return comment.String() +} + +func getPRSummaryBanner(issuesExists, isComment bool, provider vcsutils.VcsProvider) ImageSource { + if !isComment { + return fixCVETitleSrc(provider) + } + if !issuesExists { + return NoIssuesTitleSrc(provider) + } + return PRSummaryCommentTitleSrc(provider) +} + +func IsFrogbotSummaryComment(writer OutputWriter, content string) bool { + client := writer.VcsProvider() + return strings.Contains(content, writer.Image(NoIssuesTitleSrc(client))) || + strings.Contains(content, writer.Image(PRSummaryCommentTitleSrc(client))) +} + +func NoIssuesTitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource { + if vcsProvider == vcsutils.GitLab { + return NoVulnerabilityMrBannerSource + } + return NoVulnerabilityPrBannerSource +} + +func PRSummaryCommentTitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource { + if vcsProvider == vcsutils.GitLab { + return VulnerabilitiesMrBannerSource + } + return VulnerabilitiesPrBannerSource +} + +func fixCVETitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource { + if vcsProvider == vcsutils.GitLab { + return VulnerabilitiesFixMrBannerSource + } + return VulnerabilitiesFixPrBannerSource +} + +func untitledForJasMsg(writer OutputWriter) string { + if writer.IsEntitledForJas() { + return "" + } + return fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(jasFeaturesMsgWhenNotEnabled)) +} + +func footer(writer OutputWriter) string { + return fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(CommentGeneratedByFrogbot)) +} + +func getVulnerabilitiesSummaryTable(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { + // Construct table + columns := []string{"SEVERITY"} + if writer.IsShowingCaColumn() { + columns = append(columns, "CONTEXTUAL ANALYSIS") + } + columns = append(columns, "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY", "FIXED VERSIONS", "CVES") + 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 + } + // Construct rows + for _, vulnerability := range vulnerabilities { + row := []CellData{{writer.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)}} + if writer.IsShowingCaColumn() { + row = append(row, NewCellData(vulnerability.Applicable)) + } + row = append(row, + getDirectDependenciesCellData("%s:%s", 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) { + if len(components) == 0 { + return NewCellData() + } + for _, component := range components { + dependencies = append(dependencies, fmt.Sprintf(format, component.Name, component.Version)) + } + return +} + +func getCveIdsCellData(cveRows []formats.CveRow) (ids CellData) { + if len(cveRows) == 0 { + return NewCellData() + } + for _, cve := range cveRows { + ids = append(ids, cve.Id) + } + return +} + +func VulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { + if len(vulnerabilities) == 0 { + return "" + } + var contentBuilder strings.Builder + // Write summary table part + WriteContent(&contentBuilder, + writer.MarkAsTitle(vulnerableDependenciesTitle, 2), + writer.MarkAsTitle("āœļø Summary", 3), + writer.MarkInCenter(getVulnerabilitiesSummaryTable(vulnerabilities, writer)), + ) + // Write for each vulnerability details part + detailsContent := strings.TrimSpace(getVulnerabilityDetailsContent(vulnerabilities, writer)) + if detailsContent != "" { + if len(vulnerabilities) == 1 { + WriteContent(&contentBuilder, writer.MarkAsTitle(vulnerableDependenciesResearchDetailsSubTitle, 3), detailsContent) + } else { + WriteContent(&contentBuilder, writer.MarkAsDetails(vulnerableDependenciesResearchDetailsSubTitle, 3, detailsContent)) + } + } + return contentBuilder.String() +} + +func getVulnerabilityDetailsContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { + var descriptionContentBuilder strings.Builder + for i := range vulnerabilities { + vulDescriptionContent := createVulnerabilityResearchDescription(&vulnerabilities[i]) + if vulDescriptionContent == "" { + // No content + continue + } + if len(vulnerabilities) == 1 { + WriteContent(&descriptionContentBuilder, vulDescriptionContent) + break + } + WriteContent(&descriptionContentBuilder, + writer.MarkAsDetails( + fmt.Sprintf(`%s %s %s`, + getVulnerabilityDescriptionIdentifier(vulnerabilities[i].Cves, vulnerabilities[i].IssueId), + vulnerabilities[i].ImpactedDependencyName, + vulnerabilities[i].ImpactedDependencyVersion), + 4, vulDescriptionContent, + ), + ) + } + return descriptionContentBuilder.String() +} + +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 + } + + if vulnResearch.Details != "" { + WriteContent(&descriptionBuilder, MarkAsBold("Description:"), vulnResearch.Details) + } + if vulnResearch.Remediation != "" { + if vulnResearch.Details != "" { + WriteNewLine(&descriptionBuilder) + } + WriteContent(&descriptionBuilder, MarkAsBold("Remediation:"), vulnResearch.Remediation) + } + return descriptionBuilder.String() +} + +func getVulnerabilityDescriptionIdentifier(cveRows []formats.CveRow, xrayId string) string { + identifier := xrayutils.GetIssueIdentifier(cveRows, xrayId) + if identifier == "" { + return "" + } + return fmt.Sprintf("[ %s ]", identifier) +} + +func LicensesContent(licenses []formats.LicenseRow, writer OutputWriter) string { + if len(licenses) == 0 { + return "" + } + // Title + var contentBuilder strings.Builder + WriteContent(&contentBuilder, writer.MarkAsTitle("āš–ļø Violated Licenses", 2)) + // Content + table := NewMarkdownTable("LICENSE", "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY").SetDelimiter(writer.Separator()) + for _, license := range licenses { + table.AddRowWithCellData( + NewCellData(license.LicenseKey), + getDirectDependenciesCellData("%s %s", license.Components), + NewCellData(fmt.Sprintf("%s %s", license.ImpactedDependencyName, license.ImpactedDependencyVersion)), + ) + } + 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)) + 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, writer OutputWriter) string { + var contentBuilder strings.Builder + contentBuilder.WriteString(MarkdownComment(ReviewCommentId)) + WriteContent(&contentBuilder, getFallbackCommentLocationDescription(location), content, footer(writer)) + return contentBuilder.String() +} + +func IsFrogbotReviewComment(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) +} + +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) + return table.Build() +} + +func ApplicableCveReviewContent(severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string, writer OutputWriter) string { + 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)) + } + return contentBuilder.String() +} + +func getJasDescriptionTable(severity, finding string, writer OutputWriter) string { + return NewMarkdownTable("Severity", "Finding").AddRow(writer.FormattedSeverity(severity, "Applicable"), finding).Build() +} + +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 SastReviewContent(severity, finding, fullDetails string, codeFlows [][]formats.Location, writer OutputWriter) string { + var contentBuilder strings.Builder + WriteContent(&contentBuilder, + writer.MarkAsTitle(sastTitle, 2), + writer.MarkInCenter(getJasDescriptionTable(severity, finding, writer)), + writer.MarkAsDetails("Full description", 3, fullDetails), + ) + + if len(codeFlows) > 0 { + WriteContent(&contentBuilder, writer.MarkAsDetails("Code Flows", 3, sastCodeFlowsReviewContent(codeFlows, writer))) + } + return contentBuilder.String() +} + +func sastCodeFlowsReviewContent(codeFlows [][]formats.Location, writer OutputWriter) string { + var contentBuilder strings.Builder + for _, flow := range codeFlows { + WriteContent(&contentBuilder, writer.MarkAsDetails("Vulnerable data flow analysis result", 4, sastDataFlowLocationsReviewContent(flow))) + } + return contentBuilder.String() +} + +func sastDataFlowLocationsReviewContent(flow []formats.Location) string { + var contentBuilder strings.Builder + for _, location := range flow { + WriteContent(&contentBuilder, fmt.Sprintf("%s %s (at %s line %d)\n", "ā†˜ļø", MarkAsQuote(location.Snippet), location.File, location.StartLine)) + } + return contentBuilder.String() +} diff --git a/utils/outputwriter/outputcontent_test.go b/utils/outputwriter/outputcontent_test.go new file mode 100644 index 000000000..18a92ca32 --- /dev/null +++ b/utils/outputwriter/outputcontent_test.go @@ -0,0 +1,855 @@ +package outputwriter + +import ( + "path/filepath" + "strconv" + "testing" + + "github.com/jfrog/froggit-go/vcsutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/stretchr/testify/assert" +) + +func TestIsFrogbotSummaryComment(t *testing.T) { + testCases := []struct { + name string + comment string + cases []OutputTestCase + }{ + { + name: "No Summary comment", + comment: "This comment is unrelated to Frogbot", + cases: []OutputTestCase{ + { + name: "Standard output (PR)", + writer: &StandardOutput{}, + expectedOutput: "false", + }, + { + name: "Standard output (MR)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutput: "false", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "false", + }, + }, + }, + { + name: "No Vulnerability PR", + comment: "This is a comment with the " + GetBanner(NoVulnerabilityPrBannerSource) + " icon", + cases: []OutputTestCase{ + { + name: "Standard output (PR)", + writer: &StandardOutput{}, + expectedOutput: "true", + }, + { + name: "Standard output (MR)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutput: "false", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "false", + }, + }, + }, + { + name: "No Vulnerability MR", + comment: "This is a comment with the " + GetBanner(NoVulnerabilityMrBannerSource) + " icon", + cases: []OutputTestCase{ + { + name: "Standard output (PR)", + writer: &StandardOutput{}, + expectedOutput: "false", + }, + { + name: "Standard output (MR)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutput: "true", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "false", + }, + }, + }, + { + name: "No Vulnerability simplified", + comment: "This is a comment with the " + GetSimplifiedTitle(NoVulnerabilityPrBannerSource) + " icon", + cases: []OutputTestCase{ + { + name: "Standard output (PR)", + writer: &StandardOutput{}, + expectedOutput: "false", + }, + { + name: "Standard output (MR)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutput: "false", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "true", + }, + }, + }, + { + name: "Vulnerability PR", + comment: "This is a comment with the " + GetBanner(VulnerabilitiesPrBannerSource) + " icon", + cases: []OutputTestCase{ + { + name: "Standard output (PR)", + writer: &StandardOutput{}, + expectedOutput: "true", + }, + { + name: "Standard output (MR)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutput: "false", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "false", + }, + }, + }, + { + name: "Vulnerability MR", + comment: "This is a comment with the " + GetBanner(VulnerabilitiesMrBannerSource) + " icon", + cases: []OutputTestCase{ + { + name: "Standard output (PR)", + writer: &StandardOutput{}, + expectedOutput: "false", + }, + { + name: "Standard output (MR)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutput: "true", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "false", + }, + }, + }, + { + name: "Vulnerability simplified", + comment: "This is a comment with the " + GetSimplifiedTitle(VulnerabilitiesPrBannerSource) + " icon", + cases: []OutputTestCase{ + { + name: "Standard output (PR)", + writer: &StandardOutput{}, + expectedOutput: "false", + }, + { + name: "Standard output (MR)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutput: "false", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "true", + }, + }, + }, + } + for _, tc := range testCases { + for _, test := range tc.cases { + expected, err := strconv.ParseBool(GetExpectedTestOutput(t, test)) + assert.NoError(t, err) + t.Run(tc.name+"_"+test.name, func(t *testing.T) { + assert.Equal(t, expected, IsFrogbotSummaryComment(test.writer, tc.comment)) + }) + } + } +} + +func TestGetPRSummaryContent(t *testing.T) { + testCases := []struct { + name string + cases []OutputTestCase + issuesExists bool + isComment bool + }{ + { + name: "Summary comment No issues found", + issuesExists: false, + isComment: true, + cases: []OutputTestCase{ + { + name: "Pull Request not entitled (Standard output)", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_pr_no_issues_not_entitled.md"), + }, + { + name: "Pull Request entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_pr_no_issues_entitled.md"), + }, + { + name: "Merge Request not entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_mr_no_issues_not_entitled.md"), + }, + { + name: "Merge Request entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab, entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_mr_no_issues_entitled.md"), + }, + { + name: "Simplified output not entitled", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_simplified_no_issues_not_entitled.md"), + }, + { + name: "Simplified output entitled", + writer: &SimplifiedOutput{MarkdownOutput{entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_simplified_no_issues_entitled.md"), + }, + }, + }, + { + name: "Summary comment Found issues", + issuesExists: true, + isComment: true, + cases: []OutputTestCase{ + { + name: "Pull Request not entitled (Standard output)", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_pr_issues_not_entitled.md"), + }, + { + name: "Pull Request entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_pr_issues_entitled.md"), + }, + { + name: "Merge Request not entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_mr_issues_not_entitled.md"), + }, + { + name: "Merge Request entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab, entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_mr_issues_entitled.md"), + }, + { + name: "Simplified output not entitled", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_simplified_issues_not_entitled.md"), + }, + { + name: "Simplified output entitled", + writer: &SimplifiedOutput{MarkdownOutput{entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "summary_comment_simplified_issues_entitled.md"), + }, + }, + }, + { + name: "Frogbot Fix issues details content", + issuesExists: true, + isComment: false, + cases: []OutputTestCase{ + { + name: "Pull Request not entitled (Standard output)", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "fix_pr_not_entitled.md"), + }, + { + name: "Pull Request entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "fix_pr_entitled.md"), + }, + { + name: "Merge Request not entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "fix_mr_not_entitled.md"), + }, + { + name: "Merge Request entitled (Standard output)", + writer: &StandardOutput{MarkdownOutput{vcsProvider: vcsutils.GitLab, entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "fix_mr_entitled.md"), + }, + { + name: "Simplified output not entitled", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "fix_simplified_not_entitled.md"), + }, + { + name: "Simplified output entitled", + writer: &SimplifiedOutput{MarkdownOutput{entitledForJas: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "structure", "fix_simplified_entitled.md"), + }, + }, + }, + } + + content := "\n" + MarkAsCodeSnippet("some content") + + for _, tc := range testCases { + for _, test := range tc.cases { + t.Run(tc.name+"_"+test.name, func(t *testing.T) { + expectedOutput := GetExpectedTestOutput(t, test) + output := GetPRSummaryContent(content, tc.issuesExists, tc.isComment, test.writer) + assert.Equal(t, expectedOutput, output) + }) + } + } +} + +func TestVulnerabilitiesContent(t *testing.T) { + testCases := []struct { + name string + vulnerabilities []formats.VulnerabilityOrViolationRow + cases []OutputTestCase + }{ + { + name: "No vulnerabilities", + vulnerabilities: []formats.VulnerabilityOrViolationRow{}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutput: "", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "", + }, + }, + }, + { + name: "One vulnerability", + vulnerabilities: []formats.VulnerabilityOrViolationRow{ + { + Summary: "Summary CVE-2022-26652", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + 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.3]"}, + JfrogResearchInformation: &formats.JfrogResearchInformation{ + Details: "Research CVE-2022-26652 details", + Remediation: "some remediation", + }, + Cves: []formats.CveRow{{Id: "CVE-2022-26652"}}, + }, + }, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "vulnerabilities", "one_vulnerability_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "vulnerabilities", "one_vulnerability_simplified.md"), + }, + }, + }, + { + name: "One vulnerability, no Details", + vulnerabilities: []formats.VulnerabilityOrViolationRow{ + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, + 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.3]"}, + Cves: []formats.CveRow{{Id: "CVE-2022-26652"}}, + }, + }, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "vulnerabilities", "one_vulnerability_no_details_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "vulnerabilities", "one_vulnerability_no_details_simplified.md"), + }, + }, + }, + { + name: "multiple Vulnerabilities with Contextual Analysis", + vulnerabilities: []formats.VulnerabilityOrViolationRow{ + { + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + SeverityDetails: formats.SeverityDetails{Severity: "Critical", SeverityNumValue: utils.GetSeverity("Critical", utils.NotApplicable).SeverityNumValue}, + 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: formats.SeverityDetails{Severity: "High", SeverityNumValue: utils.GetSeverity("High", utils.ApplicabilityUndetermined).SeverityNumValue}, + 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: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: utils.GetSeverity("Medium", utils.Applicable).SeverityNumValue}, + 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: formats.SeverityDetails{Severity: "Low", SeverityNumValue: utils.GetSeverity("Low", utils.ApplicabilityUndetermined).SeverityNumValue}, + 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{}, + }, + }, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{MarkdownOutput{showCaColumn: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{MarkdownOutput{showCaColumn: true}}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "vulnerabilities", "vulnerabilities_simplified.md"), + }, + }, + }, + } + 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), VulnerabilitiesContent(tc.vulnerabilities, test.writer)) + }) + } + } +} + +func TestLicensesContent(t *testing.T) { + testCases := []struct { + name string + licenses []formats.LicenseRow + cases []OutputTestCase + }{ + { + name: "No license violations", + licenses: []formats.LicenseRow{}, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutput: "", + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutput: "", + }, + }, + }, + { + name: "License violations", + licenses: []formats.LicenseRow{ + { + LicenseKey: "License1", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{{Name: "Comp1", Version: "1.0"}}, + ImpactedDependencyName: "Dep1", + ImpactedDependencyVersion: "2.0", + }, + }, + { + LicenseKey: "License2", + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{ + { + Name: "root", + Version: "1.0.0", + }, + { + Name: "minimatch", + Version: "1.2.3", + }, + }, + ImpactedDependencyName: "Dep2", + ImpactedDependencyVersion: "3.0", + }, + }, + }, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "license", "license_violation_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testSummaryCommentDir, "license", "license_violation_simplified.md"), + }, + }, + }, + } + 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)) + }) + } + } +} + +func TestIsFrogbotReviewComment(t *testing.T) { + testCases := []struct { + name string + content string + expectedOutput bool + }{ + { + name: "Not frogbot comments", + content: "This comment is unrelated to Frogbot", + expectedOutput: false, + }, + { + name: "Frogbot review comment", + content: MarkdownComment(ReviewCommentId) + "This is a review comment", + expectedOutput: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedOutput, IsFrogbotReviewComment(tc.content)) + }) + } +} + +func TestGenerateReviewComment(t *testing.T) { + testCases := []struct { + name string + location *formats.Location + cases []OutputTestCase + }{ + { + name: "Review comment structure", + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "review_comment_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "review_comment_simplified.md"), + }, + }, + }, + { + name: "Fallback review comment structure", + location: &formats.Location{ + File: "file", + StartLine: 11, + StartColumn: 22, + EndLine: 33, + EndColumn: 44, + Snippet: "snippet", + }, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "review_comment_fallback_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "review_comment_fallback_simplified.md"), + }, + }, + }, + } + + content := "\n" + MarkAsCodeSnippet("some review content") + + for _, tc := range testCases { + for _, test := range tc.cases { + t.Run(tc.name+"_"+test.name, func(t *testing.T) { + expectedOutput := GetExpectedTestOutput(t, test) + output := GenerateReviewCommentContent(content, test.writer) + if tc.location != nil { + output = GetFallbackReviewCommentContent(content, *tc.location, test.writer) + } + assert.Equal(t, expectedOutput, output) + }) + } + } +} + +func TestApplicableReviewContent(t *testing.T) { + testCases := []struct { + name string + 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", + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "applicable", "applicable_review_content_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: 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", + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "applicable", "applicable_review_content_no_remediation_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "applicable", "applicable_review_content_no_remediation_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, ApplicableCveReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.cve, tc.cveDetails, tc.impactedDependency, tc.remediation, test.writer)) + }) + } + } +} + +func TestIacReviewContent(t *testing.T) { + testCases := []struct { + name string + severity, finding, fullDetails string + 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", + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "iac", "iac_review_content_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "iac", "iac_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)) + }) + } + } +} + +func TestSastReviewContent(t *testing.T) { + testCases := []struct { + name string + severity string + finding string + fullDetails string + codeFlows [][]formats.Location + 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{ + { + { + 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", + }, + }, + }, + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "sast", "sast_review_content_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: 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.", + cases: []OutputTestCase{ + { + name: "Standard output", + writer: &StandardOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_standard.md"), + }, + { + name: "Simplified output", + writer: &SimplifiedOutput{}, + expectedOutputPath: filepath.Join(testReviewCommentDir, "sast", "sast_review_content_no_code_flow_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, SastReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.codeFlows, test.writer)) + }) + } + } +} diff --git a/utils/outputwriter/outputwriter.go b/utils/outputwriter/outputwriter.go index b6cc6f421..678fb2ad3 100644 --- a/utils/outputwriter/outputwriter.go +++ b/utils/outputwriter/outputwriter.go @@ -5,26 +5,10 @@ import ( "strings" "github.com/jfrog/froggit-go/vcsutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" - xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" ) const ( - FrogbotTitlePrefix = "[šŸø Frogbot]" - CommentGeneratedByFrogbot = "[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)" - ReviewCommentId = "FrogbotReviewComment" - vulnerabilitiesTableHeader = "\n| SEVERITY | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: |" - vulnerabilitiesTableHeaderWithContextualAnalysis = "| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: |" - iacTableHeader = "\n| SEVERITY | FILE | LINE:COLUMN | FINDING |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: |" - vulnerableDependenciesTitle = "## šŸ“¦ Vulnerable Dependencies" - summaryTitle = "### āœļø Summary" - researchDetailsTitle = "## šŸ”¬ Research Details" - sastTitle = "## šŸŽÆ Static Application Security Testing (SAST) Vulnerability" - iacTitle = "## šŸ› ļø Infrastructure as Code" - licenseTitle = "## āš–ļø Violated Licenses" - contextualAnalysisTitle = "## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability\n" - licenseTableHeader = "\n| LICENSE | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | \n| :---------------------: | :----------------------------------: | :-----------------------------------: |" - SecretsEmailCSS = `body { + SecretsEmailCSS = `body { font-family: Arial, sans-serif; background-color: #f5f5f5; } @@ -103,164 +87,88 @@ const ( // The OutputWriter interface allows Frogbot output to be written in an appropriate way for each git provider. // Some git providers support markdown only partially, whereas others support it fully. type OutputWriter interface { - VulnerabilitiesTableRow(vulnerability formats.VulnerabilityOrViolationRow) string - NoVulnerabilitiesTitle() string - VulnerabilitiesTitle(isComment bool) string - VulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow) string - LicensesContent(licenses []formats.LicenseRow) string - IacTableContent(iacRows []formats.SourceCodeRow) string - Footer() string - Separator() string - FormattedSeverity(severity, applicability string) string - IsFrogbotResultComment(comment string) bool + // Options SetJasOutputFlags(entitled, showCaColumn bool) + IsShowingCaColumn() bool + IsEntitledForJas() bool + // VCS info VcsProvider() vcsutils.VcsProvider SetVcsProvider(provider vcsutils.VcsProvider) - UntitledForJasMsg() string - - ApplicableCveReviewContent(severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string) string - IacReviewContent(severity, finding, fullDetails string) string - SastReviewContent(severity, finding, fullDetails string, codeFlows [][]formats.Location) string + // Markdown interface + FormattedSeverity(severity, applicability string) string + Separator() string + MarkInCenter(content string) string + MarkAsDetails(summary string, subTitleDepth int, content string) string + MarkAsTitle(title string, subTitleDepth int) string + Image(source ImageSource) string } -func GetCompatibleOutputWriter(provider vcsutils.VcsProvider) OutputWriter { - switch provider { - case vcsutils.BitbucketServer: - return &SimplifiedOutput{vcsProvider: provider} - default: - return &StandardOutput{vcsProvider: provider} - } +type MarkdownOutput struct { + showCaColumn bool + entitledForJas bool + vcsProvider vcsutils.VcsProvider } -func createVulnerabilityDescription(vulnerability *formats.VulnerabilityOrViolationRow) string { - var descriptionBuilder strings.Builder - vulnResearch := vulnerability.JfrogResearchInformation - if vulnResearch == nil { - vulnResearch = &formats.JfrogResearchInformation{Details: vulnerability.Summary} - } - - // Write description if exists: - if vulnResearch.Details != "" { - descriptionBuilder.WriteString(fmt.Sprintf("\n**Description:**\n%s\n", vulnResearch.Details)) - } +func (mo *MarkdownOutput) SetVcsProvider(provider vcsutils.VcsProvider) { + mo.vcsProvider = provider +} - // Write remediation if exists - if vulnResearch.Remediation != "" { - descriptionBuilder.WriteString(fmt.Sprintf("**Remediation:**\n%s\n", vulnResearch.Remediation)) - } +func (mo *MarkdownOutput) VcsProvider() vcsutils.VcsProvider { + return mo.vcsProvider +} - return descriptionBuilder.String() +func (mo *MarkdownOutput) SetJasOutputFlags(entitled, showCaColumn bool) { + mo.entitledForJas = entitled + mo.showCaColumn = showCaColumn } -func getVulnerabilitiesTableContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { - var tableContent string - for _, vulnerability := range vulnerabilities { - tableContent += "\n" + writer.VulnerabilitiesTableRow(vulnerability) - } - return tableContent +func (mo *MarkdownOutput) IsShowingCaColumn() bool { + return mo.showCaColumn } -func getLicensesTableContent(licenses []formats.LicenseRow, writer OutputWriter) string { - var tableContent strings.Builder - for _, license := range licenses { - var directDependenciesBuilder strings.Builder - for _, component := range license.Components { - directDependenciesBuilder.WriteString(fmt.Sprintf("%s %s%s", component.Name, component.Version, writer.Separator())) - } - directDependencies := strings.TrimSuffix(directDependenciesBuilder.String(), writer.Separator()) - impactedDependency := fmt.Sprintf("%s %s", license.ImpactedDependencyName, license.ImpactedDependencyVersion) - tableContent.WriteString(fmt.Sprintf("\n| %s | %s | %s |", license.LicenseKey, directDependencies, impactedDependency)) - } - return tableContent.String() +func (mo *MarkdownOutput) IsEntitledForJas() bool { + return mo.entitledForJas } -func getIacTableContent(iacRows []formats.SourceCodeRow, writer OutputWriter) string { - var tableContent string - for _, iac := range iacRows { - tableContent += fmt.Sprintf("\n| %s | %s | %s | %s |", writer.FormattedSeverity(iac.Severity, string(xrayutils.Applicable)), iac.File, fmt.Sprintf("%d:%d", iac.StartLine, iac.StartColumn), iac.Snippet) +func GetCompatibleOutputWriter(provider vcsutils.VcsProvider) OutputWriter { + switch provider { + case vcsutils.BitbucketServer: + return &SimplifiedOutput{MarkdownOutput{vcsProvider: provider}} + default: + return &StandardOutput{MarkdownOutput{vcsProvider: provider}} } - return tableContent } func MarkdownComment(text string) string { return fmt.Sprintf("\n\n[comment]: <> (%s)\n", text) } -func MarkAsQuote(s string) string { - return fmt.Sprintf("`%s`", s) -} - -func MarkAsCodeSnippet(snippet string) string { - return fmt.Sprintf("```\n%s\n```", snippet) -} - -func GetJasMarkdownDescription(severity, finding string) string { - headerRow := "| Severity | Finding |\n" - separatorRow := "| :--------------: | :---: |\n" - return headerRow + separatorRow + fmt.Sprintf("| %s | %s |", severity, finding) -} - -func GetApplicabilityMarkdownDescription(severity, cve, impactedDependency, finding string) string { - headerRow := "| Severity | Impacted Dependency | Finding | CVE |\n" - separatorRow := "| :--------------: | :---: | :---: | :---: |\n" - return headerRow + separatorRow + fmt.Sprintf("| %s | %s | %s | %s |", severity, impactedDependency, finding, cve) +func MarkAsBold(content string) string { + return fmt.Sprintf("**%s**", content) } -func GetLocationDescription(location formats.Location) string { - return fmt.Sprintf(` -%s -at %s (line %d) -`, - MarkAsCodeSnippet(location.Snippet), - MarkAsQuote(location.File), - location.StartLine) +func MarkAsQuote(content string) string { + return fmt.Sprintf("`%s`", content) } -func getVulnerabilitiesTableHeader(showCaColumn bool) string { - if showCaColumn { - return vulnerabilitiesTableHeaderWithContextualAnalysis - } - return vulnerabilitiesTableHeader +func MarkAsLink(content, link string) string { + return fmt.Sprintf("[%s](%s)", content, link) } -func convertCveRowsToCveIds(cveRows []formats.CveRow, seperator string) string { - cvesBuilder := strings.Builder{} - for _, cve := range cveRows { - if cve.Id != "" { - cvesBuilder.WriteString(fmt.Sprintf("%s%s", cve.Id, seperator)) - } - } - return strings.TrimSuffix(cvesBuilder.String(), seperator) -} - -func getTableRowCves(row formats.VulnerabilityOrViolationRow, writer OutputWriter) string { - cves := convertCveRowsToCveIds(row.Cves, writer.Separator()) - if cves == "" { - cves = " - " - } - return cves +func SectionDivider() string { + return "\n---" } -func GetTableRowsFixedVersions(row formats.VulnerabilityOrViolationRow, writer OutputWriter) string { - fixedVersions := strings.Join(row.FixedVersions, writer.Separator()) - if fixedVersions == "" { - fixedVersions = " - " - } - return strings.TrimSuffix(fixedVersions, writer.Separator()) +func MarkAsCodeSnippet(snippet string) string { + return fmt.Sprintf("```\n%s\n```", snippet) } -func getVulnerabilityDescriptionIdentifier(cveRows []formats.CveRow, xrayId string) string { - identifier := xrayutils.GetIssueIdentifier(cveRows, xrayId) - if identifier == "" { - return "" +func WriteContent(builder *strings.Builder, contents ...string) { + for _, content := range contents { + fmt.Fprintf(builder, "\n%s", content) } - return fmt.Sprintf("[ %s ] ", identifier) -} - -func GenerateReviewCommentContent(content string, writer OutputWriter) string { - return MarkdownComment(ReviewCommentId) + content + writer.Footer() } -func GetFallbackReviewCommentContent(content string, location formats.Location, writer OutputWriter) string { - return MarkdownComment(ReviewCommentId) + GetLocationDescription(location) + content + writer.Footer() +func WriteNewLine(builder *strings.Builder) { + builder.WriteString("\n") } diff --git a/utils/outputwriter/outputwriter_test.go b/utils/outputwriter/outputwriter_test.go index db4a06df5..f6d622c8f 100644 --- a/utils/outputwriter/outputwriter_test.go +++ b/utils/outputwriter/outputwriter_test.go @@ -1,9 +1,10 @@ package outputwriter import ( - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" - "github.com/stretchr/testify/assert" + "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestMarkdownComment(t *testing.T) { @@ -18,49 +19,23 @@ func TestMarkdownComment(t *testing.T) { assert.Equal(t, expected, result) } -func testGetLicensesTableContent(t *testing.T, writer OutputWriter) { - licenses := []formats.LicenseRow{} - result := getLicensesTableContent(licenses, writer) - expected := "" - assert.Equal(t, expected, result) - - // Single license with components - licenses = []formats.LicenseRow{ - { - LicenseKey: "License1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{{Name: "Comp1", Version: "1.0"}}, - ImpactedDependencyName: "Dep1", - ImpactedDependencyVersion: "2.0", - }, - }, - } - result = getLicensesTableContent(licenses, writer) - expected = "\n| License1 | Comp1 1.0 | Dep1 2.0 |" - assert.Equal(t, expected, result) - - // Test case 3: Multiple licenses with components - licenses = []formats.LicenseRow{ +func TestMarkAsBold(t *testing.T) { + testCases := []struct { + input string + expectedOutput string + }{ { - LicenseKey: "License1", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{{Name: "Comp1", Version: "1.0"}}, - ImpactedDependencyName: "Dep1", - ImpactedDependencyVersion: "2.0", - }, + input: "", + expectedOutput: "****", }, { - LicenseKey: "License2", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - Components: []formats.ComponentRow{{Name: "Comp2", Version: "2.0"}}, - ImpactedDependencyName: "Dep2", - ImpactedDependencyVersion: "3.0", - }, + input: "bold", + expectedOutput: "**bold**", }, } - result = getLicensesTableContent(licenses, writer) - expected = "\n| License1 | Comp1 1.0 | Dep1 2.0 |\n| License2 | Comp2 2.0 | Dep2 3.0 |" - assert.Equal(t, expected, result) + for _, tc := range testCases { + assert.Equal(t, tc.expectedOutput, MarkAsBold(tc.input)) + } } func TestMarkAsQuote(t *testing.T) { @@ -82,98 +57,82 @@ func TestMarkAsQuote(t *testing.T) { } } -func TestMarkAsCodeSnippet(t *testing.T) { +func TestMarkAsLink(t *testing.T) { testCases := []struct { - input string + content string + url string expectedOutput string }{ { - input: "", - expectedOutput: "```\n\n```", + content: "", + url: "", + expectedOutput: "[]()", }, { - input: "snippet", - expectedOutput: "```\nsnippet\n```", + content: "content", + url: "", + expectedOutput: "[content]()", }, - } - for _, tc := range testCases { - assert.Equal(t, tc.expectedOutput, MarkAsCodeSnippet(tc.input)) - } -} - -func TestGetLocationDescription(t *testing.T) { - testCases := []struct { - input formats.Location - expectedOutput string - }{ { - input: formats.Location{ - File: "file1", - StartLine: 1, - Snippet: "snippet", - }, - expectedOutput: "\n```\nsnippet\n```\nat `file1` (line 1)\n", + content: "", + url: "url", + expectedOutput: "[](url)", }, { - input: formats.Location{ - File: "dir/other-dir/file1", - StartLine: 134, - Snippet: "clientTestUtils.ChangeDirAndAssert(t, prevWd)", - }, - expectedOutput: "\n```\nclientTestUtils.ChangeDirAndAssert(t, prevWd)\n```\nat `dir/other-dir/file1` (line 134)\n", + content: "content", + url: "url", + expectedOutput: "[content](url)", }, } for _, tc := range testCases { - assert.Equal(t, tc.expectedOutput, GetLocationDescription(tc.input)) + assert.Equal(t, tc.expectedOutput, MarkAsLink(tc.content, tc.url)) } } -func TestGetJasMarkdownDescription(t *testing.T) { +func TestSectionDivider(t *testing.T) { + assert.Equal(t, "\n---", SectionDivider()) +} + +func TestMarkAsCodeSnippet(t *testing.T) { testCases := []struct { - severity string - finding string + input string expectedOutput string }{ { - severity: "High", - finding: "finding", - expectedOutput: "| Severity | Finding |\n| :--------------: | :---: |\n| High | finding |", + input: "", + expectedOutput: "```\n\n```", }, { - severity: "Low", - finding: "finding (other)", - expectedOutput: "| Severity | Finding |\n| :--------------: | :---: |\n| Low | finding (other) |", + input: "snippet", + expectedOutput: "```\nsnippet\n```", }, } for _, tc := range testCases { - assert.Equal(t, tc.expectedOutput, GetJasMarkdownDescription(tc.severity, tc.finding)) + assert.Equal(t, tc.expectedOutput, MarkAsCodeSnippet(tc.input)) } } -func TestGetApplicabilityMarkdownDescription(t *testing.T) { +func TestWriteContent(t *testing.T) { testCases := []struct { - severity string - cve string - impactedDependency string - finding string - expectedOutput string + expectedOutput string + input []string }{ { - severity: "High", - cve: "CVE-100-234", - impactedDependency: "dependency:1.0.0", - finding: "applicable finding", - expectedOutput: "| Severity | Impacted Dependency | Finding | CVE |\n| :--------------: | :---: | :---: | :---: |\n| High | dependency:1.0.0 | applicable finding | CVE-100-234 |", + input: []string{}, + expectedOutput: "", + }, + { + input: []string{"content"}, + expectedOutput: "\ncontent", }, { - severity: "Low", - cve: "CVE-222-233", - impactedDependency: "dependency:3.4.1", - finding: "applicable finding (diff)", - expectedOutput: "| Severity | Impacted Dependency | Finding | CVE |\n| :--------------: | :---: | :---: | :---: |\n| Low | dependency:3.4.1 | applicable finding (diff) | CVE-222-233 |", + input: []string{"contentA", "contentB", "contentC"}, + expectedOutput: "\ncontentA\ncontentB\ncontentC", }, } for _, tc := range testCases { - assert.Equal(t, tc.expectedOutput, GetApplicabilityMarkdownDescription(tc.severity, tc.cve, tc.impactedDependency, tc.finding)) + builder := &strings.Builder{} + WriteContent(builder, tc.input...) + assert.Equal(t, tc.expectedOutput, builder.String()) } } diff --git a/utils/outputwriter/simplifiedoutput.go b/utils/outputwriter/simplifiedoutput.go index fbdeba32d..5a9ee5e72 100644 --- a/utils/outputwriter/simplifiedoutput.go +++ b/utils/outputwriter/simplifiedoutput.go @@ -3,282 +3,36 @@ package outputwriter import ( "fmt" "strings" - - "github.com/jfrog/froggit-go/vcsutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" ) const ( - directDependencyRow = "| | %s | | |" - directDependencyRowWithJas = "| | | %s | | |" + simpleSeparator = ", " ) type SimplifiedOutput struct { - showCaColumn bool - entitledForJas bool - vcsProvider vcsutils.VcsProvider -} - -func (smo *SimplifiedOutput) VulnerabilitiesTableRow(vulnerability formats.VulnerabilityOrViolationRow) string { - row := fmt.Sprintf("| %s | ", smo.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)) - directsRowFmt := directDependencyRow - if smo.showCaColumn { - row += vulnerability.Applicable + " |" - directsRowFmt = directDependencyRowWithJas - } - var firstDirectDependency string - if len(vulnerability.Components) > 0 { - firstDirectDependency = fmt.Sprintf("%s:%s", vulnerability.Components[0].Name, vulnerability.Components[0].Version) - } - - cves := getTableRowCves(vulnerability, smo) - fixedVersions := GetTableRowsFixedVersions(vulnerability, smo) - row += fmt.Sprintf(" %s | %s | %s | %s |", - firstDirectDependency, - fmt.Sprintf("%s:%s", vulnerability.ImpactedDependencyName, vulnerability.ImpactedDependencyVersion), - fixedVersions, - cves, - ) - for i := 1; i < len(vulnerability.Components); i++ { - currDirect := vulnerability.Components[i] - row += "\n" + fmt.Sprintf(directsRowFmt, fmt.Sprintf("%s:%s", currDirect.Name, currDirect.Version)) - } - return row -} - -func (smo *SimplifiedOutput) NoVulnerabilitiesTitle() string { - return GetSimplifiedTitle(NoVulnerabilityPrBannerSource) -} - -func (smo *SimplifiedOutput) VulnerabilitiesTitle(isComment bool) string { - if isComment { - return GetSimplifiedTitle(VulnerabilitiesPrBannerSource) - } - return GetSimplifiedTitle(VulnerabilitiesFixPrBannerSource) -} - -func (smo *SimplifiedOutput) IsFrogbotResultComment(comment string) bool { - return strings.HasPrefix(comment, GetSimplifiedTitle(NoVulnerabilityPrBannerSource)) || strings.HasPrefix(comment, GetSimplifiedTitle(VulnerabilitiesPrBannerSource)) -} - -func (smo *SimplifiedOutput) SetVcsProvider(provider vcsutils.VcsProvider) { - smo.vcsProvider = provider -} - -func (smo *SimplifiedOutput) VcsProvider() vcsutils.VcsProvider { - return smo.vcsProvider -} - -func (smo *SimplifiedOutput) SetJasOutputFlags(entitled, showCaColumn bool) { - smo.entitledForJas = entitled - smo.showCaColumn = showCaColumn + MarkdownOutput } -func (smo *SimplifiedOutput) VulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow) string { - if len(vulnerabilities) == 0 { - return "" - } - - var contentBuilder strings.Builder - // Write summary table part - contentBuilder.WriteString(fmt.Sprintf(` ---- -%s ---- - -%s - -%s %s -`, - - vulnerableDependenciesTitle, - summaryTitle, - getVulnerabilitiesTableHeader(smo.showCaColumn), - getVulnerabilitiesTableContent(vulnerabilities, smo))) - - // Write for each vulnerability details part - var descriptionContentBuilder strings.Builder - descriptionContentBuilder.WriteString(fmt.Sprintf(` ---- -%s ---- - -`, - researchDetailsTitle)) - - shouldOutputDescriptionSection := false - for i := range vulnerabilities { - vulDescriptionContent := createVulnerabilityDescription(&vulnerabilities[i]) - if strings.TrimSpace(vulDescriptionContent) == "" { - // No content - continue - } - shouldOutputDescriptionSection = true - descriptionContentBuilder.WriteString(fmt.Sprintf(` -#### %s%s %s - -%s -`, - getVulnerabilityDescriptionIdentifier(vulnerabilities[i].Cves, vulnerabilities[i].IssueId), - vulnerabilities[i].ImpactedDependencyName, - vulnerabilities[i].ImpactedDependencyVersion, - vulDescriptionContent)) - } - - if shouldOutputDescriptionSection { - return contentBuilder.String() + descriptionContentBuilder.String() - } - return contentBuilder.String() -} - -func (smo *SimplifiedOutput) ApplicableCveReviewContent(severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string) string { - var contentBuilder strings.Builder - contentBuilder.WriteString(fmt.Sprintf(` -## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability - -%s - -### Description - -%s - -### CVE details - -%s - -`, - GetApplicabilityMarkdownDescription(smo.FormattedSeverity(severity, "Applicable"), cve, impactedDependency, finding), - fullDetails, - cveDetails)) - - if len(remediation) > 0 { - contentBuilder.WriteString(fmt.Sprintf(` -### Remediation - -%s - -`, - remediation)) - } - return contentBuilder.String() -} - -func (smo *SimplifiedOutput) IacReviewContent(severity, finding, fullDetails string) string { - return fmt.Sprintf(` -%s - -%s - -%s - -%s - -`, - iacTitle, - GetJasMarkdownDescription(smo.FormattedSeverity(severity, "Applicable"), finding), - researchDetailsTitle, - fullDetails) -} - -func (smo *SimplifiedOutput) SastReviewContent(severity, finding, fullDetails string, codeFlows [][]formats.Location) string { - var contentBuilder strings.Builder - contentBuilder.WriteString(fmt.Sprintf(` -## šŸŽÆ Static Application Security Testing (SAST) Vulnerability - -%s - ---- -### Full description - -%s -`, - GetJasMarkdownDescription(smo.FormattedSeverity(severity, "Applicable"), finding), - fullDetails, - )) - - if len(codeFlows) > 0 { - contentBuilder.WriteString(` ---- -### Code Flows -`) - for _, flow := range codeFlows { - contentBuilder.WriteString(` - ---- -Vulnerable data flow analysis result: -`) - for _, location := range flow { - contentBuilder.WriteString(fmt.Sprintf(` -%s %s (at %s line %d) -`, - "ā†˜ļø", - MarkAsQuote(location.Snippet), - location.File, - location.StartLine, - )) - } - contentBuilder.WriteString(` - ---- - -`, - ) - } - } - return contentBuilder.String() -} - -func (smo *SimplifiedOutput) LicensesContent(licenses []formats.LicenseRow) string { - if len(licenses) == 0 { - return "" - } - - return fmt.Sprintf(` ---- -%s ---- - -%s -%s - -`, - licenseTitle, - licenseTableHeader, - getLicensesTableContent(licenses, smo)) +func (smo *SimplifiedOutput) Separator() string { + return simpleSeparator } -func (smo *SimplifiedOutput) IacTableContent(iacRows []formats.SourceCodeRow) string { - if len(iacRows) == 0 { - return "" - } - - return fmt.Sprintf(` -%s - -%s %s - -`, - iacTitle, - iacTableHeader, - getIacTableContent(iacRows, smo)) +func (smo *SimplifiedOutput) FormattedSeverity(severity, _ string) string { + return severity } -func (smo *SimplifiedOutput) Footer() string { - return fmt.Sprintf("\n%s", CommentGeneratedByFrogbot) +func (smo *SimplifiedOutput) Image(source ImageSource) string { + return GetSimplifiedTitle(source) } -func (smo *SimplifiedOutput) Separator() string { - return ", " +func (smo *SimplifiedOutput) MarkInCenter(content string) string { + return content } -func (smo *SimplifiedOutput) FormattedSeverity(severity, _ string) string { - return severity +func (smo *SimplifiedOutput) MarkAsDetails(summary string, subTitleDepth int, content string) string { + return fmt.Sprintf("%s\n%s", smo.MarkAsTitle(summary, subTitleDepth), content) } -func (smo *SimplifiedOutput) UntitledForJasMsg() string { - msg := "" - if !smo.entitledForJas { - msg = "\n\n---\n**Frogbot** also supports **Contextual Analysis, Secret Detection and IaC Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/xray/) package, which isn't enabled on your system.\n" - } - return msg +func (smo *SimplifiedOutput) MarkAsTitle(title string, subTitleDepth int) string { + 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 663a5c35d..53a9c7ee9 100644 --- a/utils/outputwriter/simplifiedoutput_test.go +++ b/utils/outputwriter/simplifiedoutput_test.go @@ -1,449 +1,217 @@ package outputwriter import ( - "fmt" "testing" - "github.com/jfrog/froggit-go/vcsutils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/stretchr/testify/assert" ) -func TestSimplifiedOutput_VulnerabilitiesTableRow(t *testing.T) { - type testCase struct { - name string - vulnerability formats.VulnerabilityOrViolationRow - expectedOutput string - showCaColumn bool - } - - testCases := []testCase{ - { - name: "Single CVE and one direct dependency", - vulnerability: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, - ImpactedDependencyName: "impacted_dep", - ImpactedDependencyVersion: "2.0.0", - Components: []formats.ComponentRow{ - {Name: "dep1", Version: "1.0.0"}, - }, - }, - FixedVersions: []string{"3.0.0"}, - Cves: []formats.CveRow{ - {Id: "CVE-2022-0001"}, - }, - Technology: coreutils.Nuget, - }, - expectedOutput: "| High | dep1:1.0.0 | impacted_dep:2.0.0 | 3.0.0 | CVE-2022-0001 |", - }, - { - name: "No CVE and multiple direct dependencies", - vulnerability: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low"}, - ImpactedDependencyName: "impacted_dep", - ImpactedDependencyVersion: "3.0.0", - Components: []formats.ComponentRow{ - {Name: "dep1", Version: "1.0.0"}, - {Name: "dep2", Version: "2.0.0"}, - }, - }, - FixedVersions: []string{"4.0.0", "4.1.0", "4.2.0", "5.0.0"}, - Cves: []formats.CveRow{}, - Technology: coreutils.Dotnet, - }, - expectedOutput: "| Low | dep1:1.0.0 | impacted_dep:3.0.0 | 4.0.0, 4.1.0, 4.2.0, 5.0.0 | - |\n| | dep2:2.0.0 | | |", - }, - { - name: "Multiple CVEs", - vulnerability: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Critical"}, - ImpactedDependencyName: "impacted_dep", - ImpactedDependencyVersion: "4.0.0", - Components: []formats.ComponentRow{{Name: "direct", Version: "1.0.2"}}, - }, - Applicable: "Applicable", - FixedVersions: []string{"5.0.0", "6.0.0"}, - Cves: []formats.CveRow{ - {Id: "CVE-2022-0002"}, - {Id: "CVE-2022-0003"}, - }, - Technology: coreutils.Pip, - }, - expectedOutput: "| Critical | Applicable | direct:1.0.2 | impacted_dep:4.0.0 | 5.0.0, 6.0.0 | CVE-2022-0002, CVE-2022-0003 |", - showCaColumn: true, - }, +func TestSimpleOutputFlags(t *testing.T) { + testCases := []struct { + name string + entitled bool + showCaColumn bool + }{ + {name: "entitled", entitled: true, showCaColumn: false}, + {name: "not entitled", entitled: false, showCaColumn: false}, + {name: "entitled with ca column", entitled: true, showCaColumn: true}, + {name: "not entitled with ca column", entitled: false, showCaColumn: true}, } - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - smo := &SimplifiedOutput{entitledForJas: true, showCaColumn: tc.showCaColumn} - actualOutput := smo.VulnerabilitiesTableRow(tc.vulnerability) - assert.Equal(t, tc.expectedOutput, actualOutput) + smo := &SimplifiedOutput{} + smo.SetJasOutputFlags(tc.entitled, tc.showCaColumn) + assert.Equal(t, tc.entitled, smo.entitledForJas) + assert.Equal(t, tc.showCaColumn, smo.showCaColumn) + assert.Equal(t, tc.entitled, smo.IsEntitledForJas()) + assert.Equal(t, tc.showCaColumn, smo.IsShowingCaColumn()) }) } } -func TestSimplifiedOutput_IsFrogbotResultComment(t *testing.T) { +func TestSimpleSeparator(t *testing.T) { + smo := &SimplifiedOutput{} + assert.Equal(t, ", ", smo.Separator()) +} + +func TestSimpleFormattedSeverity(t *testing.T) { testCases := []struct { - name string - comment string - expected bool + name string + severity string + applicability string + expectedOutput string }{ { - name: "Starts with No Vulnerability Banner", - comment: "**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.** \n", - expected: true, - }, - { - name: "Starts with Vulnerabilities Banner", - comment: "**šŸšØ Frogbot scanned this pull request and found the below:**\n", - expected: true, + name: "Applicable severity", + severity: "Low", + applicability: "Applicable", + expectedOutput: "Low", }, { - name: "Does not start with Banner", - comment: "This is a random comment.", - expected: false, + name: "Not applicable severity", + severity: "Medium", + applicability: "Not Applicable", + expectedOutput: "Medium", }, } - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { smo := &SimplifiedOutput{} - actual := smo.IsFrogbotResultComment(tc.comment) - assert.Equal(t, tc.expected, actual) + assert.Equal(t, tc.expectedOutput, smo.FormattedSeverity(tc.severity, tc.applicability)) }) } } -func TestSimplifiedOutput_VulnerabilitiesContent(t *testing.T) { - // Create a new instance of StandardOutput - so := &SimplifiedOutput{} - - // Create some sample vulnerabilitiesRows for testing - vulnerabilitiesRows := []formats.VulnerabilityOrViolationRow{ +func TestSimpleImage(t *testing.T) { + testCases := []struct { + name string + source ImageSource + expectedOutput string + }{ { - Summary: "CVE-2023-1234 summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Critical"}, - ImpactedDependencyName: "Dependency1", - ImpactedDependencyVersion: "1.0.0", - Components: []formats.ComponentRow{{Name: "Direct1", Version: "1.0.0"}, {Name: "Direct2", Version: "2.0.0"}}, - }, - FixedVersions: []string{"2.2.3"}, - Cves: []formats.CveRow{{Id: "CVE-2023-1234"}}, + name: "no vulnerability pr banner", + source: NoVulnerabilityPrBannerSource, + expectedOutput: "**šŸ‘ Frogbot scanned this pull request and found that it did not add vulnerable dependencies.**", }, { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, - ImpactedDependencyName: "Dependency2", - ImpactedDependencyVersion: "2.0.0", - Components: []formats.ComponentRow{{Name: "Direct1", Version: "1.0.0"}, {Name: "Direct2", Version: "2.0.0"}}, - }, - FixedVersions: []string{"2.2.3"}, - Cves: []formats.CveRow{{Id: "CVE-2023-1234"}}, - }, - } - - // Set the expected content string based on the sample data - expectedContent := fmt.Sprintf(` ---- -## šŸ“¦ Vulnerable Dependencies ---- - -### āœļø Summary - -%s %s - ---- -## šŸ”¬ Research Details ---- - - -#### %s %s %s - -%s -`, - getVulnerabilitiesTableHeader(false), - getVulnerabilitiesTableContent(vulnerabilitiesRows, so), - fmt.Sprintf("[ %s ]", vulnerabilitiesRows[0].Cves[0].Id), - vulnerabilitiesRows[0].ImpactedDependencyName, - vulnerabilitiesRows[0].ImpactedDependencyVersion, - createVulnerabilityDescription(&vulnerabilitiesRows[0]), - ) - - actualContent := so.VulnerabilitiesContent(vulnerabilitiesRows) - assert.Equal(t, expectedContent, actualContent, "Content mismatch") - - vulnerabilitiesRows = []formats.VulnerabilityOrViolationRow{} - expectedContent = "" - actualContent = so.VulnerabilitiesContent(vulnerabilitiesRows) - assert.Equal(t, expectedContent, actualContent, "Content mismatch") -} - -func TestSimplifiedOutput_ContentWithContextualAnalysis(t *testing.T) { - // Create a new instance of StandardOutput - so := &SimplifiedOutput{entitledForJas: true, vcsProvider: vcsutils.BitbucketServer, showCaColumn: true} - - vulnerabilitiesRows := []formats.VulnerabilityOrViolationRow{ - { - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, - ImpactedDependencyName: "Dependency1", - ImpactedDependencyVersion: "1.0.0", - Components: []formats.ComponentRow{{Name: "Direct1", Version: "1.0.0"}, {Name: "Direct2", Version: "2.0.0"}}, - }, - FixedVersions: []string{"2.2.3"}, - Cves: []formats.CveRow{{Id: "CVE-2023-1234"}}, - Applicable: utils.Applicable.String(), - Technology: coreutils.Npm, + name: "vulnerabilities pr banner", + source: VulnerabilitiesPrBannerSource, + expectedOutput: "**šŸšØ Frogbot scanned this pull request and found the below:**", }, { - Summary: "CVE-2023-1234 summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low"}, - ImpactedDependencyName: "Dependency2", - ImpactedDependencyVersion: "2.0.0", - Components: []formats.ComponentRow{{Name: "Direct1", Version: "1.0.0"}, {Name: "Direct2", Version: "2.0.0"}}, - }, - FixedVersions: []string{"2.2.3"}, - Cves: []formats.CveRow{{Id: "CVE-2024-1234"}}, - Applicable: "Not Applicable", - Technology: coreutils.Poetry, + name: "vulnerabilities fix pr banner", + source: VulnerabilitiesFixPrBannerSource, + expectedOutput: "**šŸšØ This automated pull request was created by Frogbot and fixes the below:**", }, } - - expectedContent := fmt.Sprintf(` ---- -## šŸ“¦ Vulnerable Dependencies ---- - -### āœļø Summary - -%s %s - ---- -## šŸ”¬ Research Details ---- - - -#### %s %s %s - -%s -`, - getVulnerabilitiesTableHeader(true), - getVulnerabilitiesTableContent(vulnerabilitiesRows, so), - fmt.Sprintf("[ %s ]", "CVE-2024-1234"), - vulnerabilitiesRows[1].ImpactedDependencyName, - vulnerabilitiesRows[1].ImpactedDependencyVersion, - createVulnerabilityDescription(&vulnerabilitiesRows[1]), - ) - - actualContent := so.VulnerabilitiesContent(vulnerabilitiesRows) - assert.Equal(t, expectedContent, actualContent, "Content mismatch") - assert.Contains(t, actualContent, "CONTEXTUAL ANALYSIS") - assert.Contains(t, actualContent, "| Applicable |") - assert.Contains(t, actualContent, "| Not Applicable |") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + smo := &SimplifiedOutput{} + assert.Equal(t, tc.expectedOutput, smo.Image(tc.source)) + }) + } } -func TestSimplifiedOutput_IacContent(t *testing.T) { +func TestSimpleMarkInCenter(t *testing.T) { testCases := []struct { name string - iacRows []formats.SourceCodeRow + content string expectedOutput string }{ { - name: "Empty IAC rows", - iacRows: []formats.SourceCodeRow{}, + name: "empty content", + content: "", expectedOutput: "", }, { - name: "Single IAC row", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "applicable/req_sw_terraform_azure_redis_auth.tf", - StartLine: 11, - StartColumn: 1, - Snippet: "Missing Periodic patching was detected", - }, - }, - }, - expectedOutput: "\n## šŸ› ļø Infrastructure as Code\n\n\n| SEVERITY | FILE | LINE:COLUMN | FINDING |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | \n| High | applicable/req_sw_terraform_azure_redis_auth.tf | 11:1 | Missing Periodic patching was detected |\n\n", - }, - { - name: "Multiple IAC rows", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "applicable/req_sw_terraform_azure_redis_patch.tf", - StartLine: 11, - StartColumn: 1, - Snippet: "Missing redis firewall definition or start_ip=0.0.0.0 was detected, Missing redis firewall definition or start_ip=0.0.0.0 was detected", - }, - }, - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "applicable/req_sw_terraform_azure_redis_auth.tf", - StartLine: 11, - StartColumn: 1, - Snippet: "Missing Periodic patching was detected", - }, - }, - }, - expectedOutput: "\n## šŸ› ļø Infrastructure as Code\n\n\n| SEVERITY | FILE | LINE:COLUMN | FINDING |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | \n| High | applicable/req_sw_terraform_azure_redis_patch.tf | 11:1 | Missing redis firewall definition or start_ip=0.0.0.0 was detected, Missing redis firewall definition or start_ip=0.0.0.0 was detected |\n| High | applicable/req_sw_terraform_azure_redis_auth.tf | 11:1 | Missing Periodic patching was detected |\n\n", + name: "non empty content", + content: "content", + expectedOutput: "content", }, } - - writer := &SimplifiedOutput{} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := writer.IacTableContent(tc.iacRows) - assert.Equal(t, tc.expectedOutput, output) + smo := &SimplifiedOutput{} + assert.Equal(t, tc.expectedOutput, smo.MarkInCenter(tc.content)) }) } } -func TestSimplifiedOutput_GetIacTableContent(t *testing.T) { +func TestSimpleMarkAsDetails(t *testing.T) { testCases := []struct { name string - iacRows []formats.SourceCodeRow + summary string + content string expectedOutput string + subTitleDepth int }{ { - name: "Empty IAC rows", - iacRows: []formats.SourceCodeRow{}, - expectedOutput: "", + name: "empty", + summary: "", + subTitleDepth: 0, + content: "", + expectedOutput: "\n---\n \n\n---\n", + }, + { + name: "empty content", + summary: "summary", + subTitleDepth: 1, + content: "", + expectedOutput: "\n---\n# summary\n\n---\n", + }, + { + name: "empty summary", + summary: "", + subTitleDepth: 1, + content: "content", + expectedOutput: "\n---\n# \n\n---\ncontent", }, { - name: "Single IAC row", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 2}, - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - Snippet: "Public access to MySQL was detected", - }, - }, - }, - expectedOutput: "\n| Medium | file1 | 1:10 | Public access to MySQL was detected |", + name: "Main details", + summary: "summary", + subTitleDepth: 1, + content: "content", + expectedOutput: "\n---\n# summary\n\n---\ncontent", }, { - name: "Multiple IAC rows", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - Snippet: "Public access to MySQL was detected", - }, - }, - { - SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 2}, - Location: formats.Location{ - File: "file2", - StartLine: 2, - StartColumn: 5, - Snippet: "Public access to MySQL was detected", - }, - }, - }, - expectedOutput: "\n| High | file1 | 1:10 | Public access to MySQL was detected |\n| Medium | file2 | 2:5 | Public access to MySQL was detected |", + name: "Sub details", + summary: "summary", + subTitleDepth: 2, + content: "content", + expectedOutput: "\n---\n## summary\n\n---\ncontent", + }, + { + name: "Sub sub details", + summary: "summary", + subTitleDepth: 3, + content: "content", + expectedOutput: "\n---\n### summary\n\n---\ncontent", }, } - for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := getIacTableContent(tc.iacRows, &SimplifiedOutput{}) - assert.Equal(t, tc.expectedOutput, output) + smo := &SimplifiedOutput{} + assert.Equal(t, tc.expectedOutput, smo.MarkAsDetails(tc.summary, tc.subTitleDepth, tc.content)) }) } } -func TestSimplifiedOutput_GetLicensesTableContent(t *testing.T) { - writer := &SimplifiedOutput{} - testGetLicensesTableContent(t, writer) -} - -func TestSimplifiedOutput_SastReviewContent(t *testing.T) { +func TestSimpleMarkAsTitle(t *testing.T) { testCases := []struct { name string - severity string - finding string - fullDetails string + title string expectedOutput string - codeFlows [][]formats.Location + subTitleDepth int }{ { - 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{ - { - { - 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", - }, - }, - }, - expectedOutput: "\n## šŸŽÆ Static Application Security Testing (SAST) Vulnerability\n\t\n| Severity | Finding |\n| :--------------: | :---: |\n| Low | Stack Trace Exposure |\n\n---\n### Full description\n\n\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.\n\n---\n### Code Flows\n\n\n---\nVulnerable data flow analysis result:\n\nā†˜ļø `other-snippet` (at file2 line 1)\n\nā†˜ļø `snippet` (at file line 0)\n\n\n---\n\n\n\n---\nVulnerable data flow analysis result:\n\nā†˜ļø `a-snippet` (at file line 10)\n\nā†˜ļø `snippet` (at file line 0)\n\n\n---\n\n", + name: "empty", + title: "", + subTitleDepth: 0, + expectedOutput: "\n---\n \n\n---", }, { - 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.", - expectedOutput: "\n## šŸŽÆ Static Application Security Testing (SAST) Vulnerability\n\t\n| Severity | Finding |\n| :--------------: | :---: |\n| Low | Stack Trace Exposure |\n\n---\n### Full description\n\n\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.\n", + name: "Main title", + title: "title", + subTitleDepth: 1, + expectedOutput: "\n---\n# title\n\n---", + }, + { + name: "Sub title", + title: "title", + subTitleDepth: 2, + expectedOutput: "\n---\n## title\n\n---", + }, + { + name: "Sub sub title", + title: "title", + subTitleDepth: 3, + expectedOutput: "\n---\n### title\n\n---", }, } - - so := &SimplifiedOutput{} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := so.SastReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.codeFlows) - assert.Equal(t, tc.expectedOutput, output) + smo := &SimplifiedOutput{} + assert.Equal(t, tc.expectedOutput, smo.MarkAsTitle(tc.title, tc.subTitleDepth)) }) } } diff --git a/utils/outputwriter/standardoutput.go b/utils/outputwriter/standardoutput.go index e049ef4f9..2cf65856b 100644 --- a/utils/outputwriter/standardoutput.go +++ b/utils/outputwriter/standardoutput.go @@ -3,307 +3,10 @@ package outputwriter import ( "fmt" "strings" - - "github.com/jfrog/froggit-go/vcsutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" ) type StandardOutput struct { - showCaColumn bool - entitledForJas bool - vcsProvider vcsutils.VcsProvider -} - -func (so *StandardOutput) VulnerabilitiesTableRow(vulnerability formats.VulnerabilityOrViolationRow) string { - var directDependencies strings.Builder - for _, dependency := range vulnerability.Components { - directDependencies.WriteString(fmt.Sprintf("%s:%s%s", dependency.Name, dependency.Version, so.Separator())) - } - - row := fmt.Sprintf("| %s | ", so.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)) - if so.showCaColumn { - row += vulnerability.Applicable + " | " - } - cves := getTableRowCves(vulnerability, so) - fixedVersions := GetTableRowsFixedVersions(vulnerability, so) - row += fmt.Sprintf("%s | %s | %s | %s |", - strings.TrimSuffix(directDependencies.String(), so.Separator()), - fmt.Sprintf("%s:%s", vulnerability.ImpactedDependencyName, vulnerability.ImpactedDependencyVersion), - fixedVersions, - cves, - ) - return row -} - -func (so *StandardOutput) NoVulnerabilitiesTitle() string { - if so.vcsProvider == vcsutils.GitLab { - return GetBanner(NoVulnerabilityMrBannerSource) - } - return GetBanner(NoVulnerabilityPrBannerSource) -} - -func (so *StandardOutput) VulnerabilitiesTitle(isComment bool) string { - var banner string - switch { - case isComment && so.vcsProvider == vcsutils.GitLab: - banner = GetBanner(VulnerabilitiesMrBannerSource) - case isComment && so.vcsProvider != vcsutils.GitLab: - banner = GetBanner(VulnerabilitiesPrBannerSource) - case !isComment && so.vcsProvider == vcsutils.GitLab: - banner = GetBanner(VulnerabilitiesFixMrBannerSource) - case !isComment && so.vcsProvider != vcsutils.GitLab: - banner = GetBanner(VulnerabilitiesFixPrBannerSource) - } - return banner -} - -func (so *StandardOutput) IsFrogbotResultComment(comment string) bool { - return strings.Contains(comment, string(NoVulnerabilityPrBannerSource)) || - strings.Contains(comment, string(VulnerabilitiesPrBannerSource)) || - strings.Contains(comment, string(NoVulnerabilityMrBannerSource)) || - strings.Contains(comment, string(VulnerabilitiesMrBannerSource)) -} - -func (so *StandardOutput) SetVcsProvider(provider vcsutils.VcsProvider) { - so.vcsProvider = provider -} - -func (so *StandardOutput) VcsProvider() vcsutils.VcsProvider { - return so.vcsProvider -} - -func (so *StandardOutput) SetJasOutputFlags(entitled, showCaColumn bool) { - so.entitledForJas = entitled - so.showCaColumn = showCaColumn -} - -func (so *StandardOutput) VulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow) string { - if len(vulnerabilities) == 0 { - return "" - } - var contentBuilder strings.Builder - // Write summary table part - contentBuilder.WriteString(fmt.Sprintf(` -%s - -%s - -
- -%s %s - -
-`, - vulnerableDependenciesTitle, - summaryTitle, - getVulnerabilitiesTableHeader(so.showCaColumn), - getVulnerabilitiesTableContent(vulnerabilities, so))) - // Write for each vulnerability details part - var descriptionContentBuilder strings.Builder - descriptionContentBuilder.WriteString(fmt.Sprintf(` -%s -`, - researchDetailsTitle)) - shouldOutputDescriptionSection := false - for i := range vulnerabilities { - vulDescriptionContent := createVulnerabilityDescription(&vulnerabilities[i]) - if strings.TrimSpace(vulDescriptionContent) == "" { - // No content - continue - } - shouldOutputDescriptionSection = true - if len(vulnerabilities) == 1 { - descriptionContentBuilder.WriteString(fmt.Sprintf(` -%s -`, vulDescriptionContent)) - break - } - descriptionContentBuilder.WriteString(fmt.Sprintf(` -
- %s%s %s -
-%s - -
- -`, - getVulnerabilityDescriptionIdentifier(vulnerabilities[i].Cves, vulnerabilities[i].IssueId), - vulnerabilities[i].ImpactedDependencyName, - vulnerabilities[i].ImpactedDependencyVersion, - vulDescriptionContent)) - } - if shouldOutputDescriptionSection { - return contentBuilder.String() + descriptionContentBuilder.String() - } - return contentBuilder.String() -} - -func (so *StandardOutput) ApplicableCveReviewContent(severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string) string { - var contentBuilder strings.Builder - contentBuilder.WriteString(fmt.Sprintf(` -%s - -
- -%s - -
- -
- Description -
- -%s - -
- -
- CVE details -
- -%s - -
- -`, - contextualAnalysisTitle, - GetApplicabilityMarkdownDescription(so.FormattedSeverity(severity, "Applicable"), cve, impactedDependency, finding), - fullDetails, - cveDetails)) - if len(remediation) > 0 { - contentBuilder.WriteString(fmt.Sprintf(` -
- Remediation -
- -%s - -
- -`, - remediation)) - } - - return contentBuilder.String() -} - -func (so *StandardOutput) IacReviewContent(severity, finding, fullDetails string) string { - return fmt.Sprintf(` -%s - -
- -%s - -
- -
- Full description -
- -%s - -
- -`, - iacTitle, - GetJasMarkdownDescription(so.FormattedSeverity(severity, "Applicable"), finding), - fullDetails) -} - -func (so *StandardOutput) SastReviewContent(severity, finding, fullDetails string, codeFlows [][]formats.Location) string { - var contentBuilder strings.Builder - contentBuilder.WriteString(fmt.Sprintf(` -## šŸŽÆ Static Application Security Testing (SAST) Vulnerability - -
- -%s - -
- -
- Full description -
- -%s - -
- -`, - GetJasMarkdownDescription(so.FormattedSeverity(severity, "Applicable"), finding), - fullDetails, - )) - - if len(codeFlows) > 0 { - contentBuilder.WriteString(` - -
-Code Flows - -`) - for _, flow := range codeFlows { - contentBuilder.WriteString(` - -
-Vulnerable data flow analysis result -
-`) - for _, location := range flow { - contentBuilder.WriteString(fmt.Sprintf(` -%s %s (at %s line %d) -`, - "ā†˜ļø", - MarkAsQuote(location.Snippet), - location.File, - location.StartLine, - )) - } - - contentBuilder.WriteString(` - -
- -`, - ) - } - contentBuilder.WriteString(` - -
- -`, - ) - } - return contentBuilder.String() -} - -func (so *StandardOutput) IacTableContent(iacRows []formats.SourceCodeRow) string { - if len(iacRows) == 0 { - return "" - } - - return fmt.Sprintf(` -## šŸ› ļø Infrastructure as Code - -
- -%s %s - -
- -`, - iacTableHeader, - getIacTableContent(iacRows, so)) -} - -func (so *StandardOutput) Footer() string { - return fmt.Sprintf(` ---- -
- -%s - -
`, CommentGeneratedByFrogbot) + MarkdownOutput } func (so *StandardOutput) Separator() string { @@ -314,37 +17,25 @@ func (so *StandardOutput) FormattedSeverity(severity, applicability string) stri return fmt.Sprintf("%s%8s", getSeverityTag(IconName(severity), applicability), severity) } -func (so *StandardOutput) UntitledForJasMsg() string { - msg := "" - if !so.entitledForJas { - msg = - ` ---- -
- -**Frogbot** also supports **Contextual Analysis, Secret Detection and IaC Vulnerabilities Scanning**. This features are included as part of the [JFrog Advanced Security](https://jfrog.com/xray/) package, which isn't enabled on your system. +func (so *StandardOutput) Image(source ImageSource) string { + return GetBanner(source) +} -
-` - } - return msg +func (so *StandardOutput) MarkInCenter(content string) string { + return GetMarkdownCenterTag(content) } -func (so *StandardOutput) LicensesContent(licenses []formats.LicenseRow) string { - if len(licenses) == 0 { - return "" +func (so *StandardOutput) MarkAsDetails(summary string, subTitleDepth int, content string) string { + if summary != "" { + summary = fmt.Sprintf(" %s \n
\n", summary) } - return fmt.Sprintf(` -%s - -
- -%s %s + return fmt.Sprintf("
\n%s\n%s\n\n
\n", summary, content) +} -
+func (so *StandardOutput) MarkAsTitle(title string, subTitleDepth int) string { + return fmt.Sprintf("%s %s", strings.Repeat("#", subTitleDepth), title) +} -`, - licenseTitle, - licenseTableHeader, - getLicensesTableContent(licenses, so)) +func GetMarkdownCenterTag(content string) string { + return fmt.Sprintf("
\n\n%s\n\n
\n", content) } diff --git a/utils/outputwriter/standardoutput_test.go b/utils/outputwriter/standardoutput_test.go index 5fdb45820..21f55991e 100644 --- a/utils/outputwriter/standardoutput_test.go +++ b/utils/outputwriter/standardoutput_test.go @@ -1,539 +1,243 @@ package outputwriter import ( - "fmt" - "github.com/jfrog/froggit-go/vcsutils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" - "github.com/stretchr/testify/assert" - "strings" "testing" + + "github.com/stretchr/testify/assert" ) -func TestStandardOutput_TableRow(t *testing.T) { - var tests = []struct { - vulnerability formats.VulnerabilityOrViolationRow - expected string - name string +func TestStandardOutputFlags(t *testing.T) { + testCases := []struct { + name string + entitled bool + showCaColumn bool }{ { - name: "Single CVE and no direct dependencies", - vulnerability: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Critical"}, - ImpactedDependencyName: "testdep", - ImpactedDependencyVersion: "1.0.0", - }, - FixedVersions: []string{"2.0.0"}, - Cves: []formats.CveRow{{Id: "CVE-2022-1234"}}, - }, - expected: "| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableCriticalSeverity.png)
Critical | | testdep:1.0.0 | 2.0.0 | CVE-2022-1234 |", + name: "entitled", + entitled: true, + showCaColumn: false, }, { - name: "Multiple CVEs and no direct dependencies", - vulnerability: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, - ImpactedDependencyName: "testdep2", - ImpactedDependencyVersion: "1.0.0", - }, - FixedVersions: []string{"2.0.0", "3.0.0"}, - Cves: []formats.CveRow{ - {Id: "CVE-2022-1234"}, - {Id: "CVE-2022-5678"}, - }, - }, - expected: "| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | | testdep2:1.0.0 | 2.0.0
3.0.0 | CVE-2022-1234
CVE-2022-5678 |", + name: "not entitled", + entitled: false, + showCaColumn: false, }, { - name: "Single CVE and direct dependencies", - vulnerability: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Low"}, - ImpactedDependencyName: "testdep3", - ImpactedDependencyVersion: "1.0.0", - Components: []formats.ComponentRow{ - {Name: "dep1", Version: "1.0.0"}, - {Name: "dep2", Version: "2.0.0"}, - }, - }, - FixedVersions: []string{"2.0.0"}, - Cves: []formats.CveRow{{Id: "CVE-2022-1234"}}, - }, - expected: "| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | dep1:1.0.0
dep2:2.0.0 | testdep3:1.0.0 | 2.0.0 | CVE-2022-1234 |", + name: "entitled with ca column", + entitled: true, + showCaColumn: true, }, { - name: "Multiple CVEs and direct dependencies", - vulnerability: formats.VulnerabilityOrViolationRow{ - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, - ImpactedDependencyName: "impacted", - ImpactedDependencyVersion: "3.0.0", - Components: []formats.ComponentRow{ - {Name: "dep1", Version: "1.0.0"}, - {Name: "dep2", Version: "2.0.0"}, - }, - }, - Cves: []formats.CveRow{ - {Id: "CVE-1"}, - {Id: "CVE-2"}, - }, - FixedVersions: []string{"4.0.0", "5.0.0"}, - }, - expected: "| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | dep1:1.0.0
dep2:2.0.0 | impacted:3.0.0 | 4.0.0
5.0.0 | CVE-1
CVE-2 |", + name: "not entitled with ca column", + entitled: false, + showCaColumn: true, }, } - - for _, tc := range tests { + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { smo := &StandardOutput{} - actualOutput := smo.VulnerabilitiesTableRow(tc.vulnerability) - assert.Equal(t, tc.expected, actualOutput) + smo.SetJasOutputFlags(tc.entitled, tc.showCaColumn) + assert.Equal(t, tc.entitled, smo.entitledForJas) + assert.Equal(t, tc.showCaColumn, smo.showCaColumn) + assert.Equal(t, tc.entitled, smo.IsEntitledForJas()) + assert.Equal(t, tc.showCaColumn, smo.IsShowingCaColumn()) }) } } -func TestStandardOutput_IsFrogbotResultComment(t *testing.T) { - so := &StandardOutput{} +func TestStandardSeparator(t *testing.T) { + smo := &StandardOutput{} + assert.Equal(t, "
", smo.Separator()) +} - tests := []struct { - comment string - expected bool +func TestStandardFormattedSeverity(t *testing.T) { + testCases := []struct { + name string + severity string + applicability string + expectedOutput string }{ { - comment: "This is a comment with the " + GetIconTag(NoVulnerabilityPrBannerSource) + " icon", - expected: true, - }, - { - comment: "This is a comment with the " + GetIconTag(VulnerabilitiesPrBannerSource) + " icon", - expected: true, - }, - { - comment: "This is a comment with the " + GetIconTag(VulnerabilitiesMrBannerSource) + " icon", - expected: true, - }, - { - comment: "This is a comment with the " + GetIconTag(NoVulnerabilityMrBannerSource) + " icon", - expected: true, + name: "Applicable severity", + severity: "Low", + applicability: "Applicable", + expectedOutput: "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low", }, { - comment: "This is a comment with no icons", - expected: false, + name: "Not applicable severity", + severity: "Medium", + applicability: "Not Applicable", + expectedOutput: "![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/notApplicableMedium.png)
Medium", }, } - - for _, test := range tests { - result := so.IsFrogbotResultComment(test.comment) - assert.Equal(t, test.expected, result) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + smo := &StandardOutput{} + assert.Equal(t, tc.expectedOutput, smo.FormattedSeverity(tc.severity, tc.applicability)) + }) } } -func TestStandardOutput_VulnerabilitiesContent(t *testing.T) { - // Create a new instance of StandardOutput - so := &StandardOutput{} - - // Create some sample vulnerabilitiesRows for testing - vulnerabilitiesRows := []formats.VulnerabilityOrViolationRow{ +func TestStandardImage(t *testing.T) { + testCases := []struct { + name string + source ImageSource + expectedOutput string + }{ { - Summary: "CVE-2023-1234 summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "Dependency1", - ImpactedDependencyVersion: "1.0.0", - }, + name: "no vulnerability pr banner", + source: NoVulnerabilityPrBannerSource, + expectedOutput: "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", }, { - Summary: "CVE-2023-1234 summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "Dependency2", - ImpactedDependencyVersion: "2.0.0", - }, + name: "vulnerabilities pr banner", + source: VulnerabilitiesPrBannerSource, + expectedOutput: "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", + }, + { + name: "no vulnerability mr banner", + source: NoVulnerabilityMrBannerSource, + expectedOutput: "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/noVulnerabilityBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", }, - } - - // Set the expected content string based on the sample data - expectedContent := fmt.Sprintf(` -## šŸ“¦ Vulnerable Dependencies - -### āœļø Summary - -
- -%s %s - -
- -## šŸ”¬ Research Details - -
- %s%s %s -
-%s - -
- - -
- %s%s %s -
-%s - -
- -`, - getVulnerabilitiesTableHeader(false), - getVulnerabilitiesTableContent(vulnerabilitiesRows, so), - "", - vulnerabilitiesRows[0].ImpactedDependencyName, - vulnerabilitiesRows[0].ImpactedDependencyVersion, - createVulnerabilityDescription(&vulnerabilitiesRows[0]), - "", - vulnerabilitiesRows[1].ImpactedDependencyName, - vulnerabilitiesRows[1].ImpactedDependencyVersion, - createVulnerabilityDescription(&vulnerabilitiesRows[1]), - ) - - actualContent := so.VulnerabilitiesContent(vulnerabilitiesRows) - assert.Equal(t, expectedContent, actualContent, "Content mismatch") -} - -func TestStandardOutput_ContentWithContextualAnalysis(t *testing.T) { - // Create a new instance of StandardOutput - so := &StandardOutput{entitledForJas: true, vcsProvider: vcsutils.GitHub, showCaColumn: true} - - vulnerabilitiesRows := []formats.VulnerabilityOrViolationRow{} - expectedContent := "" - actualContent := so.VulnerabilitiesContent(vulnerabilitiesRows) - assert.Equal(t, expectedContent, actualContent) - - // Create some sample vulnerabilitiesRows for testing - vulnerabilitiesRows = []formats.VulnerabilityOrViolationRow{ { - Summary: "CVE-2023-1234 summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "Dependency1", - ImpactedDependencyVersion: "1.0.0", - }, - Applicable: "Applicable", - Technology: coreutils.Pip, - Cves: []formats.CveRow{{Id: "CVE-2023-1234"}, {Id: "CVE-2023-4321"}}, + name: "vulnerabilities fix pr banner", + source: VulnerabilitiesFixPrBannerSource, + expectedOutput: "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", }, { - Summary: "CVE-2023-1234 summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - ImpactedDependencyName: "Dependency2", - ImpactedDependencyVersion: "2.0.0", - }, - Applicable: "Not Applicable", - Technology: coreutils.Pip, - Cves: []formats.CveRow{{Id: "CVE-2022-4321"}}, + name: "vulnerabilities fix mr banner", + source: VulnerabilitiesFixMrBannerSource, + expectedOutput: "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesFixBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n", }, } - - // Set the expected content string based on the sample data - expectedContent = fmt.Sprintf(` -## šŸ“¦ Vulnerable Dependencies - -### āœļø Summary - -
- -%s %s - -
- -## šŸ”¬ Research Details - -
- %s%s %s -
-%s - -
- - -
- %s%s %s -
-%s - -
- -`, - getVulnerabilitiesTableHeader(true), - getVulnerabilitiesTableContent(vulnerabilitiesRows, so), - fmt.Sprintf("[ %s ] ", strings.Join([]string{vulnerabilitiesRows[0].Cves[0].Id, vulnerabilitiesRows[0].Cves[1].Id}, ", ")), - vulnerabilitiesRows[0].ImpactedDependencyName, - vulnerabilitiesRows[0].ImpactedDependencyVersion, - createVulnerabilityDescription(&vulnerabilitiesRows[0]), - fmt.Sprintf("[ %s ] ", strings.Join([]string{vulnerabilitiesRows[1].Cves[0].Id}, ",")), - vulnerabilitiesRows[1].ImpactedDependencyName, - vulnerabilitiesRows[1].ImpactedDependencyVersion, - createVulnerabilityDescription(&vulnerabilitiesRows[1]), - ) - - actualContent = so.VulnerabilitiesContent(vulnerabilitiesRows) - assert.Equal(t, expectedContent, actualContent, "Content mismatch") - assert.Contains(t, actualContent, "CONTEXTUAL ANALYSIS") - assert.Contains(t, actualContent, "| Applicable |") - assert.Contains(t, actualContent, "| Not Applicable |") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + smo := &StandardOutput{} + assert.Equal(t, tc.expectedOutput, smo.Image(tc.source)) + }) + } } -func TestStandardOutput_IacContent(t *testing.T) { +func TestStandardMarkInCenter(t *testing.T) { testCases := []struct { name string - iacRows []formats.SourceCodeRow + content string expectedOutput string }{ { - name: "Empty IAC rows", - iacRows: []formats.SourceCodeRow{}, - expectedOutput: "", - }, - { - name: "Single IAC row", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "applicable/req_sw_terraform_azure_redis_auth.tf", - StartLine: 11, - StartColumn: 1, - Snippet: "Missing Periodic patching was detected", - }, - }, - }, - expectedOutput: "\n## šŸ› ļø Infrastructure as Code \n\n
\n\n\n| SEVERITY | FILE | LINE:COLUMN | FINDING |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | \n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | applicable/req_sw_terraform_azure_redis_auth.tf | 11:1 | Missing Periodic patching was detected |\n\n
\n\n", + name: "empty content", + content: "", + expectedOutput: "
\n\n\n\n
\n", }, { - name: "Multiple IAC rows", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "applicable/req_sw_terraform_azure_redis_patch.tf", - StartLine: 11, - StartColumn: 1, - Snippet: "Missing redis firewall definition or start_ip=0.0.0.0 was detected, Missing redis firewall definition or start_ip=0.0.0.0 was detected", - }, - }, - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "applicable/req_sw_terraform_azure_redis_auth.tf", - StartLine: 11, - StartColumn: 1, - Snippet: "Missing Periodic patching was detected", - }, - }, - }, - expectedOutput: "\n## šŸ› ļø Infrastructure as Code \n\n
\n\n\n| SEVERITY | FILE | LINE:COLUMN | FINDING |\n| :---------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | \n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | applicable/req_sw_terraform_azure_redis_patch.tf | 11:1 | Missing redis firewall definition or start_ip=0.0.0.0 was detected, Missing redis firewall definition or start_ip=0.0.0.0 was detected |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | applicable/req_sw_terraform_azure_redis_auth.tf | 11:1 | Missing Periodic patching was detected |\n\n
\n\n", + name: "non empty content", + content: "content", + expectedOutput: "
\n\ncontent\n\n
\n", }, } - - writer := &StandardOutput{} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := writer.IacTableContent(tc.iacRows) - assert.Equal(t, tc.expectedOutput, output) + smo := &StandardOutput{} + assert.Equal(t, tc.expectedOutput, smo.MarkInCenter(tc.content)) }) } } -func TestStandardOutput_GetIacTableContent(t *testing.T) { +func TestStandardMarkAsDetails(t *testing.T) { testCases := []struct { name string - iacRows []formats.SourceCodeRow + summary string + content string expectedOutput string + subTitleDepth int }{ { - name: "Empty IAC rows", - iacRows: []formats.SourceCodeRow{}, - expectedOutput: "", + name: "empty", + summary: "", + subTitleDepth: 1, + content: "", + expectedOutput: "
\n\n\n\n
\n", }, { - name: "Single IAC row", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 2}, - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - Snippet: "Public access to MySQL was detected", - }, - }, - }, - expectedOutput: "\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | file1 | 1:10 | Public access to MySQL was detected |", + name: "empty content", + summary: "summary", + subTitleDepth: 1, + content: "", + expectedOutput: "
\n summary \n
\n\n\n\n
\n", }, { - name: "Multiple IAC rows", - iacRows: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 3}, - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - Snippet: "Public access to MySQL was detected", - }, - }, - { - SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 2}, - Location: formats.Location{ - File: "file2", - StartLine: 2, - StartColumn: 5, - Snippet: "Public access to MySQL was detected", - }, - }, - }, - expectedOutput: "\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | file1 | 1:10 | Public access to MySQL was detected |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | file2 | 2:5 | Public access to MySQL was detected |", + name: "empty summary", + summary: "", + subTitleDepth: 1, + content: "content", + expectedOutput: "
\n\ncontent\n\n
\n", }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output := getIacTableContent(tc.iacRows, &StandardOutput{}) - assert.Equal(t, tc.expectedOutput, output) - }) - } -} - -func TestStandardOutput_GetLicensesTableContent(t *testing.T) { - writer := &StandardOutput{} - testGetLicensesTableContent(t, writer) -} - -func TestStandardOutput_ApplicableCveReviewContent(t *testing.T) { - testCases := []struct { - name string - severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string - expectedOutput string - }{ { - 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", - expectedOutput: "\n## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability\n\n\n
\n\n| Severity | Impacted Dependency | Finding | CVE |\n| :--------------: | :---: | :---: | :---: |\n| ![](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 |\n\n
\n\n
\n Description \n
\n\nThe scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.\n\n
\n\n
\n CVE details \n
\n\ncveDetails\n\n
\n\n\n
\n Remediation \n
\n\nsome remediation\n\n
\n\n", + name: "Main details", + summary: "summary", + subTitleDepth: 1, + content: "content", + expectedOutput: "
\n summary \n
\n\ncontent\n\n
\n", }, { - 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", - expectedOutput: "\n## šŸ“¦šŸ” Contextual Analysis CVE Vulnerability\n\n\n
\n\n| Severity | Impacted Dependency | Finding | CVE |\n| :--------------: | :---: | :---: | :---: |\n| ![](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 |\n\n
\n\n
\n Description \n
\n\nThe scanner checks whether the vulnerable `Development Server` of the `werkzeug` library is used by looking for calls to `werkzeug.serving.run_simple()`.\n\n
\n\n
\n CVE details \n
\n\ncveDetails\n\n
\n\n", + name: "Sub details", + summary: "summary", + subTitleDepth: 2, + content: "content", + expectedOutput: "
\n summary \n
\n\ncontent\n\n
\n", }, - } - - so := &StandardOutput{} - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output := so.ApplicableCveReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.cve, tc.cveDetails, tc.impactedDependency, tc.remediation) - assert.Equal(t, tc.expectedOutput, output) - }) - } -} - -func TestStandardOutput_IacReviewContent(t *testing.T) { - testCases := []struct { - name string - severity, finding, fullDetails string - expectedOutput string - }{ { - 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", - expectedOutput: "\n## šŸ› ļø Infrastructure as Code\n\n
\n\n| Severity | Finding |\n| :--------------: | :---: |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Missing auto upgrade was detected |\n\n
\n\n
\n Full description \n
\n\nResource `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\n\n
\n\n", + name: "Sub sub details", + summary: "summary", + subTitleDepth: 3, + content: "content", + expectedOutput: "
\n summary \n
\n\ncontent\n\n
\n", }, } - - so := &StandardOutput{} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := so.IacReviewContent(tc.severity, tc.finding, tc.fullDetails) - assert.Equal(t, tc.expectedOutput, output) + smo := &StandardOutput{} + assert.Equal(t, tc.expectedOutput, smo.MarkAsDetails(tc.summary, tc.subTitleDepth, tc.content)) }) } } -func TestStandardOutput_SastReviewContent(t *testing.T) { +func TestStandardMarkAsTitle(t *testing.T) { testCases := []struct { name string - severity string - finding string - fullDetails string + title string expectedOutput string - codeFlows [][]formats.Location + subTitleDepth int }{ { - 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{ - { - { - 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", - }, - }, - }, - expectedOutput: "\n## šŸŽÆ Static Application Security Testing (SAST) Vulnerability \n\t\n
\n\n| Severity | Finding |\n| :--------------: | :---: |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Stack Trace Exposure |\n\n
\n\n
\n Full description \n
\n\n\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.\n\n
\n\n\n\n
\nCode Flows \n\n\n\n
\nVulnerable data flow analysis result \n
\n\nā†˜ļø `other-snippet` (at file2 line 1)\n\nā†˜ļø `snippet` (at file line 0)\n\n\n
\n\n\n\n
\nVulnerable data flow analysis result \n
\n\nā†˜ļø `a-snippet` (at file line 10)\n\nā†˜ļø `snippet` (at file line 0)\n\n\n
\n\n\n\n
\n\n", + name: "empty", + title: "", + subTitleDepth: 0, + expectedOutput: " ", }, { - 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.", - expectedOutput: "\n## šŸŽÆ Static Application Security Testing (SAST) Vulnerability \n\t\n
\n\n| Severity | Finding |\n| :--------------: | :---: |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableLowSeverity.png)
Low | Stack Trace Exposure |\n\n
\n\n
\n Full description \n
\n\n\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.\n\n
\n\n", + name: "Main title", + title: "title", + subTitleDepth: 1, + expectedOutput: "# title", + }, + { + name: "Sub title", + title: "title", + subTitleDepth: 2, + expectedOutput: "## title", + }, + { + name: "Sub sub title", + title: "title", + subTitleDepth: 3, + expectedOutput: "### title", }, } - - so := &StandardOutput{} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := so.SastReviewContent(tc.severity, tc.finding, tc.fullDetails, tc.codeFlows) - assert.Equal(t, tc.expectedOutput, output) + smo := &StandardOutput{} + assert.Equal(t, tc.expectedOutput, smo.MarkAsTitle(tc.title, tc.subTitleDepth)) }) } } diff --git a/utils/outputwriter/testsutils.go b/utils/outputwriter/testsutils.go new file mode 100644 index 000000000..fca75a7db --- /dev/null +++ b/utils/outputwriter/testsutils.go @@ -0,0 +1,70 @@ +package outputwriter + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + // Used for tests that are outside the outputwriter package. + TestMessagesDir = filepath.Join("..", "testdata", "messages") + TestSummaryCommentDir = filepath.Join(TestMessagesDir, "summarycomment") + // Used for tests that are inside the outputwriter package. + testMessagesDir = filepath.Join("..", TestMessagesDir) + testReviewCommentDir = filepath.Join(testMessagesDir, "reviewcomment") + testSummaryCommentDir = filepath.Join(testMessagesDir, "summarycomment") +) + +type OutputTestCase struct { + name string + writer OutputWriter + expectedOutputPath string + expectedOutput string +} + +type TestBodyResponse struct { + Body string `json:"body"` +} + +func GetExpectedTestOutput(t *testing.T, testCase OutputTestCase) string { + if testCase.expectedOutputPath != "" { + return GetOutputFromFile(t, testCase.expectedOutputPath) + } + return testCase.expectedOutput +} + +func GetOutputFromFile(t *testing.T, filePath string) string { + content, err := os.ReadFile(filePath) + assert.NoError(t, err) + return strings.ReplaceAll(string(content), "\r\n", "\n") +} + +func GetJsonBodyOutputFromFile(t *testing.T, filePath string) []byte { + bodyRes := TestBodyResponse{Body: GetOutputFromFile(t, filePath)} + bytes, err := json.Marshal(bodyRes) + assert.NoError(t, err) + return bytes +} + +func GetPRSummaryContentNoIssues(t *testing.T, summaryTestDir string, entitled, simplified bool) string { + dataPath := filepath.Join(summaryTestDir, "structure") + if simplified { + if entitled { + dataPath = filepath.Join(dataPath, "summary_comment_simplified_no_issues_entitled.md") + } else { + dataPath = filepath.Join(dataPath, "summary_comment_simplified_no_issues_not_entitled.md") + } + } else { + if entitled { + dataPath = filepath.Join(dataPath, "summary_comment_pr_no_issues_entitled.md") + } else { + dataPath = filepath.Join(dataPath, "summary_comment_pr_no_issues_not_entitled.md") + } + } + return GetOutputFromFile(t, dataPath) +} diff --git a/utils/reviewcomment_test.go b/utils/reviewcomment_test.go deleted file mode 100644 index abc637d40..000000000 --- a/utils/reviewcomment_test.go +++ /dev/null @@ -1,370 +0,0 @@ -package utils - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/jfrog/frogbot/utils/outputwriter" - "github.com/jfrog/froggit-go/vcsclient" - "github.com/jfrog/froggit-go/vcsutils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" - "github.com/stretchr/testify/assert" -) - -func TestCreatePullRequestMessageNoVulnerabilities(t *testing.T) { - vulnerabilities := []formats.VulnerabilityOrViolationRow{} - message := createPullRequestComment(&IssuesCollection{Vulnerabilities: vulnerabilities}, &outputwriter.StandardOutput{}) - - expectedMessageByte, err := os.ReadFile(filepath.Join("..", "testdata", "messages", "novulnerabilities.md")) - assert.NoError(t, err) - expectedMessage := strings.ReplaceAll(string(expectedMessageByte), "\r\n", "\n") - assert.Equal(t, expectedMessage, message) - - outputWriter := &outputwriter.StandardOutput{} - outputWriter.SetVcsProvider(vcsutils.GitLab) - message = createPullRequestComment(&IssuesCollection{Vulnerabilities: vulnerabilities}, outputWriter) - - expectedMessageByte, err = os.ReadFile(filepath.Join("..", "testdata", "messages", "novulnerabilitiesMR.md")) - assert.NoError(t, err) - expectedMessage = strings.ReplaceAll(string(expectedMessageByte), "\r\n", "\n") - assert.Equal(t, expectedMessage, message) -} - -func TestCreatePullRequestComment(t *testing.T) { - vulnerabilities := []formats.VulnerabilityOrViolationRow{ - { - Summary: "Summary XRAY-122345", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, - 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", - Cves: []formats.CveRow{{}}, - }, - { - Summary: "Summary", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High"}, - 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{}, - }, - { - Summary: "Summary CVE-2022-26652", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "Medium"}, - 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.3]"}, - Cves: []formats.CveRow{{Id: "CVE-2022-26652"}}, - }, - } - licenses := []formats.LicenseRow{ - { - LicenseKey: "Apache-2.0", - ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ - SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 13}, - ImpactedDependencyName: "minimatch", - ImpactedDependencyVersion: "1.2.3", - Components: []formats.ComponentRow{ - { - Name: "root", - Version: "1.0.0", - }, - { - Name: "minimatch", - Version: "1.2.3", - }, - }, - }, - }, - } - - writerOutput := &outputwriter.StandardOutput{} - writerOutput.SetJasOutputFlags(true, true) - message := createPullRequestComment(&IssuesCollection{Vulnerabilities: vulnerabilities, Licenses: licenses}, writerOutput) - - expectedMessage := "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerPR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n
\n\n| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](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] | - |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3:v3.5.1 | - | - |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Undetermined | 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 |\n\n
\n\n## šŸ”¬ Research Details\n\n
\n [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 \n
\n\n**Description:**\nSummary XRAY-122345\n\n\n
\n\n\n
\n github.com/mholt/archiver/v3 v3.5.1 \n
\n\n**Description:**\nSummary\n\n\n
\n\n\n
\n [ CVE-2022-26652 ] github.com/nats-io/nats-streaming-server v0.21.0 \n
\n\n**Description:**\nSummary CVE-2022-26652\n\n\n
\n\n\n## āš–ļø Violated Licenses \n\n
\n\n\n| LICENSE | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | \n| :---------------------: | :----------------------------------: | :-----------------------------------: | \n| Apache-2.0 | root 1.0.0
minimatch 1.2.3 | minimatch 1.2.3 |\n\n
\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
" - assert.Equal(t, expectedMessage, message) - - writerOutput.SetVcsProvider(vcsutils.GitLab) - message = createPullRequestComment(&IssuesCollection{Vulnerabilities: vulnerabilities}, writerOutput) - expectedMessage = "
\n\n[![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/vulnerabilitiesBannerMR.png)](https://github.com/jfrog/frogbot#readme)\n\n
\n\n\n## šŸ“¦ Vulnerable Dependencies\n\n### āœļø Summary\n\n
\n\n| SEVERITY | CONTEXTUAL ANALYSIS | DIRECT DEPENDENCIES | IMPACTED DEPENDENCY | FIXED VERSIONS | CVES |\n| :---------------------: | :----------------------------------: | :----------------------------------: | :-----------------------------------: | :---------------------------------: | :---------------------------------: | \n| ![](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] | - |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)
High | Undetermined | github.com/mholt/archiver/v3:v3.5.1 | github.com/mholt/archiver/v3:v3.5.1 | - | - |\n| ![](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableMediumSeverity.png)
Medium | Undetermined | 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 |\n\n
\n\n## šŸ”¬ Research Details\n\n
\n [ XRAY-122345 ] github.com/nats-io/nats-streaming-server v0.21.0 \n
\n\n**Description:**\nSummary XRAY-122345\n\n\n
\n\n\n
\n github.com/mholt/archiver/v3 v3.5.1 \n
\n\n**Description:**\nSummary\n\n\n
\n\n\n
\n [ CVE-2022-26652 ] github.com/nats-io/nats-streaming-server v0.21.0 \n
\n\n**Description:**\nSummary CVE-2022-26652\n\n\n
\n\n\n---\n
\n\n[šŸø JFrog Frogbot](https://github.com/jfrog/frogbot#readme)\n\n
" - assert.Equal(t, expectedMessage, message) -} - -func TestGetFrogbotReviewComments(t *testing.T) { - testCases := []struct { - name string - existingComments []vcsclient.CommentInfo - expectedOutput []vcsclient.CommentInfo - }{ - { - name: "No frogbot comments", - existingComments: []vcsclient.CommentInfo{ - { - Content: outputwriter.FrogbotTitlePrefix, - }, - { - Content: "some comment text" + outputwriter.MarkdownComment("with hidden comment"), - }, - { - Content: outputwriter.CommentGeneratedByFrogbot, - }, - }, - expectedOutput: []vcsclient.CommentInfo{}, - }, - { - name: "With frogbot comments", - existingComments: []vcsclient.CommentInfo{ - { - Content: outputwriter.FrogbotTitlePrefix, - }, - { - Content: outputwriter.MarkdownComment(outputwriter.ReviewCommentId) + "A Frogbot review comment", - }, - { - Content: "some comment text" + outputwriter.MarkdownComment("with hidden comment"), - }, - { - Content: outputwriter.ReviewCommentId, - }, - { - Content: outputwriter.CommentGeneratedByFrogbot, - }, - }, - expectedOutput: []vcsclient.CommentInfo{ - { - Content: outputwriter.MarkdownComment(outputwriter.ReviewCommentId) + "A Frogbot review comment", - }, - { - Content: outputwriter.ReviewCommentId, - }, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output := getFrogbotReviewComments(tc.existingComments) - assert.ElementsMatch(t, tc.expectedOutput, output) - }) - } -} - -func TestGetNewReviewComments(t *testing.T) { - repo := &Repository{OutputWriter: &outputwriter.StandardOutput{}} - testCases := []struct { - name string - issues *IssuesCollection - expectedOutput []ReviewComment - }{ - { - name: "No issues for review comments", - issues: &IssuesCollection{ - Vulnerabilities: []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: coreutils.Npm, - }, - }, - Secrets: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{ - Severity: "High", - SeverityNumValue: 13, - }, - Finding: "Secret", - Location: formats.Location{ - File: "index.js", - StartLine: 5, - StartColumn: 6, - EndLine: 7, - EndColumn: 8, - Snippet: "access token exposed", - }, - }, - }, - }, - expectedOutput: []ReviewComment{}, - }, - { - name: "With issues for review comments", - issues: &IssuesCollection{ - Vulnerabilities: []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", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}}, - Technology: coreutils.Npm, - }, - }, - Iacs: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{ - Severity: "High", - SeverityNumValue: 13, - }, - Finding: "Missing auto upgrade was detected", - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - EndLine: 2, - EndColumn: 11, - Snippet: "aws-violation", - }, - }, - }, - Sast: []formats.SourceCodeRow{ - { - SeverityDetails: formats.SeverityDetails{ - Severity: "High", - SeverityNumValue: 13, - }, - Finding: "XSS Vulnerability", - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - EndLine: 2, - EndColumn: 11, - Snippet: "snippet", - }, - }, - }, - }, - expectedOutput: []ReviewComment{ - { - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - EndLine: 2, - EndColumn: 11, - Snippet: "snippet", - }, - Type: ApplicableComment, - CommentInfo: vcsclient.PullRequestComment{ - CommentInfo: vcsclient.CommentInfo{ - Content: outputwriter.GenerateReviewCommentContent(repo.ApplicableCveReviewContent("Low", "", "", "CVE-2023-4321", "summary-2", "component-C:", ""), repo.OutputWriter), - }, - PullRequestDiff: vcsclient.PullRequestDiff{ - OriginalFilePath: "file1", - OriginalStartLine: 1, - OriginalStartColumn: 10, - OriginalEndLine: 2, - OriginalEndColumn: 11, - NewFilePath: "file1", - NewStartLine: 1, - NewStartColumn: 10, - NewEndLine: 2, - NewEndColumn: 11, - }, - }, - }, - { - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - EndLine: 2, - EndColumn: 11, - Snippet: "aws-violation", - }, - Type: IacComment, - CommentInfo: vcsclient.PullRequestComment{ - CommentInfo: vcsclient.CommentInfo{ - Content: outputwriter.GenerateReviewCommentContent(repo.IacReviewContent("High", "Missing auto upgrade was detected", ""), repo.OutputWriter), - }, - PullRequestDiff: vcsclient.PullRequestDiff{ - OriginalFilePath: "file1", - OriginalStartLine: 1, - OriginalStartColumn: 10, - OriginalEndLine: 2, - OriginalEndColumn: 11, - NewFilePath: "file1", - NewStartLine: 1, - NewStartColumn: 10, - NewEndLine: 2, - NewEndColumn: 11, - }, - }, - }, - { - Location: formats.Location{ - File: "file1", - StartLine: 1, - StartColumn: 10, - EndLine: 2, - EndColumn: 11, - Snippet: "snippet", - }, - Type: SastComment, - CommentInfo: vcsclient.PullRequestComment{ - CommentInfo: vcsclient.CommentInfo{ - Content: outputwriter.GenerateReviewCommentContent(repo.SastReviewContent("High", "XSS Vulnerability", "", [][]formats.Location{}), repo.OutputWriter), - }, - PullRequestDiff: vcsclient.PullRequestDiff{ - OriginalFilePath: "file1", - OriginalStartLine: 1, - OriginalStartColumn: 10, - OriginalEndLine: 2, - OriginalEndColumn: 11, - NewFilePath: "file1", - NewStartLine: 1, - NewStartColumn: 10, - NewEndLine: 2, - NewEndColumn: 11, - }, - }, - }, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output := getNewReviewComments(repo, tc.issues) - assert.ElementsMatch(t, tc.expectedOutput, output) - }) - } -} diff --git a/utils/utils.go b/utils/utils.go index 15013017d..518e8a4d9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -361,18 +361,6 @@ func GetVulnerabiltiesUniqueID(vulnerability formats.VulnerabilityOrViolationRow len(vulnerability.FixedVersions) > 0) } -func GetSortedPullRequestComments(client vcsclient.VcsClient, repoOwner, repoName string, prID int) ([]vcsclient.CommentInfo, error) { - pullRequestsComments, err := client.ListPullRequestComments(context.Background(), repoOwner, repoName, prID) - if err != nil { - return nil, err - } - // Sort the comment according to time created, the newest comment should be the first one. - sort.Slice(pullRequestsComments, func(i, j int) bool { - return pullRequestsComments[i].Created.After(pullRequestsComments[j].Created) - }) - return pullRequestsComments, nil -} - func ConvertSarifPathsToRelative(issues *IssuesCollection, workingDirs ...string) { convertSarifPathsInCveApplicability(issues.Vulnerabilities, workingDirs...) convertSarifPathsInIacs(issues.Iacs, workingDirs...)