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