Path: blob/dev/pkg/tmplexec/flow/flow_executor.go
2070 views
package flow12import (3"fmt"4"io"5"strconv"6"strings"7"sync/atomic"89"github.com/Mzack9999/goja"10"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"11"github.com/projectdiscovery/nuclei/v3/pkg/protocols"12"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"13"github.com/projectdiscovery/nuclei/v3/pkg/scan"14templateTypes "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"1516"github.com/kitabisa/go-ci"17"github.com/projectdiscovery/nuclei/v3/pkg/types"18"github.com/projectdiscovery/utils/errkit"19fileutil "github.com/projectdiscovery/utils/file"20mapsutil "github.com/projectdiscovery/utils/maps"21"go.uber.org/multierr"22)2324var (25// ErrInvalidRequestID is a request id error26ErrInvalidRequestID = errkit.New("invalid request id provided")27)2829// ProtoOptions are options that can be passed to flow protocol callback30// ex: dns(protoOptions) <- protoOptions are optional and can be anything31type ProtoOptions struct {32protoName string33reqIDS []string34}3536// FlowExecutor is a flow executor for executing a flow37type FlowExecutor struct {38ctx *scan.ScanContext // scan context (includes target etc)39options *protocols.ExecutorOptions4041// javascript runtime reference and compiled program42program *goja.Program // compiled js program4344// protocol requests and their callback functions45allProtocols map[string][]protocols.Request46protoFunctions map[string]func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value // reqFunctions contains functions that allow executing requests/protocols from js4748// logic related variables49results *atomic.Bool50allErrs mapsutil.SyncLockMap[string, error]51// these are keys whose values are meant to be flatten before executing52// a request ex: if dynamic extractor returns ["value"] it will be converted to "value"53flattenKeys []string5455executed *mapsutil.SyncLockMap[string, struct{}]56}5758// NewFlowExecutor creates a new flow executor from a list of requests59// Note: Unlike other engine for every target x template flow needs to be compiled and executed everytime60// unlike other engines where we compile once and execute multiple times61func NewFlowExecutor(requests []protocols.Request, ctx *scan.ScanContext, options *protocols.ExecutorOptions, results *atomic.Bool, program *goja.Program) (*FlowExecutor, error) {62allprotos := make(map[string][]protocols.Request)63for _, req := range requests {64switch req.Type() {65case templateTypes.DNSProtocol:66allprotos[templateTypes.DNSProtocol.String()] = append(allprotos[templateTypes.DNSProtocol.String()], req)67case templateTypes.HTTPProtocol:68allprotos[templateTypes.HTTPProtocol.String()] = append(allprotos[templateTypes.HTTPProtocol.String()], req)69case templateTypes.NetworkProtocol:70allprotos[templateTypes.NetworkProtocol.String()] = append(allprotos[templateTypes.NetworkProtocol.String()], req)71case templateTypes.FileProtocol:72allprotos[templateTypes.FileProtocol.String()] = append(allprotos[templateTypes.FileProtocol.String()], req)73case templateTypes.HeadlessProtocol:74allprotos[templateTypes.HeadlessProtocol.String()] = append(allprotos[templateTypes.HeadlessProtocol.String()], req)75case templateTypes.SSLProtocol:76allprotos[templateTypes.SSLProtocol.String()] = append(allprotos[templateTypes.SSLProtocol.String()], req)77case templateTypes.WebsocketProtocol:78allprotos[templateTypes.WebsocketProtocol.String()] = append(allprotos[templateTypes.WebsocketProtocol.String()], req)79case templateTypes.WHOISProtocol:80allprotos[templateTypes.WHOISProtocol.String()] = append(allprotos[templateTypes.WHOISProtocol.String()], req)81case templateTypes.CodeProtocol:82allprotos[templateTypes.CodeProtocol.String()] = append(allprotos[templateTypes.CodeProtocol.String()], req)83case templateTypes.JavascriptProtocol:84allprotos[templateTypes.JavascriptProtocol.String()] = append(allprotos[templateTypes.JavascriptProtocol.String()], req)85case templateTypes.OfflineHTTPProtocol:86// offlinehttp is run in passive mode but templates are same so instead of using offlinehttp() we use http() in flow87allprotos[templateTypes.HTTPProtocol.String()] = append(allprotos[templateTypes.OfflineHTTPProtocol.String()], req)88default:89return nil, fmt.Errorf("invalid request type %s", req.Type().String())90}91}92f := &FlowExecutor{93allProtocols: allprotos,94options: options,95allErrs: mapsutil.SyncLockMap[string, error]{96ReadOnly: atomic.Bool{},97Map: make(map[string]error),98},99protoFunctions: map[string]func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value{},100results: results,101ctx: ctx,102program: program,103executed: mapsutil.NewSyncLockMap[string, struct{}](),104}105return f, nil106}107108// Compile compiles js program and registers all functions109func (f *FlowExecutor) Compile() error {110if f.results == nil {111f.results = new(atomic.Bool)112}113// load all variables and evaluate with existing data114variableMap := f.options.Variables.Evaluate(f.options.GetTemplateCtx(f.ctx.Input.MetaInput).GetAll())115// cli options116optionVars := generators.BuildPayloadFromOptions(f.options.Options)117// constants118constants := f.options.Constants119allVars := generators.MergeMaps(variableMap, constants, optionVars)120// we support loading variables from files in variables , cli options and constants121// try to load if files exist122for k, v := range allVars {123if str, ok := v.(string); ok && len(str) < 150 && fileutil.FileExists(str) {124if value, err := f.ReadDataFromFile(str); err == nil {125allVars[k] = value126} else {127f.ctx.LogWarning("could not load file '%s' for variable '%s': %s", str, k, err)128}129}130}131f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Merge(allVars) // merge all variables into template context132133// ---- define callback functions/objects----134f.protoFunctions = map[string]func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value{}135// iterate over all protocols and generate callback functions for each protocol136for p, requests := range f.allProtocols {137// for each protocol build a requestMap with reqID and protocol request138reqMap := mapsutil.Map[string, protocols.Request]{}139counter := 0140proto := strings.ToLower(p) // donot use loop variables in callback functions directly141for index := range requests {142counter++ // start index from 1143request := f.allProtocols[proto][index]144if request.GetID() != "" {145// if id is present use it146reqMap[request.GetID()] = request147}148// fallback to using index as id149// always allow index as id as a fallback150reqMap[strconv.Itoa(counter)] = request151}152// ---define hook that allows protocol/request execution from js-----153// --- this is the actual callback that is executed when function is invoked in js----154f.protoFunctions[proto] = func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value {155opts := &ProtoOptions{156protoName: proto,157}158for _, v := range call.Arguments {159switch value := v.Export().(type) {160default:161opts.reqIDS = append(opts.reqIDS, types.ToString(value))162}163}164// before executing any protocol function flatten tracked values165if len(f.flattenKeys) > 0 {166ctx := f.options.GetTemplateCtx(f.ctx.Input.MetaInput)167for _, key := range f.flattenKeys {168if value, ok := ctx.Get(key); ok {169ctx.Set(key, flatten(value))170}171}172}173return runtime.ToValue(f.requestExecutor(runtime, reqMap, opts))174}175}176return nil177}178179// ExecuteWithResults executes the flow and returns results180func (f *FlowExecutor) ExecuteWithResults(ctx *scan.ScanContext) error {181select {182case <-ctx.Context().Done():183return ctx.Context().Err()184default:185}186187f.ctx.Input = ctx.Input188// -----Load all types of variables-----189// add all input args to template context190if f.ctx.Input != nil && f.ctx.Input.HasArgs() {191f.ctx.Input.ForEach(func(key string, value interface{}) {192f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Set(key, value)193})194}195196// get a new runtime from pool197runtime := GetJSRuntime(f.options.Options)198defer func() {199// whether to reuse or not depends on the whether script modifies200// global scope or not,201PutJSRuntime(runtime, compiler.CanRunAsIIFE(f.options.Flow))202}()203defer func() {204// remove set builtin205_ = runtime.GlobalObject().Delete("set")206_ = runtime.GlobalObject().Delete("template")207for proto := range f.protoFunctions {208_ = runtime.GlobalObject().Delete(proto)209}210runtime.RemoveContextValue("executionId")211}()212213// TODO(dwisiswant0): remove this once we get the RCA.214defer func() {215if ci.IsCI() {216return217}218219if r := recover(); r != nil {220f.ctx.LogError(fmt.Errorf("panic occurred while executing flow: %v", r))221}222}()223224if ctx.OnResult == nil {225return fmt.Errorf("output callback cannot be nil")226}227// before running register set of builtins228if err := runtime.Set("set", func(call goja.FunctionCall) goja.Value {229varName := call.Argument(0).Export()230varValue := call.Argument(1).Export()231f.options.GetTemplateCtx(f.ctx.Input.MetaInput).Set(types.ToString(varName), varValue)232return goja.Null()233}); err != nil {234return err235}236// also register functions that allow executing protocols from js237for proto, fn := range f.protoFunctions {238if err := runtime.Set(proto, fn); err != nil {239return err240}241}242// register template object243tmplObj := f.options.GetTemplateCtx(f.ctx.Input.MetaInput).GetAll()244if tmplObj == nil {245tmplObj = map[string]interface{}{}246}247if err := runtime.Set("template", tmplObj); err != nil {248return err249}250251runtime.SetContextValue("executionId", f.options.Options.ExecutionId)252253// pass flow and execute the js vm and handle errors254_, err := runtime.RunProgram(f.program)255f.reconcileProgress()256if err != nil {257ctx.LogError(err)258return errkit.Wrapf(err, "failed to execute flow\n%v\n", f.options.Flow)259}260runtimeErr := f.GetRuntimeErrors()261if runtimeErr != nil {262ctx.LogError(runtimeErr)263return errkit.Wrap(runtimeErr, "got following errors while executing flow")264}265266return nil267}268269func (f *FlowExecutor) reconcileProgress() {270for proto, list := range f.allProtocols {271for idx, req := range list {272key := requestKey(proto, req, strconv.Itoa(idx+1))273if _, seen := f.executed.Get(key); !seen {274// never executed → pretend it finished so that stats match275f.options.Progress.SetRequests(uint64(req.Requests()))276}277}278}279}280281// GetRuntimeErrors returns all runtime errors (i.e errors from all protocol combined)282func (f *FlowExecutor) GetRuntimeErrors() error {283errs := []error{}284for proto, err := range f.allErrs.GetAll() {285errs = append(errs, errkit.Wrapf(err, "failed to execute %v protocol", proto))286}287return multierr.Combine(errs...)288}289290// ReadDataFromFile reads data from file respecting sandbox options291func (f *FlowExecutor) ReadDataFromFile(payload string) ([]string, error) {292values := []string{}293// load file respecting sandbox294reader, err := f.options.Options.LoadHelperFile(payload, f.options.TemplatePath, f.options.Catalog)295if err != nil {296return values, err297}298defer func() {299_ = reader.Close()300}()301bin, err := io.ReadAll(reader)302if err != nil {303return values, err304}305for _, line := range strings.Split(string(bin), "\n") {306line = strings.TrimSpace(line)307if line != "" {308values = append(values, line)309}310}311return values, nil312}313314// Name returns the type of engine315func (f *FlowExecutor) Name() string {316return "flow"317}318319320