From 74520fe075ca7cf37d602efc5b4d3bc318da4805 Mon Sep 17 00:00:00 2001 From: Assaf Attias <49212512+attiasas@users.noreply.github.com> Date: Wed, 25 Dec 2024 16:08:23 +0200 Subject: [PATCH] Add output analytics for GH integration (#270) --- commands/scan/buildscan.go | 1 + commands/scan/scan.go | 1 + jas/common_test.go | 14 +-- .../binary_analytics_vulnerabilities.md | 1 + .../build_scan_analytics_vulnerabilities.md | 1 + .../testdata/output/jobSummary/violations.md | 2 +- .../output/jobSummary/violations_analytics.md | 1 + .../violations_not_extended_view.md | 2 +- utils/formats/sarifutils/sarifutils.go | 24 ++++ utils/formats/sarifutils/sarifutils_test.go | 13 +- utils/formats/sarifutils/test_sarifutils.go | 55 +++++++-- utils/results/common_test.go | 5 +- utils/results/conversion/convertor.go | 4 +- .../conversion/sarifparser/sarifparser.go | 42 +++++-- .../sarifparser/sarifparser_test.go | 89 ++++++++------ .../simplejsonparser/simplejsonparser_test.go | 20 ++-- utils/results/output/resultwriter.go | 8 ++ utils/results/output/securityJobSummary.go | 30 ++++- .../results/output/securityJobSummary_test.go | 112 ++++++++++++++---- utils/utils.go | 8 ++ 20 files changed, 334 insertions(+), 99 deletions(-) create mode 100644 tests/testdata/output/jobSummary/binary_analytics_vulnerabilities.md create mode 100644 tests/testdata/output/jobSummary/build_scan_analytics_vulnerabilities.md create mode 100644 tests/testdata/output/jobSummary/violations_analytics.md diff --git a/commands/scan/buildscan.go b/commands/scan/buildscan.go index 55496016..4b491bf9 100644 --- a/commands/scan/buildscan.go +++ b/commands/scan/buildscan.go @@ -155,6 +155,7 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS resultsPrinter := output.NewResultsWriter(cmdResults). SetOutputFormat(bsc.outputFormat). + SetPlatformUrl(bsc.serverDetails.Url). SetHasViolationContext(true). SetIncludeVulnerabilities(bsc.includeVulnerabilities). SetIncludeLicenses(false). diff --git a/commands/scan/scan.go b/commands/scan/scan.go index 5b32c4a0..7bea7280 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -251,6 +251,7 @@ func (scanCmd *ScanCommand) RunAndRecordResults(cmdType utils.CommandType, recor if err = output.NewResultsWriter(cmdResults). SetOutputFormat(scanCmd.outputFormat). + SetPlatformUrl(scanCmd.serverDetails.Url). SetHasViolationContext(scanCmd.hasViolationContext()). SetIncludeVulnerabilities(scanCmd.includeVulnerabilities). SetIncludeLicenses(scanCmd.includeLicenses). diff --git a/jas/common_test.go b/jas/common_test.go index 2c504abc..594fc391 100644 --- a/jas/common_test.go +++ b/jas/common_test.go @@ -65,8 +65,8 @@ func TestAddScoreToRunRules(t *testing.T) { sarifutils.CreateResultWithOneLocation("file", 0, 0, 0, 0, "snippet", "rule2", "warning"), ), expectedOutput: []*sarif.ReportingDescriptor{ - sarif.NewRule("rule1").WithProperties(sarif.Properties{"security-severity": "6.9"}), - sarif.NewRule("rule2").WithProperties(sarif.Properties{"security-severity": "6.9"}), + sarifutils.CreateDummyRuleWithProperties("rule1", sarif.Properties{"security-severity": "6.9"}), + sarifutils.CreateDummyRuleWithProperties("rule2", sarif.Properties{"security-severity": "6.9"}), }, }, { @@ -78,11 +78,11 @@ func TestAddScoreToRunRules(t *testing.T) { sarifutils.CreateResultWithOneLocation("file", 0, 0, 0, 0, "snippet", "rule5", "error"), ), expectedOutput: []*sarif.ReportingDescriptor{ - sarif.NewRule("rule1").WithProperties(sarif.Properties{"security-severity": "0.0"}), - sarif.NewRule("rule2").WithProperties(sarif.Properties{"security-severity": "3.9"}), - sarif.NewRule("rule3").WithProperties(sarif.Properties{"security-severity": "6.9"}), - sarif.NewRule("rule4").WithProperties(sarif.Properties{"security-severity": "6.9"}), - sarif.NewRule("rule5").WithProperties(sarif.Properties{"security-severity": "8.9"}), + sarifutils.CreateDummyRuleWithProperties("rule1", sarif.Properties{"security-severity": "0.0"}), + sarifutils.CreateDummyRuleWithProperties("rule2", sarif.Properties{"security-severity": "3.9"}), + sarifutils.CreateDummyRuleWithProperties("rule3", sarif.Properties{"security-severity": "6.9"}), + sarifutils.CreateDummyRuleWithProperties("rule4", sarif.Properties{"security-severity": "6.9"}), + sarifutils.CreateDummyRuleWithProperties("rule5", sarif.Properties{"security-severity": "8.9"}), }, }, } diff --git a/tests/testdata/output/jobSummary/binary_analytics_vulnerabilities.md b/tests/testdata/output/jobSummary/binary_analytics_vulnerabilities.md new file mode 100644 index 00000000..bc6f9bf2 --- /dev/null +++ b/tests/testdata/output/jobSummary/binary_analytics_vulnerabilities.md @@ -0,0 +1 @@ +<pre>44 Security issues are grouped by CVE number:	44 SCA<br><br><div style="display: flex; align-items: center; text-align: center">❗️ 33 Critical</div><br><div style="display: flex; align-items: center; text-align: center">🟡 11 Low</div><br><a href="https://test-platform-url.jfrog.io/ui/onDemandScanning/3d90ec4b-cf33-4846-6831-4bf9576f2235?gh_job_id=some-job-id&gh_section=on_demand_scan">See the results of the scan in JFrog</a></pre> \ No newline at end of file diff --git a/tests/testdata/output/jobSummary/build_scan_analytics_vulnerabilities.md b/tests/testdata/output/jobSummary/build_scan_analytics_vulnerabilities.md new file mode 100644 index 00000000..beb4376a --- /dev/null +++ b/tests/testdata/output/jobSummary/build_scan_analytics_vulnerabilities.md @@ -0,0 +1 @@ +<pre>24 Security Issues:	24 SCA<br><br><div style="display: flex; align-items: center; text-align: center">🔴 3 High</div><br><div style="display: flex; align-items: center; text-align: center">🟠 1 Medium</div><br><div style="display: flex; align-items: center; text-align: center">⚪️ 20 Unknown</div><br><a href="https://test-more-info-url.jfrog.io/?gh_job_id=some-job-id&gh_section=build">See the results of the scan in JFrog</a></pre> \ No newline at end of file diff --git a/tests/testdata/output/jobSummary/violations.md b/tests/testdata/output/jobSummary/violations.md index f604e7e6..76e13466 100644 --- a/tests/testdata/output/jobSummary/violations.md +++ b/tests/testdata/output/jobSummary/violations.md @@ -1 +1 @@ -<pre>watches: <br>watch1, watch2, watch3, watch4<br>watch5</pre><br><pre>23 Policy Violations:	17 Security	2 Operational	1 License	3 Secrets<br><br><div style="display: flex; align-items: center; text-align: center">❗️ 8 Critical (2 Not Applicable)</div><br><div style="display: flex; align-items: center; text-align: center">🔴 6 High</div><br><div style="display: flex; align-items: center; text-align: center">🟠 3 Medium</div><br><div style="display: flex; align-items: center; text-align: center">🟡 5 Low (3 Not Applicable)</div><br><div style="display: flex; align-items: center; text-align: center">⚪️ 1 Unknown</div><br><a href="https://test-more-info-url.jfrog.io/">See the results of the scan in JFrog</a></pre> \ No newline at end of file +<pre>watches: <br>watch1, watch2, watch3, watch4<br>watch5</pre><br><pre>23 Policy Violations:	17 Security	2 Operational	1 License	3 Secrets<br><br><div style="display: flex; align-items: center; text-align: center">❗️ 8 Critical (2 Not Applicable)</div><br><div style="display: flex; align-items: center; text-align: center">🔴 6 High</div><br><div style="display: flex; align-items: center; text-align: center">🟠 3 Medium</div><br><div style="display: flex; align-items: center; text-align: center">🟡 5 Low (3 Not Applicable)</div><br><div style="display: flex; align-items: center; text-align: center">⚪️ 1 Unknown</div><br><a href="https://test-more-info-url.jfrog.io/scan-descendants/master?repoId=10">See the results of the scan in JFrog</a></pre> \ No newline at end of file diff --git a/tests/testdata/output/jobSummary/violations_analytics.md b/tests/testdata/output/jobSummary/violations_analytics.md new file mode 100644 index 00000000..9197b43c --- /dev/null +++ b/tests/testdata/output/jobSummary/violations_analytics.md @@ -0,0 +1 @@ +<pre>watches: <br>watch1, watch2, watch3, watch4<br>watch5</pre><br><pre>23 Policy Violations:	17 Security	2 Operational	1 License	3 Secrets<br><br><div style="display: flex; align-items: center; text-align: center">❗️ 8 Critical (2 Not Applicable)</div><br><div style="display: flex; align-items: center; text-align: center">🔴 6 High</div><br><div style="display: flex; align-items: center; text-align: center">🟠 3 Medium</div><br><div style="display: flex; align-items: center; text-align: center">🟡 5 Low (3 Not Applicable)</div><br><div style="display: flex; align-items: center; text-align: center">⚪️ 1 Unknown</div><br><a href="https://test-more-info-url.jfrog.io/scan-descendants/master?repoId=10gh_job_id=some-job-id&gh_section=on_demand_scan">See the results of the scan in JFrog</a></pre> \ No newline at end of file diff --git a/tests/testdata/output/jobSummary/violations_not_extended_view.md b/tests/testdata/output/jobSummary/violations_not_extended_view.md index 1aead66d..effe7558 100644 --- a/tests/testdata/output/jobSummary/violations_not_extended_view.md +++ b/tests/testdata/output/jobSummary/violations_not_extended_view.md @@ -1 +1 @@ -<pre>watch: watch1</pre><br><pre>26 Policy Violations:	20 Security	2 Operational	1 License	3 Secrets<br><br><a href="https://jfrog.com/help/access?xinfo:appid=csh-gen-gitbook">🐸 Unlock detailed findings</a></pre> \ No newline at end of file +<pre>watch: watch1</pre><br><pre>23 Policy Violations:	17 Security	2 Operational	1 License	3 Secrets<br><br><a href="https://jfrog.com/help/access?xinfo:appid=csh-gen-gitbook">🐸 Unlock detailed findings</a></pre> \ No newline at end of file diff --git a/utils/formats/sarifutils/sarifutils.go b/utils/formats/sarifutils/sarifutils.go index c0e8f313..a58c13a8 100644 --- a/utils/formats/sarifutils/sarifutils.go +++ b/utils/formats/sarifutils/sarifutils.go @@ -665,6 +665,30 @@ func SetRuleShortDescriptionText(value string, rule *sarif.ReportingDescriptor) rule.ShortDescription.Text = &value } +func SetRuleHelp(msg, markdown string, rule *sarif.ReportingDescriptor) { + if rule.Help == nil { + rule.Help = &sarif.MultiformatMessageString{ + Text: &msg, + Markdown: &markdown, + } + return + } + rule.Help.Markdown = &markdown + rule.Help.Text = &msg +} + +func SetRuleFullDescription(msg, markdown string, rule *sarif.ReportingDescriptor) { + if rule.FullDescription == nil { + rule.FullDescription = &sarif.MultiformatMessageString{ + Text: &msg, + Markdown: &markdown, + } + return + } + rule.FullDescription.Markdown = &markdown + rule.FullDescription.Text = &msg +} + func GetRuleShortDescriptionText(rule *sarif.ReportingDescriptor) string { if rule.ShortDescription != nil && rule.ShortDescription.Text != nil { return *rule.ShortDescription.Text diff --git a/utils/formats/sarifutils/sarifutils_test.go b/utils/formats/sarifutils/sarifutils_test.go index 11652d1e..0ccbdebf 100644 --- a/utils/formats/sarifutils/sarifutils_test.go +++ b/utils/formats/sarifutils/sarifutils_test.go @@ -566,7 +566,7 @@ func TestGetRunRules(t *testing.T) { run: CreateRunWithDummyResults( CreateDummyPassingResult("rule1"), ), - expectedOutput: []*sarif.ReportingDescriptor{sarif.NewRule("rule1")}, + expectedOutput: []*sarif.ReportingDescriptor{sarif.NewRule("rule1").WithShortDescription(sarif.NewMultiformatMessageString("")).WithFullDescription(sarif.NewMarkdownMultiformatMessageString("rule-markdown").WithText("rule-msg"))}, }, { run: CreateRunWithDummyResults( @@ -576,12 +576,17 @@ func TestGetRunRules(t *testing.T) { CreateDummyPassingResult("rule3"), CreateDummyPassingResult("rule2"), ), - expectedOutput: []*sarif.ReportingDescriptor{sarif.NewRule("rule1"), sarif.NewRule("rule2"), sarif.NewRule("rule3")}, + expectedOutput: []*sarif.ReportingDescriptor{ + sarif.NewRule("rule1").WithShortDescription(sarif.NewMultiformatMessageString("")).WithFullDescription(sarif.NewMarkdownMultiformatMessageString("rule-markdown").WithText("rule-msg")), + sarif.NewRule("rule2").WithShortDescription(sarif.NewMultiformatMessageString("")).WithFullDescription(sarif.NewMarkdownMultiformatMessageString("rule-markdown").WithText("rule-msg")), + sarif.NewRule("rule3").WithShortDescription(sarif.NewMultiformatMessageString("")).WithFullDescription(sarif.NewMarkdownMultiformatMessageString("rule-markdown").WithText("rule-msg")), + }, }, } for _, test := range tests { - assert.Equal(t, test.expectedOutput, GetRunRules(test.run)) + rules := GetRunRules(test.run) + assert.Equal(t, test.expectedOutput, rules) } } @@ -635,7 +640,7 @@ func TestGetResultFingerprint(t *testing.T) { }, { name: "Results with fingerprint field", - result: CreateDummyResultWithFingerprint("some_markdown", "masg", jasutils.SastFingerprintKey, "sast_fingerprint"), + result: CreateDummyResultWithFingerprint("some_markdown", "msg", jasutils.SastFingerprintKey, "sast_fingerprint"), expectedOutput: "sast_fingerprint", }, } diff --git a/utils/formats/sarifutils/test_sarifutils.go b/utils/formats/sarifutils/test_sarifutils.go index 78cac692..88e7e270 100644 --- a/utils/formats/sarifutils/test_sarifutils.go +++ b/utils/formats/sarifutils/test_sarifutils.go @@ -4,12 +4,24 @@ import ( "github.com/owenrumney/go-sarif/v2/sarif" ) +// TODO: Create a Builder struct (with dynamic setters) and refactor sarif tests for better maintenance + +func CreateRunWithDummyResultsWithRuleInformation(toolName, ruleShortTxtDescription, ruleTxtDescription, ruleMarkdownDescription, ruleHelpMsg, ruleHelpMarkdown, wd string, results ...*sarif.Result) *sarif.Run { + run := createRunWithDummyResults(toolName, ruleShortTxtDescription, ruleTxtDescription, ruleMarkdownDescription, ruleHelpMsg, ruleHelpMarkdown, results...) + run.Invocations = []*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))} + return run +} + +func CreateRunWithDummyResultsInWdWithHelp(helpMsg, helpMarkdown, wd string, results ...*sarif.Result) *sarif.Run { + return createRunWithDummyResults("", "", "rule-msg", "rule-markdown", helpMsg, helpMarkdown, results...).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}) +} + func CreateRunWithDummyResultsInWd(wd string, results ...*sarif.Result) *sarif.Run { - return createRunWithDummyResults("", results...).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}) + return createRunWithDummyResults("", "", "rule-msg", "rule-markdown", "", "", results...).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}) } func CreateRunWithDummyResults(results ...*sarif.Result) *sarif.Run { - return createRunWithDummyResults("", results...) + return createRunWithDummyResults("", "", "rule-msg", "rule-markdown", "", "", results...) } func CreateDummyDriver(toolName string, rules ...*sarif.ReportingDescriptor) *sarif.ToolComponent { @@ -20,20 +32,36 @@ func CreateDummyDriver(toolName string, rules ...*sarif.ReportingDescriptor) *sa } func CreateRunNameWithResults(toolName string, results ...*sarif.Result) *sarif.Run { - return createRunWithDummyResults(toolName, results...) + return createRunWithDummyResults(toolName, "", "rule-msg", "rule-markdown", "", "", results...) } -func createRunWithDummyResults(toolName string, results ...*sarif.Result) *sarif.Run { +func createRunWithDummyResults(toolName, ruleShortTxtDescription, ruleMsg, ruleMarkdown, ruleHelpMsg, ruleHelpMarkdown string, results ...*sarif.Result) *sarif.Run { run := sarif.NewRun(*sarif.NewSimpleTool(toolName)) for _, result := range results { if result.RuleID != nil { - run.AddRule(*result.RuleID) + rule := run.AddRule(*result.RuleID) + SetRuleFullDescription(ruleMsg, ruleMarkdown, rule) + if ruleHelpMsg != "" || ruleHelpMarkdown != "" { + SetRuleHelp(ruleHelpMsg, ruleHelpMarkdown, rule) + } + SetRuleShortDescriptionText(ruleShortTxtDescription, rule) } run.AddResult(result) } return run } +func CreateRunWithDummyResultAndRuleInformation(result *sarif.Result, ruleHelpMsg, ruleHelpMarkdown string, properties, values []string) *sarif.Run { + run := CreateRunWithDummyResultAndRuleProperties(result, properties, values) + if run != nil { + rule := GetRuleById(run, GetResultRuleId(result)) + if rule != nil { + SetRuleHelp(ruleHelpMsg, ruleHelpMarkdown, rule) + } + } + return run +} + func CreateRunWithDummyResultAndRuleProperties(result *sarif.Result, properties, values []string) *sarif.Run { if len(properties) != len(values) { return nil @@ -56,7 +84,7 @@ func CreateDummyResultInPath(fileName string) *sarif.Result { func CreateDummyResult(markdown, msg, ruleId, level string) *sarif.Result { return &sarif.Result{ - Message: *sarif.NewTextMessage(msg).WithMarkdown(markdown), + Message: sarif.Message{Text: &msg, Markdown: &markdown}, Level: &level, RuleID: &ruleId, } @@ -83,7 +111,7 @@ func CreateResultWithDummyLocationAmdProperty(fileName, property, value string) } func CreateResultWithLocations(msg, ruleId, level string, locations ...*sarif.Location) *sarif.Result { - result := CreateDummyResult("", msg, ruleId, level) + result := CreateDummyResult("result-markdown", msg, ruleId, level) result.Locations = locations return result } @@ -103,7 +131,7 @@ func newUintPtr(v uint) *uint { } func CreateDummyResultWithPathAndLogicalLocation(fileName, logicalName, kind, property, value string) *sarif.Result { - result := CreateDummyResult("", "", "rule", "level") + result := CreateDummyResult("result-markdown", "result-msg", "rule", "level") result.Locations = append(result.Locations, CreateDummyLocationWithPathAndLogicalLocation(fileName, logicalName, kind, property, value)) return result } @@ -146,7 +174,7 @@ func CreateDummyPassingResult(ruleId string) *sarif.Result { } func CreateResultWithOneLocation(fileName string, startLine, startCol, endLine, endCol int, snippet, ruleId, level string) *sarif.Result { - return CreateResultWithLocations("", ruleId, level, CreateLocation(fileName, startLine, startCol, endLine, endCol, snippet)) + return CreateResultWithLocations("result-msg", ruleId, level, CreateLocation(fileName, startLine, startCol, endLine, endCol, snippet)) } func CreateCodeFlow(threadFlows ...*sarif.ThreadFlow) *sarif.CodeFlow { @@ -164,3 +192,12 @@ func CreateThreadFlow(locations ...*sarif.Location) *sarif.ThreadFlow { } return stackStrace } + +func CreateDummyRuleWithProperties(id string, properties sarif.Properties) *sarif.ReportingDescriptor { + return &sarif.ReportingDescriptor{ + ID: id, + Properties: properties, + ShortDescription: sarif.NewMultiformatMessageString(""), + FullDescription: sarif.NewMarkdownMultiformatMessageString("rule-markdown").WithText("rule-msg"), + } +} diff --git a/utils/results/common_test.go b/utils/results/common_test.go index 62607fac..f56b2036 100644 --- a/utils/results/common_test.go +++ b/utils/results/common_test.go @@ -399,6 +399,7 @@ func TestGetApplicableCveValue(t *testing.T) { cves: []services.Cve{{Id: "testCve2"}}, expectedResult: jasutils.Applicable, expectedCves: []formats.CveRow{{Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.Applicable), Evidence: []formats.Evidence{{ + Reason: "result-msg", Location: formats.Location{ File: "fileName2", StartLine: 1, @@ -456,7 +457,7 @@ func TestGetApplicableCveValue(t *testing.T) { expectedCves: []formats.CveRow{ {Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.Applicable), - Evidence: []formats.Evidence{{Location: formats.Location{File: "fileName4", StartLine: 1, Snippet: "snippet"}}}, + Evidence: []formats.Evidence{{Reason: "result-msg", Location: formats.Location{File: "fileName4", StartLine: 1, Snippet: "snippet"}}}, }}, }, }, @@ -558,7 +559,7 @@ func TestGetApplicableCveValue(t *testing.T) { expectedResult: jasutils.Applicable, expectedCves: []formats.CveRow{ {Id: "testCve1", Applicability: &formats.Applicability{Status: string(jasutils.NotApplicable)}}, - {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.Applicable), Evidence: []formats.Evidence{{Location: formats.Location{File: "fileName4", StartLine: 1, Snippet: "snippet"}}}}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(jasutils.Applicable), Evidence: []formats.Evidence{{Reason: "result-msg", Location: formats.Location{File: "fileName4", StartLine: 1, Snippet: "snippet"}}}}}, }, }, } diff --git a/utils/results/conversion/convertor.go b/utils/results/conversion/convertor.go index f2c044db..6b321ad2 100644 --- a/utils/results/conversion/convertor.go +++ b/utils/results/conversion/convertor.go @@ -37,6 +37,8 @@ type ResultConvertParams struct { SimplifiedOutput bool // Convert the results to a pretty format if supported (Table and SimpleJson only) Pretty bool + // The JFrog platform URL to be used in the results (Sarif only - GitHub integration) + PlatformUrl string } func NewCommandResultsConvertor(params ResultConvertParams) *CommandResultsConvertor { @@ -67,7 +69,7 @@ func (c *CommandResultsConvertor) ConvertToSimpleJson(cmdResults *results.Securi } func (c *CommandResultsConvertor) ConvertToSarif(cmdResults *results.SecurityCommandResults) (sarifReport *sarif.Report, err error) { - parser := sarifparser.NewCmdResultsSarifConverter(c.Params.IncludeVulnerabilities, c.Params.HasViolationContext, c.Params.PatchBinaryPaths) + parser := sarifparser.NewCmdResultsSarifConverter(c.Params.PlatformUrl, c.Params.IncludeVulnerabilities, c.Params.HasViolationContext, c.Params.PatchBinaryPaths) return parseCommandResults(c.Params, parser, cmdResults) } diff --git a/utils/results/conversion/sarifparser/sarifparser.go b/utils/results/conversion/sarifparser/sarifparser.go index 7a330330..a334285c 100644 --- a/utils/results/conversion/sarifparser/sarifparser.go +++ b/utils/results/conversion/sarifparser/sarifparser.go @@ -45,6 +45,7 @@ var ( ) type CmdResultsSarifConverter struct { + baseJfrogUrl string // Include vulnerabilities/violations in the output includeVulnerabilities bool hasViolationContext bool @@ -61,8 +62,8 @@ type CmdResultsSarifConverter struct { currentCmdType utils.CommandType } -func NewCmdResultsSarifConverter(includeVulnerabilities, hasViolationContext, patchBinaryPaths bool) *CmdResultsSarifConverter { - return &CmdResultsSarifConverter{includeVulnerabilities: includeVulnerabilities, hasViolationContext: hasViolationContext, patchBinaryPaths: patchBinaryPaths} +func NewCmdResultsSarifConverter(baseUrl string, includeVulnerabilities, hasViolationContext, patchBinaryPaths bool) *CmdResultsSarifConverter { + return &CmdResultsSarifConverter{baseJfrogUrl: baseUrl, includeVulnerabilities: includeVulnerabilities, hasViolationContext: hasViolationContext, patchBinaryPaths: patchBinaryPaths} } func (sc *CmdResultsSarifConverter) Get() (*sarif.Report, error) { @@ -96,7 +97,7 @@ func (sc *CmdResultsSarifConverter) ParseNewTargetResults(target results.ScanTar } if sc.scaCurrentRun != nil { // Flush the current run - sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.ScaScan, sc.patchBinaryPaths, sc.currentTarget, sc.scaCurrentRun)...) + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.baseJfrogUrl, sc.currentCmdType, utils.ScaScan, sc.patchBinaryPaths, sc.currentTarget, sc.scaCurrentRun)...) } sc.currentTarget = target if sc.hasViolationContext || sc.includeVulnerabilities { @@ -170,7 +171,7 @@ func (sc *CmdResultsSarifConverter) ParseSecrets(target results.ScanTarget, secr if sc.current == nil { return results.ErrResetConvertor } - sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.SecretsScan, sc.patchBinaryPaths, target, secrets...)...) + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.baseJfrogUrl, sc.currentCmdType, utils.SecretsScan, sc.patchBinaryPaths, target, secrets...)...) return } @@ -181,7 +182,7 @@ func (sc *CmdResultsSarifConverter) ParseIacs(target results.ScanTarget, iacs .. if sc.current == nil { return results.ErrResetConvertor } - sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.IacScan, sc.patchBinaryPaths, target, iacs...)...) + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.baseJfrogUrl, sc.currentCmdType, utils.IacScan, sc.patchBinaryPaths, target, iacs...)...) return } @@ -192,7 +193,7 @@ func (sc *CmdResultsSarifConverter) ParseSast(target results.ScanTarget, sast .. if sc.current == nil { return results.ErrResetConvertor } - sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.currentCmdType, utils.SastScan, sc.patchBinaryPaths, target, sast...)...) + sc.current.Runs = append(sc.current.Runs, patchRunsToPassIngestionRules(sc.baseJfrogUrl, sc.currentCmdType, utils.SastScan, sc.patchBinaryPaths, target, sast...)...) return } @@ -425,7 +426,7 @@ func getScaLicenseViolationMarkdown(depName, version, key string, directDependen return fmt.Sprintf("%s<br/>Direct dependencies:<br/>%s", getLicenseViolationSummary(depName, version, key), formattedDirectDependencies), nil } -func patchRunsToPassIngestionRules(cmdType utils.CommandType, subScanType utils.SubScanType, patchBinaryPaths bool, target results.ScanTarget, runs ...*sarif.Run) []*sarif.Run { +func patchRunsToPassIngestionRules(baseJfrogUrl string, cmdType utils.CommandType, subScanType utils.SubScanType, patchBinaryPaths bool, target results.ScanTarget, runs ...*sarif.Run) []*sarif.Run { patchedRuns := []*sarif.Run{} // Patch changes may alter the original run, so we will create a new run for each for _, run := range runs { @@ -438,7 +439,7 @@ func patchRunsToPassIngestionRules(cmdType utils.CommandType, subScanType utils. sarifutils.SetRunToolName(BinarySecretScannerToolName, patched) } if patched.Tool.Driver != nil { - patched.Tool.Driver.Rules = patchRules(cmdType, subScanType, patched.Tool.Driver.Rules...) + patched.Tool.Driver.Rules = patchRules(baseJfrogUrl, cmdType, subScanType, patched.Tool.Driver.Rules...) } patched.Results = patchResults(cmdType, subScanType, patchBinaryPaths, target, patched, patched.Results...) patchedRuns = append(patchedRuns, patched) @@ -476,7 +477,7 @@ func patchDockerSecretLocations(result *sarif.Result) { } } -func patchRules(commandType utils.CommandType, subScanType utils.SubScanType, rules ...*sarif.ReportingDescriptor) (patched []*sarif.ReportingDescriptor) { +func patchRules(platformBaseUrl string, commandType utils.CommandType, subScanType utils.SubScanType, rules ...*sarif.ReportingDescriptor) (patched []*sarif.ReportingDescriptor) { patched = []*sarif.ReportingDescriptor{} for _, rule := range rules { if rule.Name != nil && rule.ID == *rule.Name { @@ -492,6 +493,10 @@ func patchRules(commandType utils.CommandType, subScanType utils.SubScanType, ru // Patch by transferring the full description to the help field. rule.Help = rule.FullDescription } + // Add analytics hidden pixel to the help content if needed (Github code scanning) + if analytics := getAnalyticsHiddenPixel(platformBaseUrl, subScanType); rule.Help != nil && analytics != "" { + rule.Help.Markdown = utils.NewStringPtr(fmt.Sprintf("%s\n\n%s", sarifutils.GetRuleHelpMarkdown(rule), analytics)) + } patched = append(patched, rule) } return @@ -757,3 +762,22 @@ func getResultWatches(result *sarif.Result) (watches string) { } return } + +// This method returns an image tag of invisible image that is used to track some parameters. +// It will send a count as soon as the page with it is logged. +func getAnalyticsHiddenPixel(baseUrl string, resultOfSubScan utils.SubScanType) string { + jobId := os.Getenv(utils.JfrogExternalJobIdEnv) + runId := os.Getenv(utils.JfrogExternalRunIdEnv) + gitRepo := os.Getenv(utils.JfrogExternalGitRepoEnv) + if jobId == "" || runId == "" || gitRepo == "" { + return "" + } + return fmt.Sprintf( + "![](%sui/api/v1/u?s=1&m=2&job_id=%s&run_id=%s&git_repo=%s&type=%s)", + baseUrl, + jobId, + runId, + gitRepo, + resultOfSubScan.String(), + ) +} diff --git a/utils/results/conversion/sarifparser/sarifparser_test.go b/utils/results/conversion/sarifparser/sarifparser_test.go index d21c35dc..29f48a31 100644 --- a/utils/results/conversion/sarifparser/sarifparser_test.go +++ b/utils/results/conversion/sarifparser/sarifparser_test.go @@ -320,7 +320,7 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "dir", "file")))), }, expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file"))), + sarifutils.CreateRunWithDummyResultsInWdWithHelp("rule-msg", "rule-markdown", wd, sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file"))), }, }, { @@ -332,17 +332,18 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { sarifutils.CreateRunWithDummyResultAndRuleProperties( sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), []string{"applicability"}, []string{"applicable"}).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}), - sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateRunWithDummyResultsInWdWithHelp("rule-msg", "rule-markdown", wd, sarifutils.CreateDummyResultWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithMessage(sarif.NewTextMessage("some-msg")), ), }, expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultAndRuleProperties( + sarifutils.CreateRunWithDummyResultAndRuleInformation( sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), ), + "rule-msg", "rule-markdown", []string{"applicability"}, []string{"applicable"}).WithInvocations([]*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}), - sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateRunWithDummyResultsInWdWithHelp("rule-msg", "rule-markdown", wd, sarifutils.CreateDummyResultWithFingerprint("some-msg\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "some-msg", jfrogFingerprintAlgorithmName, "9522c1d915eef55b4a0dc9e160bf5dc7", sarifutils.CreateDummyLocationWithPathAndLogicalLocation("sha256__f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256"), ), @@ -350,7 +351,7 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { }, }, { - name: "Docker image scan - with env vars", + name: "Docker image scan - with env vars (SCA)", target: results.ScanTarget{Name: "dockerImage:imageVersion"}, cmdType: utils.DockerImage, subScan: utils.ScaScan, @@ -363,7 +364,7 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { ), }, expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateRunWithDummyResultsWithRuleInformation("", "", "rule-msg", "rule-markdown\n\n![](url/ui/api/v1/u?s=1&m=2&job_id=job-id&run_id=run-id&git_repo=repo&type=sca)", "rule-msg", "rule-markdown\n\n![](url/ui/api/v1/u?s=1&m=2&job_id=job-id&run_id=run-id&git_repo=repo&type=sca)", wd, sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "eda26ae830c578197aeda65a82d7f093", sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation(filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml"))), @@ -385,7 +386,7 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { ), }, expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(dockerfileDir, + sarifutils.CreateRunWithDummyResultsWithRuleInformation("", "", "rule-msg", "rule-markdown\n\n![](url/ui/api/v1/u?s=1&m=2&job_id=job-id&run_id=run-id&git_repo=repo&type=sca)", "rule-msg", "rule-markdown\n\n![](url/ui/api/v1/u?s=1&m=2&job_id=job-id&run_id=run-id&git_repo=repo&type=sca)", dockerfileDir, sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("some-msg\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha256): f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml")), "some-msg", jfrogFingerprintAlgorithmName, "8cbd7268a4d20f2358ba2667ebd18956", sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "f752cb05a39e65f231a3c47c2e08cbeac1c15e4daff0188cb129c12a3ea3049d", "layer", "algorithm", "sha256").WithPhysicalLocation( sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation("Dockerfile")), @@ -407,20 +408,34 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { }), }, expectedResults: []*sarif.Run{ - { - Tool: sarif.Tool{ - Driver: sarifutils.CreateDummyDriver(BinarySecretScannerToolName, &sarif.ReportingDescriptor{ - ID: "rule", - ShortDescription: sarif.NewMultiformatMessageString("[Secret in Binary found] "), - }), - }, - Invocations: []*sarif.Invocation{sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd))}, - Results: []*sarif.Result{ - sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("🔒 Found Secrets in Binary docker scanning:\nImage: dockerImage:imageVersion\nLayer (sha1): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: %s\nEvidence: snippet", filepath.Join("usr", "src", "app", "server", "index.js")), "", jfrogFingerprintAlgorithmName, "dee156c9fd75a4237102dc8fb29277a2", - sarifutils.CreateDummyLocationWithPathAndLogicalLocation(filepath.Join("usr", "src", "app", "server", "index.js"), "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "layer", "algorithm", "sha1"), + sarifutils.CreateRunWithDummyResultsWithRuleInformation(BinarySecretScannerToolName, "[Secret in Binary found] ", "rule-msg", "rule-markdown", "rule-msg", "rule-markdown", wd, + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("🔒 Found Secrets in Binary docker scanning:\nImage: dockerImage:imageVersion\nLayer (sha1): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: %s\nEvidence: snippet", filepath.Join("usr", "src", "app", "server", "index.js")), "result-msg", jfrogFingerprintAlgorithmName, "dee156c9fd75a4237102dc8fb29277a2", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation(filepath.Join("usr", "src", "app", "server", "index.js"), "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "layer", "algorithm", "sha1"), + ), + ), + }, + }, + { + name: "Docker image scan - with env vars (Secrets)", + target: results.ScanTarget{Name: "dockerImage:imageVersion"}, + cmdType: utils.DockerImage, + subScan: utils.SecretsScan, + withEnvVars: true, + input: []*sarif.Run{ + sarifutils.CreateRunNameWithResults("some tool name", + sarifutils.CreateDummyResultInPath(fmt.Sprintf("file://%s", filepath.Join(wd, "unpacked", "filesystem", "blobs", "sha1", "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "usr", "src", "app", "server", "index.js"))), + ).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation(wd)), + }), + }, + expectedResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResultsWithRuleInformation(BinarySecretScannerToolName, "[Secret in Binary found] ", "rule-msg", "rule-markdown\n\n![](url/ui/api/v1/u?s=1&m=2&job_id=job-id&run_id=run-id&git_repo=repo&type=secrets)", "rule-msg", "rule-markdown\n\n![](url/ui/api/v1/u?s=1&m=2&job_id=job-id&run_id=run-id&git_repo=repo&type=secrets)", wd, + sarifutils.CreateDummyResultWithFingerprint(fmt.Sprintf("🔒 Found Secrets in Binary docker scanning:\nGithub Actions Workflow: %s\nRun: 123\nImage: dockerImage:imageVersion\nLayer (sha1): 9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0\nFilepath: %s\nEvidence: snippet", filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml"), filepath.Join("usr", "src", "app", "server", "index.js")), "result-msg", jfrogFingerprintAlgorithmName, "e721eacf317da6090eca3522308abd28", + sarifutils.CreateDummyLocationWithPathAndLogicalLocation("", "9e88ea9de1b44baba5e96a79e33e4af64334b2bf129e838e12f6dae71b5c86f0", "layer", "algorithm", "sha1").WithPhysicalLocation( + sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation(filepath.Join(GithubBaseWorkflowDir, "workflowFile.yml"))), ), - }, - }, + ), + ), }, }, { @@ -434,8 +449,8 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { ), }, expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, - sarifutils.CreateDummyResultWithFingerprint("", "", jfrogFingerprintAlgorithmName, "e72a936dc73acbc4283a93230ff9b6e8", sarifutils.CreateDummyLocationInPath(filepath.Join("dir", "binary"))), + sarifutils.CreateRunWithDummyResultsInWdWithHelp("rule-msg", "rule-markdown", wd, + sarifutils.CreateDummyResultWithFingerprint("result-msg", "result-msg", jfrogFingerprintAlgorithmName, "e72a936dc73acbc4283a93230ff9b6e8", sarifutils.CreateDummyLocationInPath(filepath.Join("dir", "binary"))), ), }, }, @@ -452,7 +467,7 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { ), }, expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateRunWithDummyResultsInWdWithHelp("rule-msg", "rule-markdown", wd, sarifutils.CreateDummyResultInPath("Package-Descriptor"), ), }, @@ -470,7 +485,7 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { ), }, expectedResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResultsInWd(wd, + sarifutils.CreateRunWithDummyResultsInWdWithHelp("rule-msg", "rule-markdown", wd, sarifutils.CreateDummyResultInPath(filepath.Join("dir", "file")), ), }, @@ -478,23 +493,29 @@ func TestPatchRunsToPassIngestionRules(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + cleanUps := []func(){} if tc.withEnvVars { - cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "workflow name") - defer cleanFileEnv() - cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "123") - defer cleanRunNumEnv() + // Some content depends on env vars values + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "workflow name")) + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "123")) + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, utils.JfrogExternalJobIdEnv, "job-id")) + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, utils.JfrogExternalRunIdEnv, "run-id")) + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, utils.JfrogExternalGitRepoEnv, "repo")) } else { - // Since the the env are provided by the - cleanFileEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "") - defer cleanFileEnv() - cleanRunNumEnv := clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "") - defer cleanRunNumEnv() + // Since some of the env vars are provided by the test in GitHub Actions, we need to clean them up before running the test + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowNameEnvVar, "")) + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, CurrentWorkflowRunNumberEnvVar, "")) } + defer func() { + for _, cleanUp := range cleanUps { + cleanUp() + } + }() if tc.withDockerfile { revertWd := clientTests.ChangeDirWithCallback(t, wd, dockerfileDir) defer revertWd() } - patchedRuns := patchRunsToPassIngestionRules(tc.cmdType, tc.subScan, true, tc.target, tc.input...) + patchedRuns := patchRunsToPassIngestionRules("url/", tc.cmdType, tc.subScan, true, tc.target, tc.input...) assert.ElementsMatch(t, tc.expectedResults, patchedRuns) }) } diff --git a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go index c1f30b3b..49316ec5 100644 --- a/utils/results/conversion/simplejsonparser/simplejsonparser_test.go +++ b/utils/results/conversion/simplejsonparser/simplejsonparser_test.go @@ -350,7 +350,7 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", Applicable: jasutils.NotApplicable.String(), - Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{ScannerDescription: "rule-msg", Status: jasutils.NotApplicable.String()}}}, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, ImpactedDependencyName: "component-A", @@ -366,7 +366,7 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", Applicable: jasutils.NotApplicable.String(), - Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{ScannerDescription: "rule-msg", Status: jasutils.NotApplicable.String()}}}, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, ImpactedDependencyName: "component-B", @@ -385,7 +385,8 @@ func TestPrepareSimpleJsonVulnerabilities(t *testing.T) { Cves: []formats.CveRow{{ Id: "CVE-2", Applicability: &formats.Applicability{ - Status: jasutils.Applicable.String(), + ScannerDescription: "rule-msg", + Status: jasutils.Applicable.String(), Evidence: []formats.Evidence{{ Location: formats.Location{File: "file", StartLine: 0, StartColumn: 0, EndLine: 0, EndColumn: 0, Snippet: "snippet"}, Reason: "applic_CVE-2", @@ -513,7 +514,7 @@ func TestPrepareSimpleJsonViolations(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", Applicable: jasutils.NotApplicable.String(), - Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{ScannerDescription: "rule-msg", Status: jasutils.NotApplicable.String()}}}, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, ImpactedDependencyName: "component-A", @@ -529,7 +530,7 @@ func TestPrepareSimpleJsonViolations(t *testing.T) { Summary: "summary-1", IssueId: "XRAY-1", Applicable: jasutils.NotApplicable.String(), - Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{Status: jasutils.NotApplicable.String()}}}, + Cves: []formats.CveRow{{Id: "CVE-1", Applicability: &formats.Applicability{ScannerDescription: "rule-msg", Status: jasutils.NotApplicable.String()}}}, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, ImpactedDependencyName: "component-B", @@ -548,7 +549,8 @@ func TestPrepareSimpleJsonViolations(t *testing.T) { Cves: []formats.CveRow{{ Id: "CVE-2", Applicability: &formats.Applicability{ - Status: jasutils.Applicable.String(), + ScannerDescription: "rule-msg", + Status: jasutils.Applicable.String(), Evidence: []formats.Evidence{{ Location: formats.Location{File: "file", StartLine: 0, StartColumn: 0, EndLine: 0, EndColumn: 0, Snippet: "snippet"}, Reason: "applic_CVE-2", @@ -681,8 +683,10 @@ func TestPrepareSimpleJsonJasIssues(t *testing.T) { jasIssues: issues, expectedOutput: []formats.SourceCodeRow{ { - Location: formats.Location{File: "file", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "secret-snippet"}, - SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 13}, + Finding: "result-msg", + ScannerDescription: "rule-msg", + Location: formats.Location{File: "file", StartLine: 1, StartColumn: 2, EndLine: 3, EndColumn: 4, Snippet: "secret-snippet"}, + SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 13}, }, }, }, diff --git a/utils/results/output/resultwriter.go b/utils/results/output/resultwriter.go index 4706321e..cc669a07 100644 --- a/utils/results/output/resultwriter.go +++ b/utils/results/output/resultwriter.go @@ -20,6 +20,8 @@ import ( type ResultsWriter struct { // The scan commandResults. commandResults *results.SecurityCommandResults + // PlatformUrl The JFrog platform URL to generate GH analysis links. + platformUrl string // Format The output format. format format.OutputFormat // IncludeVulnerabilities If true, include all vulnerabilities as part of the output. Else, include violations only. @@ -47,6 +49,11 @@ func (rw *ResultsWriter) SetOutputFormat(f format.OutputFormat) *ResultsWriter { return rw } +func (rw *ResultsWriter) SetPlatformUrl(platformUrl string) *ResultsWriter { + rw.platformUrl = platformUrl + return rw +} + func (rw *ResultsWriter) SetIsMultipleRootProject(isMultipleRootProject bool) *ResultsWriter { rw.isMultipleRoots = &isMultipleRootProject return rw @@ -138,6 +145,7 @@ func (rw *ResultsWriter) PrintScanResults() error { func (rw *ResultsWriter) createResultsConvertor(pretty bool) *conversion.CommandResultsConvertor { return conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + PlatformUrl: rw.platformUrl, IsMultipleRoots: rw.isMultipleRoots, IncludeLicenses: rw.includeLicenses, IncludeVulnerabilities: rw.includeVulnerabilities, diff --git a/utils/results/output/securityJobSummary.go b/utils/results/output/securityJobSummary.go index c9571099..e27b0e01 100644 --- a/utils/results/output/securityJobSummary.go +++ b/utils/results/output/securityJobSummary.go @@ -73,6 +73,7 @@ func newResultSummary(cmdResults *results.SecurityCommandResults, serverDetails summary.ResultType = cmdResults.CmdType summary.Args = &ResultSummaryArgs{BaseJfrogUrl: serverDetails.Url} summary.Summary, err = conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + PlatformUrl: serverDetails.Url, IncludeVulnerabilities: vulnerabilitiesRequested, HasViolationContext: violationsRequested, Pretty: true, @@ -201,6 +202,7 @@ func RecordSarifOutput(cmdResults *results.SecurityCommandResults, serverDetails } // Convert the results to SARIF format sarifReport, err := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + PlatformUrl: serverDetails.Url, IncludeVulnerabilities: includeVulnerabilities, HasViolationContext: hasViolationContext, PatchBinaryPaths: true, @@ -531,14 +533,38 @@ func getJfrogUrl(index commandsummary.Index, args ResultSummaryArgs, summary *fo return Link.Format(commandsummary.StaticMarkdownConfig.GetExtendedSummaryLangPage(), "🐸 Unlock detailed findings") } if moreInfoUrls := summary.GetMoreInfoUrls(); len(moreInfoUrls) > 0 { - return Link.Format(moreInfoUrls[0], "See the results of the scan in JFrog") + return Link.Format(addAnalyticsQueryParamsIfNeeded(moreInfoUrls[0], index), "See the results of the scan in JFrog") } if defaultUrl := args.GetUrl(index, summary.GetScanIds()...); defaultUrl != "" { - return Link.Format(defaultUrl, "See the results of the scan in JFrog") + return Link.Format(addAnalyticsQueryParamsIfNeeded(defaultUrl, index), "See the results of the scan in JFrog") } return } +// adds analytics query params to the url if running in Github +func addAnalyticsQueryParamsIfNeeded(url string, index commandsummary.Index) string { + githubJobId := os.Getenv(utils.JfrogExternalJobIdEnv) + if githubJobId == "" { + // Not running in Github no need to add analytics + return url + } + suffixValues := []string{fmt.Sprintf("gh_job_id=%s", githubJobId)} + // Add section analytics + indexValue := "gh_section=" + switch index { + case commandsummary.BuildScan: + indexValue += "build" + default: + indexValue += "on_demand_scan" + } + suffixValues = append(suffixValues, indexValue) + // Add the suffix to the url + if strings.Contains(url, "?") { + return fmt.Sprintf("%s%s", url, strings.Join(suffixValues, "&")) + } + return fmt.Sprintf("%s?%s", url, strings.Join(suffixValues, "&")) +} + func (mg DynamicMarkdownGenerator) generateResultsMarkdown(violations bool, moreInfoUrl string, content *formats.ScanResultSummary) (markdown string) { if !content.HasIssues() { markdown = getNoIssuesMarkdown(violations) diff --git a/utils/results/output/securityJobSummary_test.go b/utils/results/output/securityJobSummary_test.go index 5fc56acd..7d748802 100644 --- a/utils/results/output/securityJobSummary_test.go +++ b/utils/results/output/securityJobSummary_test.go @@ -17,30 +17,36 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-cli-security/utils/validations" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" clientTests "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" ) var ( summaryExpectedContentDir = filepath.Join("..", "..", "..", "tests", "testdata", "output", "jobSummary") +) - securityScaResults = formats.ResultSummary{ +func getTestScaSecurityScanResultSummary() formats.ResultSummary { + return formats.ResultSummary{ "Critical": map[string]int{jasutils.Applicable.String(): 2, jasutils.NotApplicable.String(): 2, jasutils.NotCovered.String(): 3, jasutils.ApplicabilityUndetermined.String(): 1}, "High": map[string]int{jasutils.Applicable.String(): 2, jasutils.ApplicabilityUndetermined.String(): 3}, "Low": map[string]int{jasutils.NotApplicable.String(): 3}, "Unknown": map[string]int{jasutils.NotCovered.String(): 1}, } - violationResults = formats.ScanResultSummary{ +} + +func getTestViolationResults() formats.ScanResultSummary { + return formats.ScanResultSummary{ ScaResults: &formats.ScaScanResultSummary{ ScanIds: []string{validations.TestScaScanId}, - MoreInfoUrls: []string{validations.TestMoreInfoUrl}, - Security: securityScaResults, + MoreInfoUrls: []string{validations.TestMoreInfoUrl + "scan-descendants/master?repoId=10"}, + Security: getTestScaSecurityScanResultSummary(), License: formats.ResultSummary{"High": map[string]int{formats.NoStatus: 1}}, OperationalRisk: formats.ResultSummary{"Low": map[string]int{formats.NoStatus: 2}}, }, SecretsResults: &formats.ResultSummary{"Medium": map[string]int{formats.NoStatus: 3}}, } -) +} func TestSaveSarifOutputOnlyForJasEntitled(t *testing.T) { testCases := []struct { @@ -96,12 +102,12 @@ func TestSaveLoadData(t *testing.T) { ScaResults: &formats.ScaScanResultSummary{ ScanIds: []string{validations.TestScaScanId}, MoreInfoUrls: []string{validations.TestMoreInfoUrl}, - Security: securityScaResults, + Security: getTestScaSecurityScanResultSummary(), }, }, Violations: &formats.ScanViolationsSummary{ Watches: []string{"watch1"}, - ScanResultSummary: violationResults, + ScanResultSummary: getTestViolationResults(), }, }, }, @@ -147,7 +153,7 @@ func TestSaveLoadData(t *testing.T) { Target: "build-name (build-number)", Violations: &formats.ScanViolationsSummary{ Watches: []string{"watch"}, - ScanResultSummary: violationResults, + ScanResultSummary: getTestViolationResults(), }, }, }, @@ -236,6 +242,7 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { index commandsummary.Index args *ResultSummaryArgs violations bool + GithubEnvs bool content []formats.ResultsSummary NoExtendedView bool expectedContentPath string @@ -342,6 +349,23 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { }}, }}, }, + { + name: "Build Scan Vulnerabilities - Github Envs", + index: commandsummary.BuildScan, + GithubEnvs: true, + expectedContentPath: filepath.Join(summaryExpectedContentDir, "build_scan_analytics_vulnerabilities.md"), + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, BuildName: "build-name", BuildNumbers: []string{"build-number"}}, + content: []formats.ResultsSummary{{ + Scans: []formats.ScanSummary{{ + Target: "build-name (build-number)", + Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ + ScanIds: []string{validations.TestScaScanId}, + MoreInfoUrls: []string{validations.TestMoreInfoUrl}, + Security: formats.ResultSummary{"High": map[string]int{formats.NoStatus: 3}, "Medium": map[string]int{formats.NoStatus: 1}, "Unknown": map[string]int{formats.NoStatus: 20}}, + }}, + }}, + }}, + }, { name: "Binary Scan Vulnerabilities", index: commandsummary.BinariesScan, @@ -358,6 +382,23 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { }}, }}, }, + { + name: "Binary Scan Vulnerabilities - Github Envs", + index: commandsummary.BinariesScan, + GithubEnvs: true, + expectedContentPath: filepath.Join(summaryExpectedContentDir, "binary_analytics_vulnerabilities.md"), + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl}, + content: []formats.ResultsSummary{{ + Scans: []formats.ScanSummary{{ + Target: filepath.Join(wd, "binary-with-issues"), + Vulnerabilities: &formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ + ScanIds: []string{validations.TestScaScanId, "scan-id-2"}, + MoreInfoUrls: []string{""}, + Security: formats.ResultSummary{"Critical": map[string]int{formats.NoStatus: 33}, "Low": map[string]int{formats.NoStatus: 11}}, + }}, + }}, + }}, + }, { name: "Docker Scan Vulnerabilities", index: commandsummary.DockerScan, @@ -370,7 +411,7 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { ScaResults: &formats.ScaScanResultSummary{ ScanIds: []string{validations.TestScaScanId}, MoreInfoUrls: []string{""}, - Security: securityScaResults, + Security: getTestScaSecurityScanResultSummary(), }, SecretsResults: &formats.ResultSummary{ "Medium": map[string]int{formats.NoStatus: 3}, @@ -380,51 +421,79 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { }}, }, { - name: "Violations", + name: "Violations - Not extendedView", index: commandsummary.DockerScan, violations: true, - expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations.md"), + NoExtendedView: true, + expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations_not_extended_view.md"), args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "image.tar"), Violations: &formats.ScanViolationsSummary{ - Watches: []string{"watch1", "watch2", "watch3", "watch4", "watch5"}, - ScanResultSummary: violationResults, + Watches: []string{"watch1"}, + ScanResultSummary: getTestViolationResults(), }, }}, }}, }, { - name: "Violations - Not extendedView", + name: "Vulnerability not requested", + index: commandsummary.DockerScan, + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, + content: []formats.ResultsSummary{{ + Scans: []formats.ScanSummary{{ + Target: filepath.Join(wd, "image.tar"), + }}, + }}, + }, + { + name: "Violations - Github Envs", index: commandsummary.DockerScan, violations: true, - NoExtendedView: true, - expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations_not_extended_view.md"), + GithubEnvs: true, + expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations_analytics.md"), args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "image.tar"), Violations: &formats.ScanViolationsSummary{ - Watches: []string{"watch1"}, - ScanResultSummary: violationResults, + Watches: []string{"watch1", "watch2", "watch3", "watch4", "watch5"}, + ScanResultSummary: getTestViolationResults(), }, }}, }}, }, { - name: "Vulnerability not requested", - index: commandsummary.DockerScan, - args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, + name: "Violations", + index: commandsummary.DockerScan, + violations: true, + expectedContentPath: filepath.Join(summaryExpectedContentDir, "violations.md"), + args: &ResultSummaryArgs{BaseJfrogUrl: validations.TestPlatformUrl, DockerImage: "dockerImage:version"}, content: []formats.ResultsSummary{{ Scans: []formats.ScanSummary{{ Target: filepath.Join(wd, "image.tar"), + Violations: &formats.ScanViolationsSummary{ + Watches: []string{"watch1", "watch2", "watch3", "watch4", "watch5"}, + ScanResultSummary: getTestViolationResults(), + }, }}, }}, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { + cleanUps := []func(){} + if testCase.GithubEnvs { + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, utils.JfrogExternalJobIdEnv, "some-job-id")) + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, utils.JfrogExternalRunIdEnv, "some-run-id")) + cleanUps = append(cleanUps, clientTests.SetEnvWithCallbackAndAssert(t, utils.JfrogExternalGitRepoEnv, "some-repo")) + } + defer func() { + for _, cleanUp := range cleanUps { + cleanUp() + } + }() // Read expected content from file (or empty string expected if no file is provided) expectedContent := "" if testCase.expectedContentPath != "" { @@ -445,6 +514,7 @@ func TestGenerateJobSummaryMarkdown(t *testing.T) { summary, err = createDummyDynamicMarkdown(testCase.content, testCase.index, *testCase.args, testCase.violations, !testCase.NoExtendedView) } assert.NoError(t, err) + log.Info(expectedContent) assert.Equal(t, expectedContent, summary) }) } diff --git a/utils/utils.go b/utils/utils.go index 1681ae38..9db0f861 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -28,6 +28,10 @@ const ( BaseDocumentationURL = "https://docs.jfrog-applications.jfrog.io/jfrog-security-features/" JasInfoURL = "https://jfrog.com/xray/" EntitlementsMinVersion = "3.66.5" + + JfrogExternalRunIdEnv = "JFROG_CLI_USAGE_RUN_ID" + JfrogExternalJobIdEnv = "JFROG_CLI_USAGE_JOB_ID" + JfrogExternalGitRepoEnv = "JFROG_CLI_USAGE_GIT_REPO" ) var ( @@ -147,6 +151,10 @@ func GetAsJsonString(output interface{}, escapeValues, indent bool) (string, err return string(results), nil } +func NewStringPtr(v string) *string { + return &v +} + func NewBoolPtr(v bool) *bool { return &v }