package review import ( "fmt" "regexp" "strings" ) // FormatMarkdown formats a ReviewResult into the markdown body for a Gitea review. func FormatMarkdown(result *ReviewResult, reviewerName string) string { return FormatMarkdownWithDisplay(result, reviewerName, reviewerName) } // GiteaEvent converts the verdict to the Gitea API event string. func GiteaEvent(verdict string) string { switch verdict { case "APPROVE": return "APPROVED" case "REQUEST_CHANGES": return "REQUEST_CHANGES" default: return "COMMENT" } } // markdownSpecialChars matches characters that have special meaning in Markdown. // We escape these to prevent untrusted input from breaking formatting. // Uses a quoted string since raw strings can't contain backticks. var markdownSpecialChars = regexp.MustCompile("([\\\\*_`\\[\\]()#<>|~])") // sanitizeMarkdownText escapes special Markdown characters in untrusted text. // This prevents markdown injection attacks where a malicious display name could // break formatting, inject links, or create unexpected rendering. func sanitizeMarkdownText(s string) string { // First, remove any control characters and null bytes cleaned := strings.Map(func(r rune) rune { if r < 32 && r != '\t' && r != '\n' { return -1 // drop the character } return r }, s) // Escape special Markdown characters by prepending backslash return markdownSpecialChars.ReplaceAllString(cleaned, `\$1`) } // FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name. // displayName is sanitized to prevent Markdown injection from untrusted remote persona metadata. // sentinelName is used for the cleanup sentinel comment (machine-readable, not rendered). // If displayName is empty, sentinelName is used for both. func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string { var sb strings.Builder // Use display name for header, or fall back to sentinel name headerName := displayName if headerName == "" { headerName = sentinelName } if headerName != "" { // Sanitize the header name to prevent Markdown injection title := CapitalizeFirst(sanitizeMarkdownText(headerName)) sb.WriteString(fmt.Sprintf("# %s Review\n\n", title)) } sb.WriteString("## Summary\n\n") sb.WriteString(result.Summary) sb.WriteString("\n\n") if len(result.Findings) > 0 { sb.WriteString("## Findings\n\n") sb.WriteString("| # | Severity | File | Line | Finding |\n") sb.WriteString("|---|----------|------|------|--------|\n") for i, f := range result.Findings { sb.WriteString(fmt.Sprintf("| %d | [%s] | `%s` | %d | %s |\n", i+1, f.Severity, f.File, f.Line, f.Finding)) } sb.WriteString("\n") } sb.WriteString("## Recommendation\n\n") sb.WriteString(fmt.Sprintf("**%s** — %s\n", result.Verdict, result.Recommendation)) if sentinelName != "" { // Sanitize headerName for the footer as well sb.WriteString(fmt.Sprintf("\n---\n*Review by %s*\n", sanitizeMarkdownText(headerName))) // Hidden sentinel for identifying this bot's reviews during cleanup sb.WriteString(fmt.Sprintf("\n\n", sentinelName)) } return sb.String() }