Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/installer/template.go
2070 views
1
package installer
2
3
import (
4
"bytes"
5
"context"
6
"crypto/md5"
7
"fmt"
8
"io"
9
"io/fs"
10
"os"
11
"path/filepath"
12
"strconv"
13
"strings"
14
15
"github.com/charmbracelet/glamour"
16
"github.com/olekukonko/tablewriter"
17
"github.com/projectdiscovery/gologger"
18
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
19
"github.com/projectdiscovery/nuclei/v3/pkg/external/customtemplates"
20
"github.com/projectdiscovery/utils/errkit"
21
fileutil "github.com/projectdiscovery/utils/file"
22
stringsutil "github.com/projectdiscovery/utils/strings"
23
updateutils "github.com/projectdiscovery/utils/update"
24
)
25
26
const (
27
checkSumFilePerm = 0644
28
)
29
30
var (
31
HideProgressBar = true
32
HideUpdateChangesTable = false
33
HideReleaseNotes = true
34
)
35
36
// TemplateUpdateResults contains the results of template update
37
type templateUpdateResults struct {
38
additions []string
39
deletions []string
40
modifications []string
41
totalCount int
42
}
43
44
// String returns markdown table of template update results
45
func (t *templateUpdateResults) String() string {
46
var buff bytes.Buffer
47
data := [][]string{
48
{
49
strconv.Itoa(t.totalCount),
50
strconv.Itoa(len(t.additions)),
51
strconv.Itoa(len(t.modifications)),
52
strconv.Itoa(len(t.deletions)),
53
},
54
}
55
table := tablewriter.NewWriter(&buff)
56
table.Header([]string{"Total", "Added", "Modified", "Removed"})
57
for _, v := range data {
58
_ = table.Append(v)
59
}
60
_ = table.Render()
61
defer func() {
62
_ = table.Close()
63
}()
64
return buff.String()
65
}
66
67
// TemplateManager is a manager for templates.
68
// It downloads / updates / installs templates.
69
type TemplateManager struct {
70
CustomTemplates *customtemplates.CustomTemplatesManager // optional if given tries to download custom templates
71
DisablePublicTemplates bool // if true,
72
// public templates are not downloaded from the GitHub nuclei-templates repository
73
}
74
75
// FreshInstallIfNotExists installs templates if they are not already installed
76
// if templates directory already exists, it does nothing
77
func (t *TemplateManager) FreshInstallIfNotExists() error {
78
if fileutil.FolderExists(config.DefaultConfig.TemplatesDirectory) {
79
return nil
80
}
81
gologger.Info().Msgf("nuclei-templates are not installed, installing...")
82
if err := t.installTemplatesAt(config.DefaultConfig.TemplatesDirectory); err != nil {
83
return errkit.Wrapf(err, "failed to install templates at %s", config.DefaultConfig.TemplatesDirectory)
84
}
85
if t.CustomTemplates != nil {
86
t.CustomTemplates.Download(context.TODO())
87
}
88
return nil
89
}
90
91
// UpdateIfOutdated updates templates if they are outdated
92
func (t *TemplateManager) UpdateIfOutdated() error {
93
// if the templates folder does not exist, it's a fresh installation and do not update
94
if !fileutil.FolderExists(config.DefaultConfig.TemplatesDirectory) {
95
return t.FreshInstallIfNotExists()
96
}
97
98
needsUpdate := config.DefaultConfig.NeedsTemplateUpdate()
99
100
// NOTE(dwisiswant0): if PDTM API data is not available
101
// (LatestNucleiTemplatesVersion is empty) but we have a current template
102
// version, so we MUST verify against GitHub directly.
103
if !needsUpdate && config.DefaultConfig.LatestNucleiTemplatesVersion == "" && config.DefaultConfig.TemplateVersion != "" {
104
ghrd, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
105
if err == nil {
106
latestVersion := ghrd.Latest.GetTagName()
107
if config.IsOutdatedVersion(config.DefaultConfig.TemplateVersion, latestVersion) {
108
needsUpdate = true
109
gologger.Debug().Msgf("PDTM API unavailable, verified update needed via GitHub API: %s -> %s", config.DefaultConfig.TemplateVersion, latestVersion)
110
}
111
}
112
}
113
114
if needsUpdate {
115
return t.updateTemplatesAt(config.DefaultConfig.TemplatesDirectory)
116
}
117
return nil
118
}
119
120
// installTemplatesAt installs templates at given directory
121
func (t *TemplateManager) installTemplatesAt(dir string) error {
122
if !fileutil.FolderExists(dir) {
123
if err := fileutil.CreateFolder(dir); err != nil {
124
return errkit.Wrapf(err, "failed to create directory at %s", dir)
125
}
126
}
127
if t.DisablePublicTemplates {
128
gologger.Info().Msgf("Skipping installation of public nuclei-templates")
129
return nil
130
}
131
ghrd, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
132
if err != nil {
133
return errkit.Wrapf(err, "failed to install templates at %s", dir)
134
}
135
136
// write templates to disk
137
if err := t.writeTemplatesToDisk(ghrd, dir); err != nil {
138
return errkit.Wrapf(err, "failed to write templates to disk at %s", dir)
139
}
140
gologger.Info().Msgf("Successfully installed nuclei-templates at %s", dir)
141
return nil
142
}
143
144
// updateTemplatesAt updates templates at given directory
145
func (t *TemplateManager) updateTemplatesAt(dir string) error {
146
if t.DisablePublicTemplates {
147
gologger.Info().Msgf("Skipping update of public nuclei-templates")
148
return nil
149
}
150
// firstly, read checksums from .checksum file these are used to generate stats
151
oldchecksums, err := t.getChecksumFromDir(dir)
152
if err != nil {
153
// if something went wrong, overwrite all files
154
oldchecksums = make(map[string]string)
155
}
156
157
ghrd, err := updateutils.NewghReleaseDownloader(config.OfficialNucleiTemplatesRepoName)
158
if err != nil {
159
return errkit.Wrapf(err, "failed to install templates at %s", dir)
160
}
161
162
latestVersion := ghrd.Latest.GetTagName()
163
currentVersion := config.DefaultConfig.TemplateVersion
164
165
if config.IsOutdatedVersion(currentVersion, latestVersion) {
166
gologger.Info().Msgf("Your current nuclei-templates %s are outdated. Latest is %s\n", currentVersion, latestVersion)
167
} else {
168
gologger.Debug().Msgf("Updating nuclei-templates from %s to %s (forced update)\n", currentVersion, latestVersion)
169
}
170
171
// write templates to disk
172
if err := t.writeTemplatesToDisk(ghrd, dir); err != nil {
173
return err
174
}
175
176
// get checksums from new templates
177
newchecksums, err := t.getChecksumFromDir(dir)
178
if err != nil {
179
// unlikely this case will happen
180
return errkit.Wrapf(err, "failed to get checksums from %s after update", dir)
181
}
182
183
// summarize all changes
184
results := t.summarizeChanges(oldchecksums, newchecksums)
185
186
// remove deleted templates
187
for _, deletion := range results.deletions {
188
if err := os.Remove(deletion); err != nil && !os.IsNotExist(err) {
189
gologger.Warning().Msgf("failed to remove deleted template %s: %s", deletion, err)
190
}
191
}
192
193
// print summary
194
if results.totalCount > 0 {
195
gologger.Info().Msgf("Successfully updated nuclei-templates (%v) to %s. GoodLuck!", ghrd.Latest.GetTagName(), dir)
196
if !HideUpdateChangesTable {
197
// print summary table
198
gologger.Print().Msgf("\nNuclei Templates %s Changelog\n", ghrd.Latest.GetTagName())
199
gologger.DefaultLogger.Print().Msg(results.String())
200
}
201
} else {
202
gologger.Info().Msgf("Successfully updated nuclei-templates (%v) to %s. GoodLuck!", ghrd.Latest.GetTagName(), dir)
203
}
204
return nil
205
}
206
207
// summarizeChanges summarizes changes between old and new checksums
208
func (t *TemplateManager) summarizeChanges(old, new map[string]string) *templateUpdateResults {
209
results := &templateUpdateResults{}
210
for k, v := range new {
211
if oldv, ok := old[k]; ok {
212
if oldv != v {
213
results.modifications = append(results.modifications, k)
214
}
215
} else {
216
results.additions = append(results.additions, k)
217
}
218
}
219
for k := range old {
220
if _, ok := new[k]; !ok {
221
results.deletions = append(results.deletions, k)
222
}
223
}
224
results.totalCount = len(results.additions) + len(results.deletions) + len(results.modifications)
225
return results
226
}
227
228
// getAbsoluteFilePath returns an absolute path where a file should be written based on given uri(i.e., files in zip)
229
// if a returned path is empty, it means that file should not be written and skipped
230
func (t *TemplateManager) getAbsoluteFilePath(templateDir, uri string, f fs.FileInfo) string {
231
// overwrite .nuclei-ignore every time nuclei-templates are downloaded
232
if f.Name() == config.NucleiIgnoreFileName {
233
return config.DefaultConfig.GetIgnoreFilePath()
234
}
235
// skip all meta files
236
if !strings.EqualFold(f.Name(), config.NewTemplateAdditionsFileName) {
237
if strings.TrimSpace(f.Name()) == "" || strings.HasPrefix(f.Name(), ".") || strings.EqualFold(f.Name(), "README.md") {
238
return ""
239
}
240
}
241
242
// get root or leftmost directory name from path
243
// this is in format `projectdiscovery-nuclei-templates-commithash`
244
245
index := strings.Index(uri, "/")
246
if index == -1 {
247
// zip files does not have directory at all , in this case log error but continue
248
gologger.Warning().Msgf("failed to get directory name from uri: %s", uri)
249
return filepath.Join(templateDir, uri)
250
}
251
// separator is also included in rootDir
252
rootDirectory := uri[:index+1]
253
relPath := strings.TrimPrefix(uri, rootDirectory)
254
255
// if it is a github meta directory skip it
256
if stringsutil.HasPrefixAny(relPath, ".github", ".git") {
257
return ""
258
}
259
260
newPath := filepath.Clean(filepath.Join(templateDir, relPath))
261
262
if !strings.HasPrefix(newPath, templateDir) {
263
// we don't allow LFI
264
return ""
265
}
266
267
if newPath == templateDir || newPath == templateDir+string(os.PathSeparator) {
268
// skip writing the folder itself since it already exists
269
return ""
270
}
271
272
if relPath != "" && f.IsDir() {
273
// if uri is a directory, create it
274
if err := fileutil.CreateFolder(newPath); err != nil {
275
gologger.Warning().Msgf("uri %v: got %s while installing templates", uri, err)
276
}
277
return ""
278
}
279
return newPath
280
}
281
282
// writeChecksumFileInDir is actual method responsible for writing all templates to directory
283
func (t *TemplateManager) writeTemplatesToDisk(ghrd *updateutils.GHReleaseDownloader, dir string) error {
284
localTemplatesIndex, err := config.GetNucleiTemplatesIndex()
285
if err != nil {
286
gologger.Warning().Msgf("failed to get local nuclei-templates index: %s", err)
287
if localTemplatesIndex == nil {
288
localTemplatesIndex = map[string]string{} // no-op
289
}
290
}
291
292
callbackFunc := func(uri string, f fs.FileInfo, r io.Reader) error {
293
writePath := t.getAbsoluteFilePath(dir, uri, f)
294
if writePath == "" {
295
// skip writing file
296
return nil
297
}
298
299
bin, err := io.ReadAll(r)
300
if err != nil {
301
// if error occurs, iteration also stops
302
return errkit.Wrapf(err, "failed to read file %s", uri)
303
}
304
// TODO: It might be better to just download index file from nuclei templates repo
305
// instead of creating it from scratch
306
id, _ := config.GetTemplateIDFromReader(bytes.NewReader(bin), uri)
307
if id != "" {
308
// based on template id, check if we are updating a path of official nuclei template
309
if oldPath, ok := localTemplatesIndex[id]; ok {
310
if oldPath != writePath {
311
// write new template at a new path and delete old template
312
if err := os.WriteFile(writePath, bin, f.Mode()); err != nil {
313
return errkit.Wrapf(err, "failed to write file %s", uri)
314
}
315
// after successful write, remove old template
316
if err := os.Remove(oldPath); err != nil {
317
gologger.Warning().Msgf("failed to remove old template %s: %s", oldPath, err)
318
}
319
return nil
320
}
321
}
322
}
323
// no change in template Path of official templates
324
return os.WriteFile(writePath, bin, f.Mode())
325
}
326
err = ghrd.DownloadSourceWithCallback(!HideProgressBar, callbackFunc)
327
if err != nil {
328
return errkit.Wrap(err, "failed to download templates")
329
}
330
331
if err := config.DefaultConfig.WriteTemplatesConfig(); err != nil {
332
return errkit.Wrap(err, "failed to write templates config")
333
}
334
// update ignore hash after writing new templates
335
if err := config.DefaultConfig.UpdateNucleiIgnoreHash(); err != nil {
336
return errkit.Wrap(err, "failed to update nuclei ignore hash")
337
}
338
339
// update templates version in config file
340
if err := config.DefaultConfig.SetTemplatesVersion(ghrd.Latest.GetTagName()); err != nil {
341
return errkit.Wrap(err, "failed to update templates version")
342
}
343
344
PurgeEmptyDirectories(dir)
345
346
// generate index of all templates
347
_ = os.Remove(config.DefaultConfig.GetTemplateIndexFilePath())
348
349
index, err := config.GetNucleiTemplatesIndex()
350
if err != nil {
351
return errkit.Wrap(err, "failed to get nuclei templates index")
352
}
353
354
if err = config.DefaultConfig.WriteTemplatesIndex(index); err != nil {
355
return errkit.Wrap(err, "failed to write nuclei templates index")
356
}
357
358
if !HideReleaseNotes {
359
output := ghrd.Latest.GetBody()
360
// adjust colors for both dark / light terminal themes
361
r, err := glamour.NewTermRenderer(glamour.WithAutoStyle())
362
if err != nil {
363
gologger.Error().Msgf("markdown rendering not supported: %v", err)
364
}
365
if rendered, err := r.Render(output); err == nil {
366
output = rendered
367
} else {
368
gologger.Error().Msg(err.Error())
369
}
370
gologger.Print().Msgf("\n%v\n\n", output)
371
}
372
373
// after installation, create and write checksums to .checksum file
374
return t.writeChecksumFileInDir(dir)
375
}
376
377
// getChecksumFromDir returns a map containing checksums (md5 hash) of all yaml files (with .yaml extension)
378
// if .checksum file does not exist, checksums are calculated and returned
379
func (t *TemplateManager) getChecksumFromDir(dir string) (map[string]string, error) {
380
checksumFilePath := config.DefaultConfig.GetChecksumFilePath()
381
if fileutil.FileExists(checksumFilePath) {
382
checksums, err := os.ReadFile(checksumFilePath)
383
if err == nil {
384
allChecksums := make(map[string]string)
385
for _, v := range strings.Split(string(checksums), ";") {
386
v = strings.TrimSpace(v)
387
tmparr := strings.Split(v, ",")
388
if len(tmparr) != 2 {
389
continue
390
}
391
allChecksums[tmparr[0]] = tmparr[1]
392
}
393
return allChecksums, nil
394
}
395
}
396
return t.calculateChecksumMap(dir)
397
}
398
399
// writeChecksumFileInDir creates checksums of all yaml files in given directory
400
// and writes them to a file named .checksum
401
func (t *TemplateManager) writeChecksumFileInDir(dir string) error {
402
checksumMap, err := t.calculateChecksumMap(dir)
403
if err != nil {
404
return err
405
}
406
var buff bytes.Buffer
407
for k, v := range checksumMap {
408
buff.WriteString(k)
409
buff.WriteString(",")
410
buff.WriteString(v)
411
buff.WriteString(";")
412
}
413
return os.WriteFile(config.DefaultConfig.GetChecksumFilePath(), buff.Bytes(), checkSumFilePerm)
414
}
415
416
// getChecksumMap returns a map containing checksums (md5 hash) of all yaml files (with .yaml extension)
417
func (t *TemplateManager) calculateChecksumMap(dir string) (map[string]string, error) {
418
// getchecksumMap walks given directory `dir` and returns a map containing
419
// checksums (md5 hash) of all yaml files (with .yaml extension) and the
420
// format is map[filePath]checksum
421
checksumMap := map[string]string{}
422
423
getChecksum := func(filepath string) (string, error) {
424
// return md5 hash of the file
425
bin, err := os.ReadFile(filepath)
426
if err != nil {
427
return "", err
428
}
429
return fmt.Sprintf("%x", md5.Sum(bin)), nil
430
}
431
432
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
433
if err != nil {
434
return err
435
}
436
// skip checksums of custom templates i.e github and s3
437
if stringsutil.HasPrefixAny(path, config.DefaultConfig.GetAllCustomTemplateDirs()...) {
438
return nil
439
}
440
441
// current implementations calculates checksums of all files (including .yaml,.txt,.md,.json etc)
442
if !d.IsDir() {
443
checksum, err := getChecksum(path)
444
if err != nil {
445
return err
446
}
447
checksumMap[path] = checksum
448
}
449
return nil
450
})
451
return checksumMap, errkit.Wrap(err, "failed to calculate checksums of templates")
452
}
453
454