Path: blob/dev/pkg/reporting/exporters/pdf/pdf.go
4538 views
package pdf12import (3"fmt"4"os"5"path/filepath"6"strings"7"sync"8"time"910fpdf "github.com/go-pdf/fpdf"11"github.com/pkg/errors"12"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"13"github.com/projectdiscovery/nuclei/v3/pkg/output"14)1516const (17defaultFile = "nuclei-report.pdf"18maxRawLen = 409619)2021// Options contains the configuration options for PDF exporter client.22type Options struct {23// File is the file to export found results to in PDF format.24File string `yaml:"file"`25// OmitRaw omits request/response from the report.26OmitRaw bool `yaml:"omit-raw"`27}2829// Exporter is an exporter for nuclei PDF output format.30type Exporter struct {31options *Options32mu sync.Mutex33results []output.ResultEvent34}3536// New creates a new PDF exporter integration client based on options.37func New(options *Options) (*Exporter, error) {38opts := &Options{}39if options != nil {40*opts = *options41}42if opts.File == "" {43opts.File = defaultFile44}45return &Exporter{46options: opts,47results: make([]output.ResultEvent, 0),48}, nil49}5051// Export appends a result event to the report buffer.52func (e *Exporter) Export(event *output.ResultEvent) error {53if event == nil {54return nil55}56e.mu.Lock()57defer e.mu.Unlock()58row := *event59if e.options.OmitRaw {60row.Request = ""61row.Response = ""62} else {63if len(row.Request) > maxRawLen {64row.Request = row.Request[:maxRawLen] + "\n[truncated]"65}66if len(row.Response) > maxRawLen {67row.Response = row.Response[:maxRawLen] + "\n[truncated]"68}69}70e.results = append(e.results, row)71return nil72}7374// Close generates the PDF report and writes it to disk.75// Returns nil without creating a file when there are no results.76func (e *Exporter) Close() error {77e.mu.Lock()78snapshot := make([]output.ResultEvent, len(e.results))79copy(snapshot, e.results)80opts := *e.options81e.mu.Unlock()8283if len(snapshot) == 0 {84return nil85}86if dir := filepath.Dir(opts.File); dir != "." && dir != "" {87if err := os.MkdirAll(dir, 0755); err != nil {88return errors.Wrap(err, "could not create directory for PDF report")89}90}91return generate(&opts, snapshot)92}9394func generate(opts *Options, results []output.ResultEvent) error {95doc := fpdf.New("P", "mm", "A4", "")96doc.SetMargins(12, 15, 12)97doc.SetAutoPageBreak(true, 18)98renderHeader(doc)99renderSummary(doc, results)100renderFindings(doc, results)101if err := doc.OutputFileAndClose(opts.File); err != nil {102return errors.Wrap(err, "could not write PDF report")103}104return nil105}106107func renderHeader(doc *fpdf.Fpdf) {108doc.AddPage()109doc.SetFont("Helvetica", "B", 18)110doc.SetTextColor(30, 30, 30)111doc.CellFormat(0, 10, "Nuclei Vulnerability Scan Report", "", 1, "C", false, 0, "")112doc.SetFont("Helvetica", "", 9)113doc.SetTextColor(100, 100, 100)114doc.CellFormat(0, 5, "Generated: "+time.Now().UTC().Format("2006-01-02 15:04:05 UTC"), "", 1, "C", false, 0, "")115doc.CellFormat(0, 5, "Engine: Nuclei "+config.Version, "", 1, "C", false, 0, "")116doc.Ln(6)117}118119type rgb struct{ r, g, b int }120121var sevColors = map[string]rgb{122"critical": {128, 0, 128},123"high": {200, 0, 0},124"medium": {200, 100, 0},125"low": {170, 140, 0},126"info": {0, 100, 180},127"unknown": {100, 100, 100},128}129130var sevOrder = []string{"critical", "high", "medium", "low", "info", "unknown"}131132func colorFor(sev string) (int, int, int) {133if c, ok := sevColors[strings.ToLower(sev)]; ok {134return c.r, c.g, c.b135}136return 100, 100, 100137}138139func capitalize(s string) string {140if s == "" {141return s142}143return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])144}145146func renderSummary(doc *fpdf.Fpdf, results []output.ResultEvent) {147counts := make(map[string]int, len(sevOrder))148for _, r := range results {149sev := strings.ToLower(r.Info.SeverityHolder.Severity.String())150if _, ok := sevColors[sev]; ok {151counts[sev]++152} else {153counts["unknown"]++154}155}156doc.SetFont("Helvetica", "B", 11)157doc.SetTextColor(30, 30, 30)158doc.CellFormat(0, 7, fmt.Sprintf("Summary - %d finding(s)", len(results)), "", 1, "", false, 0, "")159doc.Ln(1)160colW := 28.0161doc.SetFont("Helvetica", "B", 9)162for _, sev := range sevOrder {163r, g, b := colorFor(sev)164doc.SetFillColor(r, g, b)165doc.SetTextColor(255, 255, 255)166doc.CellFormat(colW, 6, capitalize(sev), "1", 0, "C", true, 0, "")167}168doc.Ln(-1)169doc.SetFont("Helvetica", "", 9)170doc.SetFillColor(245, 245, 245)171doc.SetTextColor(30, 30, 30)172for _, sev := range sevOrder {173doc.CellFormat(colW, 6, fmt.Sprintf("%d", counts[sev]), "1", 0, "C", true, 0, "")174}175doc.Ln(10)176}177178func renderFindings(doc *fpdf.Fpdf, results []output.ResultEvent) {179doc.SetFont("Helvetica", "B", 11)180doc.SetTextColor(30, 30, 30)181doc.CellFormat(0, 7, "Findings", "", 1, "", false, 0, "")182doc.Ln(1)183for i, r := range results {184sev := strings.ToLower(r.Info.SeverityHolder.Severity.String())185cr, cg, cb := colorFor(sev)186doc.SetFont("Helvetica", "B", 10)187doc.SetFillColor(cr, cg, cb)188doc.SetTextColor(255, 255, 255)189doc.CellFormat(0, 7, safeStr(fmt.Sprintf("[%s] %s", strings.ToUpper(sev), r.Info.Name)), "0", 1, "", true, 0, "")190doc.SetFont("Helvetica", "", 9)191doc.SetTextColor(30, 30, 30)192doc.CellFormat(30, 5, "Host:", "0", 0, "", false, 0, "")193doc.CellFormat(0, 5, safeStr(r.Host), "0", 1, "", false, 0, "")194doc.CellFormat(30, 5, "Template:", "0", 0, "", false, 0, "")195doc.CellFormat(0, 5, safeStr(r.TemplateID), "0", 1, "", false, 0, "")196if r.Info.Description != "" {197doc.SetFont("Helvetica", "I", 8)198doc.SetTextColor(60, 60, 60)199doc.MultiCell(0, 4, safeStr(r.Info.Description), "", "", false)200}201if r.Request != "" {202renderCodeBlock(doc, "Request", r.Request)203}204if r.Response != "" {205renderCodeBlock(doc, "Response", r.Response)206}207if i < len(results)-1 {208doc.Ln(3)209doc.SetDrawColor(200, 200, 200)210doc.Line(12, doc.GetY(), 198, doc.GetY())211doc.Ln(3)212}213}214}215216func renderCodeBlock(doc *fpdf.Fpdf, label, content string) {217doc.SetFont("Helvetica", "B", 8)218doc.SetTextColor(60, 60, 60)219doc.CellFormat(0, 5, label+":", "0", 1, "", false, 0, "")220doc.SetFont("Courier", "", 7)221doc.SetFillColor(240, 240, 240)222doc.SetTextColor(40, 40, 40)223doc.MultiCell(0, 4, safeStr(content), "1", "", true)224}225226// safeStr replaces characters outside ISO-8859-1 with '?' for fpdf compatibility.227func safeStr(s string) string {228out := make([]byte, 0, len(s))229for _, r := range s {230if r > 255 {231out = append(out, '?')232} else {233out = append(out, byte(r))234}235}236return string(out)237}238239240