Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/templates/compile.go
2846 views
1
package templates
2
3
import (
4
"fmt"
5
"io"
6
"reflect"
7
"sync"
8
"sync/atomic"
9
10
"github.com/logrusorgru/aurora"
11
"github.com/pkg/errors"
12
"gopkg.in/yaml.v2"
13
14
"github.com/projectdiscovery/gologger"
15
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
16
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
17
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
18
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
19
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
20
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/offlinehttp"
23
"github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
24
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec"
25
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
26
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
27
"github.com/projectdiscovery/utils/errkit"
28
stringsutil "github.com/projectdiscovery/utils/strings"
29
)
30
31
var (
32
ErrCreateTemplateExecutor = errors.New("cannot create template executer")
33
ErrIncompatibleWithOfflineMatching = errors.New("template can't be used for offline matching")
34
// track how many templates are verified and by which signer
35
SignatureStats = map[string]*atomic.Uint64{}
36
)
37
38
const (
39
Unsigned = "unsigned"
40
)
41
42
func init() {
43
for _, verifier := range signer.DefaultTemplateVerifiers {
44
SignatureStats[verifier.Identifier()] = &atomic.Uint64{}
45
}
46
SignatureStats[Unsigned] = &atomic.Uint64{}
47
}
48
49
// updateRequestOptions updates options for all request types in a template
50
func updateRequestOptions(template *Template) {
51
for i, r := range template.RequestsDNS {
52
rCopy := *r
53
rCopy.UpdateOptions(template.Options)
54
template.RequestsDNS[i] = &rCopy
55
}
56
for i, r := range template.RequestsHTTP {
57
rCopy := *r
58
rCopy.UpdateOptions(template.Options)
59
template.RequestsHTTP[i] = &rCopy
60
}
61
for i, r := range template.RequestsCode {
62
rCopy := *r
63
rCopy.UpdateOptions(template.Options)
64
template.RequestsCode[i] = &rCopy
65
}
66
for i, r := range template.RequestsFile {
67
rCopy := *r
68
rCopy.UpdateOptions(template.Options)
69
template.RequestsFile[i] = &rCopy
70
}
71
for i, r := range template.RequestsHeadless {
72
rCopy := *r
73
rCopy.UpdateOptions(template.Options)
74
template.RequestsHeadless[i] = &rCopy
75
}
76
for i, r := range template.RequestsNetwork {
77
rCopy := *r
78
rCopy.UpdateOptions(template.Options)
79
template.RequestsNetwork[i] = &rCopy
80
}
81
for i, r := range template.RequestsJavascript {
82
rCopy := *r
83
rCopy.UpdateOptions(template.Options)
84
template.RequestsJavascript[i] = &rCopy
85
}
86
for i, r := range template.RequestsSSL {
87
rCopy := *r
88
rCopy.UpdateOptions(template.Options)
89
template.RequestsSSL[i] = &rCopy
90
}
91
for i, r := range template.RequestsWHOIS {
92
rCopy := *r
93
rCopy.UpdateOptions(template.Options)
94
template.RequestsWHOIS[i] = &rCopy
95
}
96
for i, r := range template.RequestsWebsocket {
97
rCopy := *r
98
rCopy.UpdateOptions(template.Options)
99
template.RequestsWebsocket[i] = &rCopy
100
}
101
}
102
103
// parseFromSource parses a template from source with caching support
104
func parseFromSource(filePath string, preprocessor Preprocessor, options *protocols.ExecutorOptions, parser *Parser) (*Template, error) {
105
reader, err := utils.ReaderFromPathOrURL(filePath, options.Catalog)
106
if err != nil {
107
return nil, err
108
}
109
110
defer func() {
111
_ = reader.Close()
112
}()
113
114
options = options.Copy()
115
options.TemplatePath = filePath
116
117
template, err := ParseTemplateFromReader(reader, preprocessor, options)
118
if err != nil {
119
return nil, err
120
}
121
122
if template.isGlobalMatchersEnabled() {
123
item := &globalmatchers.Item{
124
TemplateID: template.ID,
125
TemplatePath: filePath,
126
TemplateInfo: template.Info,
127
}
128
129
for _, request := range template.RequestsHTTP {
130
item.Operators = append(item.Operators, request.CompiledOperators)
131
}
132
133
options.GlobalMatchers.AddOperator(item)
134
135
return nil, nil
136
}
137
138
// Compile the workflow request
139
if len(template.Workflows) > 0 {
140
compiled := &template.Workflow
141
142
compileWorkflow(filePath, preprocessor, options, compiled, options.WorkflowLoader)
143
template.CompiledWorkflow = compiled
144
template.CompiledWorkflow.Options = options
145
}
146
147
template.Path = filePath
148
if !options.DoNotCache {
149
parser.compiledTemplatesCache.StoreWithoutRaw(filePath, template, err)
150
}
151
152
return template, nil
153
}
154
155
// getParser returns a cached parser instance
156
func getParser(options *protocols.ExecutorOptions) *Parser {
157
parser, ok := options.Parser.(*Parser)
158
if !ok || parser == nil {
159
panic("invalid parser")
160
}
161
162
return parser
163
}
164
165
// Parse parses a yaml request template file
166
// TODO make sure reading from the disk the template parsing happens once: see parsers.ParseTemplate vs templates.Parse
167
func Parse(filePath string, preprocessor Preprocessor, options *protocols.ExecutorOptions) (*Template, error) {
168
parser := getParser(options)
169
170
if !options.DoNotCache {
171
if value, _, _ := parser.compiledTemplatesCache.Has(filePath); value != nil {
172
// Copy the template, apply new options, and recompile requests
173
tplCopy := *value
174
newBase := options.Copy()
175
newBase.TemplateID = tplCopy.Options.TemplateID
176
newBase.TemplatePath = tplCopy.Options.TemplatePath
177
newBase.TemplateInfo = tplCopy.Options.TemplateInfo
178
newBase.TemplateVerifier = tplCopy.Options.TemplateVerifier
179
newBase.RawTemplate = tplCopy.Options.RawTemplate
180
181
if tplCopy.Options.Variables.Len() > 0 {
182
newBase.Variables = tplCopy.Options.Variables
183
}
184
185
if len(tplCopy.Options.Constants) > 0 {
186
newBase.Constants = tplCopy.Options.Constants
187
}
188
189
tplCopy.Options = newBase
190
tplCopy.Options.ApplyNewEngineOptions(options)
191
192
if tplCopy.CompiledWorkflow != nil {
193
tplCopy.CompiledWorkflow.Options.ApplyNewEngineOptions(options)
194
for _, w := range tplCopy.CompiledWorkflow.Workflows {
195
for _, ex := range w.Executers {
196
ex.Options.ApplyNewEngineOptions(options)
197
}
198
}
199
}
200
201
// Update options for all request types
202
updateRequestOptions(&tplCopy)
203
template := &tplCopy
204
205
if template.isGlobalMatchersEnabled() {
206
item := &globalmatchers.Item{
207
TemplateID: template.ID,
208
TemplatePath: filePath,
209
TemplateInfo: template.Info,
210
}
211
212
for _, request := range template.RequestsHTTP {
213
item.Operators = append(item.Operators, request.CompiledOperators)
214
}
215
216
options.GlobalMatchers.AddOperator(item)
217
218
return nil, nil
219
}
220
221
// Compile the workflow request
222
if len(template.Workflows) > 0 {
223
compiled := &template.Workflow
224
compileWorkflow(filePath, preprocessor, tplCopy.Options, compiled, tplCopy.Options.WorkflowLoader)
225
template.CompiledWorkflow = compiled
226
template.CompiledWorkflow.Options = tplCopy.Options
227
}
228
229
if isCachedTemplateValid(template) {
230
// options.Logger.Error().Msgf("returning cached template %s after recompiling %d requests", tplCopy.Options.TemplateID, tplCopy.Requests())
231
return template, nil
232
}
233
234
// else: fallthrough to re-parse template from scratch
235
}
236
}
237
238
return parseFromSource(filePath, preprocessor, options, parser)
239
}
240
241
// isGlobalMatchersEnabled checks if any of requests in the template
242
// have global matchers enabled. It iterates through all requests and
243
// returns true if at least one request has global matchers enabled;
244
// otherwise, it returns false. If global matchers templates are not
245
// enabled in the options, the method will immediately return false.
246
//
247
// Note: This method only checks the `RequestsHTTP`
248
// field of the template, which is specific to http-protocol-based
249
// templates.
250
//
251
// TODO: support all protocols.
252
func (template *Template) isGlobalMatchersEnabled() bool {
253
if !template.Options.Options.EnableGlobalMatchersTemplates {
254
return false
255
}
256
257
for _, request := range template.RequestsHTTP {
258
if request.GlobalMatchers {
259
return true
260
}
261
}
262
return false
263
}
264
265
// parseSelfContainedRequests parses the self contained template requests.
266
func (template *Template) parseSelfContainedRequests() {
267
if template.Signature.Value.String() != "" {
268
for _, request := range template.RequestsHTTP {
269
request.Signature = template.Signature
270
}
271
}
272
if !template.SelfContained {
273
return
274
}
275
for _, request := range template.RequestsHTTP {
276
request.SelfContained = true
277
}
278
for _, request := range template.RequestsNetwork {
279
request.SelfContained = true
280
}
281
for _, request := range template.RequestsHeadless {
282
request.SelfContained = true
283
}
284
}
285
286
// Requests returns the total request count for the template
287
func (template *Template) Requests() int {
288
return len(template.RequestsDNS) +
289
len(template.RequestsHTTP) +
290
len(template.RequestsFile) +
291
len(template.RequestsNetwork) +
292
len(template.RequestsHeadless) +
293
len(template.Workflows) +
294
len(template.RequestsSSL) +
295
len(template.RequestsWebsocket) +
296
len(template.RequestsWHOIS) +
297
len(template.RequestsCode) +
298
len(template.RequestsJavascript)
299
}
300
301
// compileProtocolRequests compiles all the protocol requests for the template
302
func (template *Template) compileProtocolRequests(options *protocols.ExecutorOptions) error {
303
templateRequests := template.Requests()
304
305
if templateRequests == 0 {
306
return fmt.Errorf("no requests defined for %s", template.ID)
307
}
308
309
if options.Options.OfflineHTTP {
310
return template.compileOfflineHTTPRequest(options)
311
}
312
313
var requests []protocols.Request
314
315
if template.hasMultipleRequests() {
316
// when multiple requests are present preserve the order of requests and protocols
317
// which is already done during unmarshalling
318
requests = template.RequestsQueue
319
if options.Flow == "" {
320
options.IsMultiProtocol = true
321
}
322
} else {
323
if template.HasDNSRequest() {
324
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsDNS)...)
325
}
326
if template.HasFileRequest() {
327
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsFile)...)
328
}
329
if template.HasNetworkRequest() {
330
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsNetwork)...)
331
}
332
if template.HasHTTPRequest() {
333
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsHTTP)...)
334
}
335
if template.HasHeadlessRequest() && options.Options.Headless {
336
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsHeadless)...)
337
}
338
if template.HasSSLRequest() {
339
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsSSL)...)
340
}
341
if template.HasWebsocketRequest() {
342
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsWebsocket)...)
343
}
344
if template.HasWHOISRequest() {
345
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsWHOIS)...)
346
}
347
if template.HasCodeRequest() && options.Options.EnableCodeTemplates {
348
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsCode)...)
349
}
350
if template.HasJavascriptRequest() {
351
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsJavascript)...)
352
}
353
}
354
var err error
355
template.Executer, err = tmplexec.NewTemplateExecuter(requests, options)
356
return err
357
}
358
359
// convertRequestToProtocolsRequest is a convenience wrapper to convert
360
// arbitrary interfaces which are slices of requests from the template to a
361
// slice of protocols.Request interface items.
362
func (template *Template) convertRequestToProtocolsRequest(requests interface{}) []protocols.Request {
363
switch reflect.TypeOf(requests).Kind() {
364
case reflect.Slice:
365
s := reflect.ValueOf(requests)
366
367
requestSlice := make([]protocols.Request, s.Len())
368
for i := 0; i < s.Len(); i++ {
369
value := s.Index(i)
370
valueInterface := value.Interface()
371
requestSlice[i] = valueInterface.(protocols.Request)
372
}
373
return requestSlice
374
}
375
return nil
376
}
377
378
// compileOfflineHTTPRequest iterates all requests if offline http mode is
379
// specified and collects all matchers for all the base request templates
380
// (those with URL {{BaseURL}} and it's slash variation.)
381
func (template *Template) compileOfflineHTTPRequest(options *protocols.ExecutorOptions) error {
382
operatorsList := []*operators.Operators{}
383
384
mainLoop:
385
for _, req := range template.RequestsHTTP {
386
hasPaths := len(req.Path) > 0
387
if !hasPaths {
388
break mainLoop
389
}
390
for _, path := range req.Path {
391
pathIsBaseURL := stringsutil.EqualFoldAny(path, "{{BaseURL}}", "{{BaseURL}}/", "/")
392
if !pathIsBaseURL {
393
break mainLoop
394
}
395
}
396
operatorsList = append(operatorsList, &req.Operators)
397
}
398
if len(operatorsList) > 0 {
399
options.Operators = operatorsList
400
var err error
401
template.Executer, err = tmplexec.NewTemplateExecuter([]protocols.Request{&offlinehttp.Request{}}, options)
402
if err != nil {
403
// it seems like flow executor cannot be used for offline http matching (ex:http(1) && http(2))
404
return ErrIncompatibleWithOfflineMatching
405
}
406
return err
407
}
408
409
return ErrIncompatibleWithOfflineMatching
410
}
411
412
// ParseTemplateFromReader parses a template from an [io.Reader] with optional
413
// preprocessing.
414
func ParseTemplateFromReader(reader io.Reader, preprocessor Preprocessor, options *protocols.ExecutorOptions) (*Template, error) {
415
data, err := io.ReadAll(reader)
416
if err != nil {
417
return nil, err
418
}
419
420
// a preprocessor is a variable like
421
// {{randstr}} which is replaced before unmarshalling
422
// as it is known to be a random static value per template
423
hasPreprocessor := false
424
allPreprocessors := getPreprocessors(preprocessor)
425
for _, preprocessor := range allPreprocessors {
426
if preprocessor.Exists(data) {
427
hasPreprocessor = true
428
break
429
}
430
}
431
432
if !hasPreprocessor {
433
// if no preprocessors exists parse template and exit
434
template, err := parseTemplate(data, options)
435
if err != nil {
436
return nil, err
437
}
438
if !template.Verified && len(template.Workflows) == 0 {
439
if config.DefaultConfig.LogAllEvents {
440
gologger.DefaultLogger.Print().Msgf("[%v] Template %s is not signed or tampered\n", aurora.Yellow("WRN").String(), template.ID)
441
}
442
}
443
return template, nil
444
}
445
446
// if preprocessor is required / exists in this template
447
// expand all preprocessors, parse once, then verify against original data
448
generatedConstants := map[string]interface{}{}
449
450
// ==== execute preprocessors ======
451
processedData := data
452
for _, v := range allPreprocessors {
453
var replaced map[string]interface{}
454
processedData, replaced = v.ProcessNReturnData(processedData)
455
// preprocess kind of act like a constant and are generated while loading
456
// and stay constant for the template lifecycle
457
generatedConstants = generators.MergeMaps(generatedConstants, replaced)
458
}
459
460
template, err := parseTemplateNoVerify(processedData, options)
461
if err != nil {
462
return nil, err
463
}
464
465
// add generated constants to constants map and executer options
466
template.Constants = generators.MergeMaps(template.Constants, generatedConstants)
467
template.Options.Constants = template.Constants
468
applyTemplateVerification(template, data)
469
470
if !template.Verified && len(template.Workflows) == 0 {
471
// workflows are not signed by default
472
if config.DefaultConfig.LogAllEvents {
473
gologger.DefaultLogger.Print().Msgf("[%v] Template %s is not signed or tampered\n", aurora.Yellow("WRN").String(), template.ID)
474
}
475
}
476
477
return template, nil
478
}
479
480
// parseTemplate parses the template and applies verification.
481
func parseTemplate(data []byte, srcOptions *protocols.ExecutorOptions) (*Template, error) {
482
template, err := parseTemplateNoVerify(data, srcOptions)
483
if err != nil {
484
return nil, err
485
}
486
applyTemplateVerification(template, data)
487
488
return template, nil
489
}
490
491
// parseTemplateNoVerify parses the template without applying any verification.
492
func parseTemplateNoVerify(data []byte, srcOptions *protocols.ExecutorOptions) (*Template, error) {
493
// Create a copy of the options specifically for this template
494
options := srcOptions.Copy()
495
496
template := &Template{}
497
var err error
498
switch config.GetTemplateFormatFromExt(template.Path) {
499
case config.JSON:
500
err = json.Unmarshal(data, template)
501
case config.YAML:
502
err = yaml.Unmarshal(data, template)
503
default:
504
// assume its yaml
505
if err = yaml.Unmarshal(data, template); err != nil {
506
return nil, err
507
}
508
}
509
if err != nil {
510
return nil, errkit.Wrapf(err, "failed to parse %s", template.Path)
511
}
512
513
if utils.IsBlank(template.Info.Name) {
514
return nil, errors.New("no template name field provided")
515
}
516
if template.Info.Authors.IsEmpty() {
517
return nil, errors.New("no template author field provided")
518
}
519
520
numberOfWorkflows := len(template.Workflows)
521
if numberOfWorkflows > 0 && numberOfWorkflows != template.Requests() {
522
return nil, errors.New("workflows cannot have other protocols")
523
}
524
525
// use default unknown severity
526
if len(template.Workflows) == 0 {
527
if template.Info.SeverityHolder.Severity == severity.Undefined {
528
// set unknown severity with counter and forced warning
529
template.Info.SeverityHolder.Severity = severity.Unknown
530
if options.Options.Validate {
531
// when validating return error
532
return nil, errors.New("no template severity field provided")
533
}
534
}
535
}
536
537
// Setting up variables regarding template metadata
538
options.TemplateID = template.ID
539
options.TemplateInfo = template.Info
540
options.StopAtFirstMatch = template.StopAtFirstMatch
541
542
if template.Variables.Len() > 0 {
543
options.Variables = template.Variables
544
}
545
546
// if more than 1 request per protocol exist we add request id to protocol request
547
// since in template context we have proto_prefix for each protocol it is overwritten
548
// if request id is not present
549
template.validateAllRequestIDs()
550
551
// create empty context args for template scope
552
options.CreateTemplateCtxStore()
553
options.ProtocolType = template.Type()
554
options.Constants = template.Constants
555
556
// initialize the js compiler if missing
557
if options.JsCompiler == nil {
558
options.JsCompiler = GetJsCompiler() // this is a singleton
559
}
560
561
template.Options = options
562
// If no requests, and it is also not a workflow, return error.
563
if template.Requests() == 0 {
564
return nil, fmt.Errorf("no requests defined for %s", template.ID)
565
}
566
567
// load `flow` and `source` in code protocol from file
568
// if file is referenced instead of actual source code
569
if err := template.ImportFileRefs(template.Options); err != nil {
570
return nil, errkit.Wrapf(err, "failed to load file refs for %s", template.ID)
571
}
572
573
if err := template.compileProtocolRequests(template.Options); err != nil {
574
return nil, err
575
}
576
577
if template.Executer != nil {
578
if err := template.Executer.Compile(); err != nil {
579
return nil, errors.Wrap(err, "could not compile request")
580
}
581
template.TotalRequests = template.Executer.Requests()
582
}
583
if template.Executer == nil && template.CompiledWorkflow == nil {
584
return nil, ErrCreateTemplateExecutor
585
}
586
template.parseSelfContainedRequests()
587
588
return template, nil
589
}
590
591
// applyTemplateVerification verifies a parsed template against the provided data.
592
func applyTemplateVerification(template *Template, data []byte) {
593
if template == nil || template.Options == nil {
594
return
595
}
596
597
options := template.Options
598
// check if the template is verified
599
// only valid templates can be verified or signed
600
if options.TemplateVerificationCallback != nil && options.TemplatePath != "" {
601
if cached := options.TemplateVerificationCallback(options.TemplatePath); cached != nil {
602
template.Verified = cached.Verified
603
template.TemplateVerifier = cached.Verifier
604
options.TemplateVerifier = cached.Verifier
605
//nolint
606
if !(template.Verified && template.TemplateVerifier == "projectdiscovery/nuclei-templates") {
607
template.Options.RawTemplate = data
608
}
609
return
610
}
611
}
612
613
var verifier *signer.TemplateSigner
614
for _, verifier = range signer.DefaultTemplateVerifiers {
615
template.Verified, _ = verifier.Verify(data, template)
616
if config.DefaultConfig.LogAllEvents {
617
gologger.Verbose().Msgf("template %v verified by %s : %v", template.ID, verifier.Identifier(), template.Verified)
618
}
619
if template.Verified {
620
template.TemplateVerifier = verifier.Identifier()
621
break
622
}
623
}
624
options.TemplateVerifier = template.TemplateVerifier
625
626
//nolint
627
if !(template.Verified && verifier.Identifier() == "projectdiscovery/nuclei-templates") {
628
template.Options.RawTemplate = data
629
}
630
}
631
632
// isCachedTemplateValid validates that a cached template is still usable after
633
// option updates
634
func isCachedTemplateValid(template *Template) bool {
635
// no requests or workflows
636
if template.Requests() == 0 && len(template.Workflows) == 0 {
637
return false
638
}
639
640
// options not initialized
641
if template.Options == nil {
642
return false
643
}
644
645
// executer not available for non-workflow template
646
if len(template.Workflows) == 0 && template.Executer == nil {
647
return false
648
}
649
650
// compiled workflow not available
651
if len(template.Workflows) > 0 && template.CompiledWorkflow == nil {
652
return false
653
}
654
655
// template ID mismatch
656
if template.Options.TemplateID != template.ID {
657
return false
658
}
659
660
// executer exists but no requests or flow available
661
if template.Executer != nil {
662
// NOTE(dwisiswant0): This is a basic sanity check since we can't access
663
// private fields, but we can check requests tho
664
if template.Requests() == 0 && template.Options.Flow == "" {
665
return false
666
}
667
}
668
669
if template.Options.Options == nil {
670
return false
671
}
672
673
return true
674
}
675
676
var (
677
jsCompiler *compiler.Compiler
678
jsCompilerOnce = sync.OnceFunc(func() {
679
jsCompiler = compiler.New()
680
})
681
)
682
683
func GetJsCompiler() *compiler.Compiler {
684
jsCompilerOnce()
685
return jsCompiler
686
}
687
688