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  }