Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/tmplexec/flow/flow_executor.go
2070 views
1
package flow
2
3
import (
4
"fmt"
5
"io"
6
"strconv"
7
"strings"
8
"sync/atomic"
9
10
"github.com/Mzack9999/goja"
11
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
12
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
13
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
14
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
15
templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
16
17
"github.com/kitabisa/go-ci"
18
"github.com/projectdiscovery/nuclei/v3/pkg/types"
19
"github.com/projectdiscovery/utils/errkit"
20
fileutil "github.com/projectdiscovery/utils/file"
21
mapsutil "github.com/projectdiscovery/utils/maps"
22
"go.uber.org/multierr"
23
)
24
25
var (
26
// ErrInvalidRequestID is a request id error
27
ErrInvalidRequestID = errkit.New("invalid request id provided")
28
)
29
30
// ProtoOptions are options that can be passed to flow protocol callback
31
// ex: dns(protoOptions) <- protoOptions are optional and can be anything
32
type ProtoOptions struct {
33
protoName string
34
reqIDS []string
35
}
36
37
// FlowExecutor is a flow executor for executing a flow
38
type FlowExecutor struct {
39
ctx *scan.ScanContext // scan context (includes target etc)
40
options *protocols.ExecutorOptions
41
42
// javascript runtime reference and compiled program
43
program *goja.Program // compiled js program
44
45
// protocol requests and their callback functions
46
allProtocols map[string][]protocols.Request
47
protoFunctions map[string]func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value // reqFunctions contains functions that allow executing requests/protocols from js
48
49
// logic related variables
50
results *atomic.Bool
51
allErrs mapsutil.SyncLockMap[string, error]
52
// these are keys whose values are meant to be flatten before executing
53
// a request ex: if dynamic extractor returns ["value"] it will be converted to "value"
54
flattenKeys []string
55
56
executed *mapsutil.SyncLockMap[string, struct{}]
57
}
58
59
// NewFlowExecutor creates a new flow executor from a list of requests
60
// Note: Unlike other engine for every target x template flow needs to be compiled and executed everytime
61
// unlike other engines where we compile once and execute multiple times
62
func NewFlowExecutor(requests []protocols.Request, ctx *scan.ScanContext, options *protocols.ExecutorOptions, results *atomic.Bool, program *goja.Program) (*FlowExecutor, error) {
63
allprotos := make(map[string][]protocols.Request)
64
for _, req := range requests {
65
switch req.Type() {
66
case templateTypes.DNSProtocol:
67
allprotos[templateTypes.DNSProtocol.String()] = append(allprotos[templateTypes.DNSProtocol.String()], req)
68
case templateTypes.HTTPProtocol:
69
allprotos[templateTypes.HTTPProtocol.String()] = append(allprotos[templateTypes.HTTPProtocol.String()], req)
70
case templateTypes.NetworkProtocol:
71
allprotos[templateTypes.NetworkProtocol.String()] = append(allprotos[templateTypes.NetworkProtocol.String()], req)
72
case templateTypes.FileProtocol:
73
allprotos[templateTypes.FileProtocol.String()] = append(allprotos[templateTypes.FileProtocol.String()], req)
74
case templateTypes.HeadlessProtocol:
75
allprotos[templateTypes.HeadlessProtocol.String()] = append(allprotos[templateTypes.HeadlessProtocol.String()], req)
76
case templateTypes.SSLProtocol:
77
allprotos[templateTypes.SSLProtocol.String()] = append(allprotos[templateTypes.SSLProtocol.String()], req)
78
case templateTypes.WebsocketProtocol:
79
allprotos[templateTypes.WebsocketProtocol.String()] = append(allprotos[templateTypes.WebsocketProtocol.String()], req)
80
case templateTypes.WHOISProtocol:
81
allprotos[templateTypes.WHOISProtocol.String()] = append(allprotos[templateTypes.WHOISProtocol.String()], req)
82
case templateTypes.CodeProtocol:
83
allprotos[templateTypes.CodeProtocol.String()] = append(allprotos[templateTypes.CodeProtocol.String()], req)
84
case templateTypes.JavascriptProtocol:
85
allprotos[templateTypes.JavascriptProtocol.String()] = append(allprotos[templateTypes.JavascriptProtocol.String()], req)
86
case templateTypes.OfflineHTTPProtocol:
87
// offlinehttp is run in passive mode but templates are same so instead of using offlinehttp() we use http() in flow
88
allprotos[templateTypes.HTTPProtocol.String()] = append(allprotos[templateTypes.OfflineHTTPProtocol.String()], req)
89
default:
90
return nil, fmt.Errorf("invalid request type %s", req.Type().String())
91
}
92
}
93
f := &FlowExecutor{
94
allProtocols: allprotos,
95
options: options,
96
allErrs: mapsutil.SyncLockMap[string, error]{
97
ReadOnly: atomic.Bool{},
98
Map: make(map[string]error),
99
},
100
protoFunctions: map[string]func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value{},
101
results: results,
102
ctx: ctx,
103
program: program,
104
executed: mapsutil.NewSyncLockMap[string, struct{}](),
105
}
106
return f, nil
107
}
108
109
// Compile compiles js program and registers all functions
110
func (f *FlowExecutor) Compile() error {
111
if f.results == nil {
112
f.results = new(atomic.Bool)
113
}
114
// load all variables and evaluate with existing data
115
variableMap := f.options.Variables.Evaluate(f.options.GetTemplateCtx(f.ctx.Input.MetaInput).GetAll())
116
// cli options
117
optionVars := generators.BuildPayloadFromOptions(f.options.Options)
118
// constants
119
constants := f.options.Constants
120
allVars := generators.MergeMaps(variableMap, constants, optionVars)
121
// we support loading variables from files in variables , cli options and constants
122
// try to load if files exist
123
for k, v := range allVars {
124
if str, ok := v.(string); ok && len(str) < 150 && fileutil.FileExists(str) {
125
if value, err := f.ReadDataFromFile(str); err == nil {
126
allVars[k] = value
127
} else {
128
f.ctx.LogWarning("could not load file '%s' for variable '%s': %s", str, k, err)
129
}
130
}
131
}
132
f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Merge(allVars) // merge all variables into template context
133
134
// ---- define callback functions/objects----
135
f.protoFunctions = map[string]func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value{}
136
// iterate over all protocols and generate callback functions for each protocol
137
for p, requests := range f.allProtocols {
138
// for each protocol build a requestMap with reqID and protocol request
139
reqMap := mapsutil.Map[string, protocols.Request]{}
140
counter := 0
141
proto := strings.ToLower(p) // donot use loop variables in callback functions directly
142
for index := range requests {
143
counter++ // start index from 1
144
request := f.allProtocols[proto][index]
145
if request.GetID() != "" {
146
// if id is present use it
147
reqMap[request.GetID()] = request
148
}
149
// fallback to using index as id
150
// always allow index as id as a fallback
151
reqMap[strconv.Itoa(counter)] = request
152
}
153
// ---define hook that allows protocol/request execution from js-----
154
// --- this is the actual callback that is executed when function is invoked in js----
155
f.protoFunctions[proto] = func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value {
156
opts := &ProtoOptions{
157
protoName: proto,
158
}
159
for _, v := range call.Arguments {
160
switch value := v.Export().(type) {
161
default:
162
opts.reqIDS = append(opts.reqIDS, types.ToString(value))
163
}
164
}
165
// before executing any protocol function flatten tracked values
166
if len(f.flattenKeys) > 0 {
167
ctx := f.options.GetTemplateCtx(f.ctx.Input.MetaInput)
168
for _, key := range f.flattenKeys {
169
if value, ok := ctx.Get(key); ok {
170
ctx.Set(key, flatten(value))
171
}
172
}
173
}
174
return runtime.ToValue(f.requestExecutor(runtime, reqMap, opts))
175
}
176
}
177
return nil
178
}
179
180
// ExecuteWithResults executes the flow and returns results
181
func (f *FlowExecutor) ExecuteWithResults(ctx *scan.ScanContext) error {
182
select {
183
case <-ctx.Context().Done():
184
return ctx.Context().Err()
185
default:
186
}
187
188
f.ctx.Input = ctx.Input
189
// -----Load all types of variables-----
190
// add all input args to template context
191
if f.ctx.Input != nil && f.ctx.Input.HasArgs() {
192
f.ctx.Input.ForEach(func(key string, value interface{}) {
193
f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Set(key, value)
194
})
195
}
196
197
// get a new runtime from pool
198
runtime := GetJSRuntime(f.options.Options)
199
defer func() {
200
// whether to reuse or not depends on the whether script modifies
201
// global scope or not,
202
PutJSRuntime(runtime, compiler.CanRunAsIIFE(f.options.Flow))
203
}()
204
defer func() {
205
// remove set builtin
206
_ = runtime.GlobalObject().Delete("set")
207
_ = runtime.GlobalObject().Delete("template")
208
for proto := range f.protoFunctions {
209
_ = runtime.GlobalObject().Delete(proto)
210
}
211
runtime.RemoveContextValue("executionId")
212
}()
213
214
// TODO(dwisiswant0): remove this once we get the RCA.
215
defer func() {
216
if ci.IsCI() {
217
return
218
}
219
220
if r := recover(); r != nil {
221
f.ctx.LogError(fmt.Errorf("panic occurred while executing flow: %v", r))
222
}
223
}()
224
225
if ctx.OnResult == nil {
226
return fmt.Errorf("output callback cannot be nil")
227
}
228
// before running register set of builtins
229
if err := runtime.Set("set", func(call goja.FunctionCall) goja.Value {
230
varName := call.Argument(0).Export()
231
varValue := call.Argument(1).Export()
232
f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Set(types.ToString(varName), varValue)
233
return goja.Null()
234
}); err != nil {
235
return err
236
}
237
// also register functions that allow executing protocols from js
238
for proto, fn := range f.protoFunctions {
239
if err := runtime.Set(proto, fn); err != nil {
240
return err
241
}
242
}
243
// register template object
244
tmplObj := f.options.GetTemplateCtx(f.ctx.Input.MetaInput).GetAll()
245
if tmplObj == nil {
246
tmplObj = map[string]interface{}{}
247
}
248
if err := runtime.Set("template", tmplObj); err != nil {
249
return err
250
}
251
252
runtime.SetContextValue("executionId", f.options.Options.ExecutionId)
253
254
// pass flow and execute the js vm and handle errors
255
_, err := runtime.RunProgram(f.program)
256
f.reconcileProgress()
257
if err != nil {
258
ctx.LogError(err)
259
return errkit.Wrapf(err, "failed to execute flow\n%v\n", f.options.Flow)
260
}
261
runtimeErr := f.GetRuntimeErrors()
262
if runtimeErr != nil {
263
ctx.LogError(runtimeErr)
264
return errkit.Wrap(runtimeErr, "got following errors while executing flow")
265
}
266
267
return nil
268
}
269
270
func (f *FlowExecutor) reconcileProgress() {
271
for proto, list := range f.allProtocols {
272
for idx, req := range list {
273
key := requestKey(proto, req, strconv.Itoa(idx+1))
274
if _, seen := f.executed.Get(key); !seen {
275
// never executed → pretend it finished so that stats match
276
f.options.Progress.SetRequests(uint64(req.Requests()))
277
}
278
}
279
}
280
}
281
282
// GetRuntimeErrors returns all runtime errors (i.e errors from all protocol combined)
283
func (f *FlowExecutor) GetRuntimeErrors() error {
284
errs := []error{}
285
for proto, err := range f.allErrs.GetAll() {
286
errs = append(errs, errkit.Wrapf(err, "failed to execute %v protocol", proto))
287
}
288
return multierr.Combine(errs...)
289
}
290
291
// ReadDataFromFile reads data from file respecting sandbox options
292
func (f *FlowExecutor) ReadDataFromFile(payload string) ([]string, error) {
293
values := []string{}
294
// load file respecting sandbox
295
reader, err := f.options.Options.LoadHelperFile(payload, f.options.TemplatePath, f.options.Catalog)
296
if err != nil {
297
return values, err
298
}
299
defer func() {
300
_ = reader.Close()
301
}()
302
bin, err := io.ReadAll(reader)
303
if err != nil {
304
return values, err
305
}
306
for _, line := range strings.Split(string(bin), "\n") {
307
line = strings.TrimSpace(line)
308
if line != "" {
309
values = append(values, line)
310
}
311
}
312
return values, nil
313
}
314
315
// Name returns the type of engine
316
func (f *FlowExecutor) Name() string {
317
return "flow"
318
}
319
320