Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/cmd/tmc/main.go
2070 views
1
package main
2
3
import (
4
"bytes"
5
"fmt"
6
"log"
7
"os"
8
"path/filepath"
9
"reflect"
10
"regexp"
11
"sort"
12
"strings"
13
14
"github.com/projectdiscovery/goflags"
15
"github.com/projectdiscovery/gologger"
16
"github.com/projectdiscovery/gologger/levels"
17
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
18
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
19
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
20
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
22
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
23
"github.com/projectdiscovery/nuclei/v3/pkg/types"
24
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
25
"github.com/projectdiscovery/retryablehttp-go"
26
"github.com/projectdiscovery/utils/errkit"
27
"gopkg.in/yaml.v3"
28
)
29
30
const (
31
yamlIndentSpaces = 2
32
// templateman api base url
33
tmBaseUrlDefault = "https://tm.nuclei.sh"
34
)
35
36
var tmBaseUrl string
37
38
func init() {
39
tmBaseUrl = os.Getenv("TEMPLATEMAN_SERVER")
40
if tmBaseUrl == "" {
41
tmBaseUrl = tmBaseUrlDefault
42
}
43
}
44
45
// allTagsRegex is a list of all tags in nuclei templates except id, info, and -
46
var allTagsRegex []*regexp.Regexp
47
var defaultOpts = types.DefaultOptions()
48
49
func init() {
50
var tm templates.Template
51
t := reflect.TypeOf(tm)
52
for i := 0; i < t.NumField(); i++ {
53
tag := t.Field(i).Tag.Get("yaml")
54
if strings.Contains(tag, ",") {
55
tag = strings.Split(tag, ",")[0]
56
}
57
// ignore these tags
58
if tag == "id" || tag == "info" || tag == "" || tag == "-" {
59
continue
60
}
61
re := regexp.MustCompile(tag + `:\s*\n`)
62
if t.Field(i).Type.Kind() == reflect.Bool {
63
re = regexp.MustCompile(tag + `:\s*(true|false)\s*\n`)
64
}
65
allTagsRegex = append(allTagsRegex, re)
66
}
67
68
// need to set headless to true for headless templates
69
defaultOpts.Headless = true
70
defaultOpts.EnableCodeTemplates = true
71
defaultOpts.EnableSelfContainedTemplates = true
72
if err := protocolstate.Init(defaultOpts); err != nil {
73
gologger.Fatal().Msgf("Could not initialize protocol state: %s\n", err)
74
}
75
if err := protocolinit.Init(defaultOpts); err != nil {
76
gologger.Fatal().Msgf("Could not initialize protocol state: %s\n", err)
77
}
78
}
79
80
type options struct {
81
input string
82
errorLogFile string
83
lint bool
84
validate bool
85
format bool
86
enhance bool
87
maxRequest bool
88
debug bool
89
}
90
91
func main() {
92
opts := options{}
93
flagSet := goflags.NewFlagSet()
94
flagSet.SetDescription(`TemplateMan CLI is basic utility built on the TemplateMan API to standardize nuclei templates.`)
95
96
flagSet.CreateGroup("Input", "input",
97
flagSet.StringVarP(&opts.input, "input", "i", "", "Templates to annotate"),
98
)
99
100
flagSet.CreateGroup("Config", "config",
101
flagSet.BoolVarP(&opts.lint, "lint", "l", false, "lint given nuclei template"),
102
flagSet.BoolVarP(&opts.validate, "validate", "v", false, "validate given nuclei template"),
103
flagSet.BoolVarP(&opts.format, "format", "f", false, "format given nuclei template"),
104
flagSet.BoolVarP(&opts.enhance, "enhance", "e", false, "enhance given nuclei template"),
105
flagSet.BoolVarP(&opts.maxRequest, "max-request", "mr", false, "add / update max request counter"),
106
flagSet.StringVarP(&opts.errorLogFile, "error-log", "el", "", "file to write failed template update"),
107
flagSet.BoolVarP(&opts.debug, "debug", "d", false, "show debug message"),
108
)
109
110
if err := flagSet.Parse(); err != nil {
111
gologger.Fatal().Msgf("Error parsing flags: %s\n", err)
112
}
113
114
if opts.input == "" {
115
gologger.Fatal().Msg("input template path/directory is required")
116
}
117
if strings.HasPrefix(opts.input, "~/") {
118
home, err := os.UserHomeDir()
119
if err != nil {
120
log.Fatalf("Failed to read UserHomeDir: %v, provide absolute template path/directory\n", err)
121
}
122
opts.input = filepath.Join(home, (opts.input)[2:])
123
}
124
gologger.DefaultLogger.SetMaxLevel(levels.LevelInfo)
125
if opts.debug {
126
gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug)
127
}
128
if err := process(opts); err != nil {
129
gologger.Error().Msgf("could not process: %s\n", err)
130
}
131
}
132
133
func process(opts options) error {
134
tempDir, err := os.MkdirTemp("", "nuclei-nvd")
135
if err != nil {
136
return err
137
}
138
defer func() {
139
_ = os.RemoveAll(tempDir)
140
}()
141
142
var errFile *os.File
143
if opts.errorLogFile != "" {
144
errFile, err = os.OpenFile(opts.errorLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
145
if err != nil {
146
gologger.Fatal().Msgf("could not open error log file: %s\n", err)
147
}
148
defer func() {
149
_ = errFile.Close()
150
}()
151
}
152
153
templateCatalog := disk.NewCatalog(filepath.Dir(opts.input))
154
paths, err := templateCatalog.GetTemplatePath(opts.input)
155
if err != nil {
156
return err
157
}
158
for _, path := range paths {
159
data, err := os.ReadFile(path)
160
if err != nil {
161
return err
162
}
163
dataString := string(data)
164
165
if opts.maxRequest {
166
var updated bool // if max-requests is updated
167
dataString, updated, err = parseAndAddMaxRequests(templateCatalog, path, dataString)
168
if err != nil {
169
gologger.Info().Label("max-request").Msg(logErrMsg(path, err, opts.debug, errFile))
170
} else {
171
if updated {
172
gologger.Info().Label("max-request").Msgf("✅ updated template: %s\n", path)
173
}
174
// do not print if max-requests is not updated
175
}
176
}
177
178
if opts.lint {
179
lint, err := lintTemplate(dataString)
180
if err != nil {
181
gologger.Info().Label("lint").Msg(logErrMsg(path, err, opts.debug, errFile))
182
}
183
if lint {
184
gologger.Info().Label("lint").Msgf("✅ lint template: %s\n", path)
185
}
186
}
187
188
if opts.validate {
189
validate, err := validateTemplate(dataString)
190
if err != nil {
191
gologger.Info().Label("validate").Msg(logErrMsg(path, err, opts.debug, errFile))
192
}
193
if validate {
194
gologger.Info().Label("validate").Msgf("✅ validated template: %s\n", path)
195
}
196
}
197
198
if opts.format {
199
formatedTemplateData, isFormated, err := formatTemplate(dataString)
200
if err != nil {
201
gologger.Info().Label("format").Msg(logErrMsg(path, err, opts.debug, errFile))
202
} else {
203
if isFormated {
204
_ = os.WriteFile(path, []byte(formatedTemplateData), 0644)
205
dataString = formatedTemplateData
206
gologger.Info().Label("format").Msgf("✅ formated template: %s\n", path)
207
}
208
}
209
}
210
211
if opts.enhance {
212
enhancedTemplateData, isEnhanced, err := enhanceTemplate(dataString)
213
if err != nil {
214
gologger.Info().Label("enhance").Msg(logErrMsg(path, err, opts.debug, errFile))
215
continue
216
} else {
217
if isEnhanced {
218
_ = os.WriteFile(path, []byte(enhancedTemplateData), 0644)
219
gologger.Info().Label("enhance").Msgf("✅ updated template: %s\n", path)
220
}
221
}
222
}
223
}
224
return nil
225
}
226
227
func logErrMsg(path string, err error, debug bool, errFile *os.File) string {
228
msg := fmt.Sprintf("❌ template: %s\n", path)
229
if debug {
230
msg = fmt.Sprintf("❌ template: %s err: %s\n", path, err)
231
}
232
if errFile != nil {
233
_, _ = fmt.Fprintf(errFile, "❌ template: %s err: %s\n", path, err)
234
}
235
return msg
236
}
237
238
// enhanceTemplate enhances template data using templateman
239
// ref: https://github.com/projectdiscovery/templateman/blob/main/templateman-rest-api/README.md#enhance-api
240
func enhanceTemplate(data string) (string, bool, error) {
241
resp, err := retryablehttp.DefaultClient().Post(fmt.Sprintf("%s/enhance", tmBaseUrl), "application/x-yaml", strings.NewReader(data))
242
if err != nil {
243
return data, false, err
244
}
245
if resp.StatusCode != 200 {
246
return data, false, errkit.New("unexpected status code: %v", resp.Status)
247
}
248
var templateResp TemplateResp
249
if err := json.NewDecoder(resp.Body).Decode(&templateResp); err != nil {
250
return data, false, err
251
}
252
if strings.TrimSpace(templateResp.Enhanced) != "" {
253
return templateResp.Enhanced, templateResp.Enhance, nil
254
}
255
if templateResp.ValidateErrorCount > 0 {
256
if len(templateResp.ValidateError) > 0 {
257
return data, false, errkit.New(templateResp.ValidateError[0].Message+": at line %v", templateResp.ValidateError[0].Mark.Line, "tag", "validate")
258
}
259
return data, false, errkit.New("validation failed", "tag", "validate")
260
}
261
if templateResp.Error.Name != "" {
262
return data, false, errkit.New("%s", templateResp.Error.Name)
263
}
264
if strings.TrimSpace(templateResp.Enhanced) == "" && !templateResp.Lint {
265
if templateResp.LintError.Reason != "" {
266
return data, false, errkit.New(templateResp.LintError.Reason+" : at line %v", templateResp.LintError.Mark.Line, "tag", "lint")
267
}
268
return data, false, errkit.New("at line: %v", templateResp.LintError.Mark.Line, "tag", "lint")
269
}
270
return data, false, errkit.New("template enhance failed")
271
}
272
273
// formatTemplate formats template data using templateman format api
274
func formatTemplate(data string) (string, bool, error) {
275
resp, err := retryablehttp.DefaultClient().Post(fmt.Sprintf("%s/format", tmBaseUrl), "application/x-yaml", strings.NewReader(data))
276
if err != nil {
277
return data, false, err
278
}
279
if resp.StatusCode != 200 {
280
return data, false, errkit.New("unexpected status code: %v", resp.Status)
281
}
282
var templateResp TemplateResp
283
if err := json.NewDecoder(resp.Body).Decode(&templateResp); err != nil {
284
return data, false, err
285
}
286
if strings.TrimSpace(templateResp.Updated) != "" {
287
return templateResp.Updated, templateResp.Format, nil
288
}
289
if templateResp.ValidateErrorCount > 0 {
290
if len(templateResp.ValidateError) > 0 {
291
return data, false, errkit.New(templateResp.ValidateError[0].Message+": at line %v", templateResp.ValidateError[0].Mark.Line, "tag", "validate")
292
}
293
return data, false, errkit.New("validation failed", "tag", "validate")
294
}
295
if templateResp.Error.Name != "" {
296
return data, false, errkit.New("%s", templateResp.Error.Name)
297
}
298
if strings.TrimSpace(templateResp.Updated) == "" && !templateResp.Lint {
299
if templateResp.LintError.Reason != "" {
300
return data, false, errkit.New(templateResp.LintError.Reason+" : at line %v", templateResp.LintError.Mark.Line, "tag", "lint")
301
}
302
return data, false, errkit.New("at line: %v", templateResp.LintError.Mark.Line, "tag", "lint")
303
}
304
return data, false, errkit.New("template format failed")
305
}
306
307
// lintTemplate lints template data using templateman lint api
308
func lintTemplate(data string) (bool, error) {
309
resp, err := retryablehttp.DefaultClient().Post(fmt.Sprintf("%s/lint", tmBaseUrl), "application/x-yaml", strings.NewReader(data))
310
if err != nil {
311
return false, err
312
}
313
if resp.StatusCode != 200 {
314
return false, errkit.New("unexpected status code: %v", resp.Status)
315
}
316
var lintResp TemplateLintResp
317
if err := json.NewDecoder(resp.Body).Decode(&lintResp); err != nil {
318
return false, err
319
}
320
if lintResp.Lint {
321
return true, nil
322
}
323
if lintResp.LintError.Reason != "" {
324
return false, errkit.New(lintResp.LintError.Reason+" : at line %v", lintResp.LintError.Mark.Line, "tag", "lint")
325
}
326
return false, errkit.New("at line: %v", lintResp.LintError.Mark.Line, "tag", "lint")
327
}
328
329
// validateTemplate validates template data using templateman validate api
330
func validateTemplate(data string) (bool, error) {
331
resp, err := retryablehttp.DefaultClient().Post(fmt.Sprintf("%s/validate", tmBaseUrl), "application/x-yaml", strings.NewReader(data))
332
if err != nil {
333
return false, err
334
}
335
if resp.StatusCode != 200 {
336
return false, errkit.New("unexpected status code: %v", resp.Status)
337
}
338
var validateResp TemplateResp
339
if err := json.NewDecoder(resp.Body).Decode(&validateResp); err != nil {
340
return false, err
341
}
342
if validateResp.Validate {
343
return true, nil
344
}
345
if validateResp.ValidateErrorCount > 0 {
346
if len(validateResp.ValidateError) > 0 {
347
return false, errkit.New(validateResp.ValidateError[0].Message+": at line %v", validateResp.ValidateError[0].Mark.Line, "tag", "validate")
348
}
349
return false, errkit.New("validation failed", "tag", "validate")
350
}
351
if validateResp.Error.Name != "" {
352
return false, errkit.New("%s", validateResp.Error.Name)
353
}
354
return false, errkit.New("template validation failed")
355
}
356
357
// parseAndAddMaxRequests parses and adds max requests to templates
358
func parseAndAddMaxRequests(catalog catalog.Catalog, path, data string) (string, bool, error) {
359
template, err := parseTemplate(catalog, path)
360
if err != nil {
361
return data, false, err
362
}
363
if template.TotalRequests < 1 {
364
return data, false, nil
365
}
366
// Marshal the updated info block back to YAML.
367
infoBlockStart, infoBlockEnd := getInfoStartEnd(data)
368
infoBlockOrig := data[infoBlockStart:infoBlockEnd]
369
infoBlockOrig = strings.TrimRight(infoBlockOrig, "\n")
370
infoBlock := InfoBlock{}
371
err = yaml.Unmarshal([]byte(data), &infoBlock)
372
if err != nil {
373
return data, false, err
374
}
375
// if metadata is nil, create a new map
376
if infoBlock.Info.Metadata == nil {
377
infoBlock.Info.Metadata = make(map[string]interface{})
378
}
379
// do not update if it is already present and equal
380
if mr, ok := infoBlock.Info.Metadata["max-request"]; ok && mr.(int) == template.TotalRequests {
381
return data, false, nil
382
}
383
infoBlock.Info.Metadata["max-request"] = template.TotalRequests
384
385
var newInfoBlock bytes.Buffer
386
yamlEncoder := yaml.NewEncoder(&newInfoBlock)
387
yamlEncoder.SetIndent(yamlIndentSpaces)
388
err = yamlEncoder.Encode(infoBlock)
389
if err != nil {
390
return data, false, err
391
}
392
newInfoBlockData := strings.TrimSuffix(newInfoBlock.String(), "\n")
393
// replace old info block with new info block
394
newTemplate := strings.ReplaceAll(data, infoBlockOrig, newInfoBlockData)
395
err = os.WriteFile(path, []byte(newTemplate), 0644)
396
if err == nil {
397
return newTemplate, true, nil
398
}
399
return newTemplate, false, err
400
}
401
402
// parseTemplate parses a template and returns the template object
403
func parseTemplate(catalog catalog.Catalog, templatePath string) (*templates.Template, error) {
404
executorOpts := &protocols.ExecutorOptions{
405
Catalog: catalog,
406
Options: defaultOpts,
407
}
408
reader, err := executorOpts.Catalog.OpenFile(templatePath)
409
if err != nil {
410
return nil, err
411
}
412
template, err := templates.ParseTemplateFromReader(reader, nil, executorOpts)
413
if err != nil {
414
return nil, err
415
}
416
return template, nil
417
}
418
419
// find the start and end of the info block
420
func getInfoStartEnd(data string) (int, int) {
421
info := strings.Index(data, "info:")
422
var indices []int
423
for _, re := range allTagsRegex {
424
// find the first occurrence of the label
425
match := re.FindStringIndex(data)
426
if match != nil {
427
indices = append(indices, match[0])
428
}
429
}
430
// find the first one after info block
431
sort.Ints(indices)
432
return info, indices[0] - 1
433
}
434
435