github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/utils/outputwriter/outputcontent.go (about) 1 package outputwriter 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/jfrog/froggit-go/vcsutils" 8 "github.com/jfrog/jfrog-cli-core/v2/xray/formats" 9 xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" 10 ) 11 12 const ( 13 FrogbotTitlePrefix = "[🐸 Frogbot]" 14 FrogbotRepoUrl = "https://github.com/jfrog/frogbot" 15 FrogbotDocumentationUrl = "https://docs.jfrog-applications.jfrog.io/jfrog-applications/frogbot" 16 ReviewCommentId = "FrogbotReviewComment" 17 18 vulnerableDependenciesTitle = "📦 Vulnerable Dependencies" 19 vulnerableDependenciesResearchDetailsSubTitle = "🔬 Research Details" 20 21 contextualAnalysisTitle = "📦🔍 Contextual Analysis CVE Vulnerability" 22 iacTitle = "🛠️ Infrastructure as Code Vulnerability" 23 sastTitle = "🎯 Static Application Security Testing (SAST) Vulnerability" 24 ) 25 26 var ( 27 CommentGeneratedByFrogbot = MarkAsLink("🐸 JFrog Frogbot", FrogbotDocumentationUrl) 28 jasFeaturesMsgWhenNotEnabled = MarkAsBold("Frogbot") + " also supports " + MarkAsBold("Contextual Analysis, Secret Detection, IaC and SAST Vulnerabilities Scanning") + ". This features are included as part of the " + MarkAsLink("JFrog Advanced Security", "https://jfrog.com/advanced-security") + " package, which isn't enabled on your system." 29 ) 30 31 func GetPRSummaryContent(content string, issuesExists, isComment bool, writer OutputWriter) string { 32 comment := strings.Builder{} 33 comment.WriteString(writer.Image(getPRSummaryBanner(issuesExists, isComment, writer.VcsProvider()))) 34 customCommentTitle := writer.PullRequestCommentTitle() 35 if customCommentTitle != "" { 36 WriteContent(&comment, writer.MarkAsTitle(MarkAsBold(customCommentTitle), 2)) 37 } 38 if issuesExists { 39 WriteContent(&comment, content) 40 } 41 WriteContent(&comment, 42 untitledForJasMsg(writer), 43 footer(writer), 44 ) 45 return comment.String() 46 } 47 48 func getPRSummaryBanner(issuesExists, isComment bool, provider vcsutils.VcsProvider) ImageSource { 49 if !isComment { 50 return fixCVETitleSrc(provider) 51 } 52 if !issuesExists { 53 return NoIssuesTitleSrc(provider) 54 } 55 return PRSummaryCommentTitleSrc(provider) 56 } 57 58 func IsFrogbotSummaryComment(writer OutputWriter, content string) bool { 59 client := writer.VcsProvider() 60 return strings.Contains(content, GetBanner(NoIssuesTitleSrc(client))) || 61 strings.Contains(content, GetSimplifiedTitle(NoIssuesTitleSrc(client))) || 62 strings.Contains(content, GetBanner(PRSummaryCommentTitleSrc(client))) || 63 strings.Contains(content, GetSimplifiedTitle(PRSummaryCommentTitleSrc(client))) 64 } 65 66 func NoIssuesTitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource { 67 if vcsProvider == vcsutils.GitLab { 68 return NoVulnerabilityMrBannerSource 69 } 70 return NoVulnerabilityPrBannerSource 71 } 72 73 func PRSummaryCommentTitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource { 74 if vcsProvider == vcsutils.GitLab { 75 return VulnerabilitiesMrBannerSource 76 } 77 return VulnerabilitiesPrBannerSource 78 } 79 80 func fixCVETitleSrc(vcsProvider vcsutils.VcsProvider) ImageSource { 81 if vcsProvider == vcsutils.GitLab { 82 return VulnerabilitiesFixMrBannerSource 83 } 84 return VulnerabilitiesFixPrBannerSource 85 } 86 87 func untitledForJasMsg(writer OutputWriter) string { 88 if writer.AvoidExtraMessages() || writer.IsEntitledForJas() { 89 return "" 90 } 91 return writer.MarkAsDetails("Note:", 0, fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(jasFeaturesMsgWhenNotEnabled))) 92 } 93 94 func footer(writer OutputWriter) string { 95 return fmt.Sprintf("%s\n%s", SectionDivider(), writer.MarkInCenter(CommentGeneratedByFrogbot)) 96 } 97 98 func getVulnerabilitiesSummaryTable(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { 99 // Construct table 100 columns := []string{"SEVERITY"} 101 if writer.IsShowingCaColumn() { 102 columns = append(columns, "CONTEXTUAL ANALYSIS") 103 } 104 columns = append(columns, "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY", "FIXED VERSIONS", "CVES") 105 table := NewMarkdownTable(columns...).SetDelimiter(writer.Separator()) 106 if _, ok := writer.(*SimplifiedOutput); ok { 107 // The values in this cell can be potentially large, since SimplifiedOutput does not support tags, we need to show each value in a separate row. 108 // It means that the first row will show the full details, and the following rows will show only the direct dependency. 109 // It makes it easier to read the table and less crowded with text in a single cell that could be potentially large. 110 table.GetColumnInfo("DIRECT DEPENDENCIES").ColumnType = MultiRowColumn 111 } 112 // Construct rows 113 for _, vulnerability := range vulnerabilities { 114 row := []CellData{{writer.FormattedSeverity(vulnerability.Severity, vulnerability.Applicable)}} 115 if writer.IsShowingCaColumn() { 116 row = append(row, NewCellData(vulnerability.Applicable)) 117 } 118 row = append(row, 119 getDirectDependenciesCellData("%s:%s", vulnerability.Components), 120 NewCellData(fmt.Sprintf("%s %s", vulnerability.ImpactedDependencyName, vulnerability.ImpactedDependencyVersion)), 121 NewCellData(vulnerability.FixedVersions...), 122 getCveIdsCellData(vulnerability.Cves), 123 ) 124 table.AddRowWithCellData(row...) 125 } 126 return table.Build() 127 } 128 129 func getDirectDependenciesCellData(format string, components []formats.ComponentRow) (dependencies CellData) { 130 if len(components) == 0 { 131 return NewCellData() 132 } 133 for _, component := range components { 134 dependencies = append(dependencies, fmt.Sprintf(format, component.Name, component.Version)) 135 } 136 return 137 } 138 139 func getCveIdsCellData(cveRows []formats.CveRow) (ids CellData) { 140 if len(cveRows) == 0 { 141 return NewCellData() 142 } 143 for _, cve := range cveRows { 144 ids = append(ids, cve.Id) 145 } 146 return 147 } 148 149 func VulnerabilitiesContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { 150 if len(vulnerabilities) == 0 { 151 return "" 152 } 153 var contentBuilder strings.Builder 154 // Write summary table part 155 WriteContent(&contentBuilder, 156 writer.MarkAsTitle(vulnerableDependenciesTitle, 2), 157 writer.MarkAsTitle("✍️ Summary", 3), 158 writer.MarkInCenter(getVulnerabilitiesSummaryTable(vulnerabilities, writer)), 159 ) 160 // Write for each vulnerability details part 161 detailsContent := strings.TrimSpace(getVulnerabilityDetailsContent(vulnerabilities, writer)) 162 if detailsContent != "" { 163 if len(vulnerabilities) == 1 { 164 WriteContent(&contentBuilder, writer.MarkAsTitle(vulnerableDependenciesResearchDetailsSubTitle, 3), detailsContent) 165 } else { 166 WriteContent(&contentBuilder, writer.MarkAsDetails(vulnerableDependenciesResearchDetailsSubTitle, 3, detailsContent)) 167 } 168 } 169 return contentBuilder.String() 170 } 171 172 func getVulnerabilityDetailsContent(vulnerabilities []formats.VulnerabilityOrViolationRow, writer OutputWriter) string { 173 var descriptionContentBuilder strings.Builder 174 for i := range vulnerabilities { 175 vulDescriptionContent := createVulnerabilityResearchDescription(&vulnerabilities[i]) 176 if vulDescriptionContent == "" { 177 // No content 178 continue 179 } 180 if len(vulnerabilities) == 1 { 181 WriteContent(&descriptionContentBuilder, vulDescriptionContent) 182 break 183 } 184 WriteContent(&descriptionContentBuilder, 185 writer.MarkAsDetails( 186 fmt.Sprintf(`%s %s %s`, 187 getVulnerabilityDescriptionIdentifier(vulnerabilities[i].Cves, vulnerabilities[i].IssueId), 188 vulnerabilities[i].ImpactedDependencyName, 189 vulnerabilities[i].ImpactedDependencyVersion), 190 4, vulDescriptionContent, 191 ), 192 ) 193 } 194 return descriptionContentBuilder.String() 195 } 196 197 func createVulnerabilityResearchDescription(vulnerability *formats.VulnerabilityOrViolationRow) string { 198 var descriptionBuilder strings.Builder 199 vulnResearch := vulnerability.JfrogResearchInformation 200 if vulnResearch == nil { 201 vulnResearch = &formats.JfrogResearchInformation{Details: vulnerability.Summary} 202 } else if vulnResearch.Details == "" { 203 vulnResearch.Details = vulnerability.Summary 204 } 205 206 if vulnResearch.Details != "" { 207 WriteContent(&descriptionBuilder, MarkAsBold("Description:"), vulnResearch.Details) 208 } 209 if vulnResearch.Remediation != "" { 210 if vulnResearch.Details != "" { 211 WriteNewLine(&descriptionBuilder) 212 } 213 WriteContent(&descriptionBuilder, MarkAsBold("Remediation:"), vulnResearch.Remediation) 214 } 215 return descriptionBuilder.String() 216 } 217 218 func getVulnerabilityDescriptionIdentifier(cveRows []formats.CveRow, xrayId string) string { 219 identifier := xrayutils.GetIssueIdentifier(cveRows, xrayId) 220 if identifier == "" { 221 return "" 222 } 223 return fmt.Sprintf("[ %s ]", identifier) 224 } 225 226 func LicensesContent(licenses []formats.LicenseRow, writer OutputWriter) string { 227 if len(licenses) == 0 { 228 return "" 229 } 230 // Title 231 var contentBuilder strings.Builder 232 WriteContent(&contentBuilder, writer.MarkAsTitle("⚖️ Violated Licenses", 2)) 233 // Content 234 table := NewMarkdownTable("LICENSE", "DIRECT DEPENDENCIES", "IMPACTED DEPENDENCY").SetDelimiter(writer.Separator()) 235 for _, license := range licenses { 236 table.AddRowWithCellData( 237 NewCellData(license.LicenseKey), 238 getDirectDependenciesCellData("%s %s", license.Components), 239 NewCellData(fmt.Sprintf("%s %s", license.ImpactedDependencyName, license.ImpactedDependencyVersion)), 240 ) 241 } 242 WriteContent(&contentBuilder, writer.MarkInCenter(table.Build())) 243 return contentBuilder.String() 244 } 245 246 // For review comment Frogbot creates on Scan PR 247 func GenerateReviewCommentContent(content string, writer OutputWriter) string { 248 var contentBuilder strings.Builder 249 contentBuilder.WriteString(MarkdownComment(ReviewCommentId)) 250 customCommentTitle := writer.PullRequestCommentTitle() 251 if customCommentTitle != "" { 252 WriteContent(&contentBuilder, writer.MarkAsTitle(MarkAsBold(customCommentTitle), 2)) 253 } 254 WriteContent(&contentBuilder, content, footer(writer)) 255 return contentBuilder.String() 256 } 257 258 // When can't create review comment, create a fallback comment by adding the location description to the content as a prefix 259 func GetFallbackReviewCommentContent(content string, location formats.Location, writer OutputWriter) string { 260 var contentBuilder strings.Builder 261 contentBuilder.WriteString(MarkdownComment(ReviewCommentId)) 262 WriteContent(&contentBuilder, getFallbackCommentLocationDescription(location), content, footer(writer)) 263 return contentBuilder.String() 264 } 265 266 func IsFrogbotReviewComment(content string) bool { 267 return strings.Contains(content, ReviewCommentId) 268 } 269 270 func getFallbackCommentLocationDescription(location formats.Location) string { 271 return fmt.Sprintf("%s\nat %s (line %d)", MarkAsCodeSnippet(location.Snippet), MarkAsQuote(location.File), location.StartLine) 272 } 273 274 func GetApplicabilityDescriptionTable(severity, cve, impactedDependency, finding string, writer OutputWriter) string { 275 table := NewMarkdownTable("Severity", "Impacted Dependency", "Finding", "CVE").AddRow(writer.FormattedSeverity(severity, "Applicable"), impactedDependency, finding, cve) 276 return table.Build() 277 } 278 279 func ApplicableCveReviewContent(severity, finding, fullDetails, cve, cveDetails, impactedDependency, remediation string, writer OutputWriter) string { 280 var contentBuilder strings.Builder 281 WriteContent(&contentBuilder, 282 writer.MarkAsTitle(contextualAnalysisTitle, 2), 283 writer.MarkInCenter(GetApplicabilityDescriptionTable(severity, cve, impactedDependency, finding, writer)), 284 writer.MarkAsDetails("Description", 3, fullDetails), 285 writer.MarkAsDetails("CVE details", 3, cveDetails), 286 ) 287 288 if len(remediation) > 0 { 289 WriteContent(&contentBuilder, writer.MarkAsDetails("Remediation", 3, remediation)) 290 } 291 return contentBuilder.String() 292 } 293 294 func getJasDescriptionTable(severity, finding string, writer OutputWriter) string { 295 return NewMarkdownTable("Severity", "Finding").AddRow(writer.FormattedSeverity(severity, "Applicable"), finding).Build() 296 } 297 298 func IacReviewContent(severity, finding, fullDetails string, writer OutputWriter) string { 299 var contentBuilder strings.Builder 300 WriteContent(&contentBuilder, 301 writer.MarkAsTitle(iacTitle, 2), 302 writer.MarkInCenter(getJasDescriptionTable(severity, finding, writer)), 303 writer.MarkAsDetails("Full description", 3, fullDetails), 304 ) 305 return contentBuilder.String() 306 } 307 308 func SastReviewContent(severity, finding, fullDetails string, codeFlows [][]formats.Location, writer OutputWriter) string { 309 var contentBuilder strings.Builder 310 WriteContent(&contentBuilder, 311 writer.MarkAsTitle(sastTitle, 2), 312 writer.MarkInCenter(getJasDescriptionTable(severity, finding, writer)), 313 writer.MarkAsDetails("Full description", 3, fullDetails), 314 ) 315 316 if len(codeFlows) > 0 { 317 WriteContent(&contentBuilder, writer.MarkAsDetails("Code Flows", 3, sastCodeFlowsReviewContent(codeFlows, writer))) 318 } 319 return contentBuilder.String() 320 } 321 322 func sastCodeFlowsReviewContent(codeFlows [][]formats.Location, writer OutputWriter) string { 323 var contentBuilder strings.Builder 324 for _, flow := range codeFlows { 325 WriteContent(&contentBuilder, writer.MarkAsDetails("Vulnerable data flow analysis result", 4, sastDataFlowLocationsReviewContent(flow))) 326 } 327 return contentBuilder.String() 328 } 329 330 func sastDataFlowLocationsReviewContent(flow []formats.Location) string { 331 var contentBuilder strings.Builder 332 for _, location := range flow { 333 WriteContent(&contentBuilder, fmt.Sprintf("%s %s (at %s line %d)\n", "↘️", MarkAsQuote(location.Snippet), location.File, location.StartLine)) 334 } 335 return contentBuilder.String() 336 }