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:&Tab;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:&Tab;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:&Tab;17 Security&Tab;2 Operational&Tab;1 License&Tab;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:&Tab;17 Security&Tab;2 Operational&Tab;1 License&Tab;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:&Tab;17 Security&Tab;2 Operational&Tab;1 License&Tab;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:&Tab;20 Security&Tab;2 Operational&Tab;1 License&Tab;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:&Tab;17 Security&Tab;2 Operational&Tab;1 License&Tab;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
 }