github.com/crowdsecurity/crowdsec@v1.6.1/pkg/exprhelpers/debugger.go (about)

     1  package exprhelpers
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  
     8  	"github.com/antonmedv/expr"
     9  	"github.com/antonmedv/expr/vm"
    10  	log "github.com/sirupsen/logrus"
    11  )
    12  
    13  type ExprRuntimeDebug struct {
    14  	Logger  *log.Entry
    15  	Lines   []string
    16  	Outputs []OpOutput
    17  }
    18  
    19  var IndentStep = 4
    20  
    21  // we use this struct to store the output of the expr runtime
    22  type OpOutput struct {
    23  	Code string //relevant code part
    24  
    25  	CodeDepth  int //level of nesting
    26  	BlockStart bool
    27  	BlockEnd   bool
    28  
    29  	Func        bool //true if it's a function call
    30  	FuncName    string
    31  	Args        []string
    32  	FuncResults []string
    33  	//
    34  	Comparison bool //true if it's a comparison
    35  	Negated    bool
    36  	Left       string
    37  	Right      string
    38  	//
    39  	JumpIf  bool //true if it's conditional jump
    40  	IfTrue  bool
    41  	IfFalse bool
    42  	//
    43  	Condition         bool //true if it's a condition
    44  	ConditionIn       bool
    45  	ConditionContains bool
    46  	//used for comparisons, conditional jumps and conditions
    47  	StrConditionResult string
    48  	ConditionResult    *bool //should always be present for conditions
    49  
    50  	//
    51  	Finalized bool //used when a node is finalized, we already fetched result from next OP
    52  }
    53  
    54  func (o *OpOutput) String() string {
    55  
    56  	ret := fmt.Sprintf("%*c", o.CodeDepth, ' ')
    57  	if len(o.Code) != 0 {
    58  		ret += fmt.Sprintf("[%s]", o.Code)
    59  	}
    60  	ret += " "
    61  
    62  	switch {
    63  	case o.BlockStart:
    64  		ret = fmt.Sprintf("%*cBLOCK_START [%s]", o.CodeDepth-IndentStep, ' ', o.Code)
    65  		return ret
    66  	case o.BlockEnd:
    67  		indent := o.CodeDepth - (IndentStep * 2)
    68  		if indent < 0 {
    69  			indent = 0
    70  		}
    71  		ret = fmt.Sprintf("%*cBLOCK_END [%s]", indent, ' ', o.Code)
    72  		if len(o.StrConditionResult) > 0 {
    73  			ret += fmt.Sprintf(" -> %s", o.StrConditionResult)
    74  		}
    75  		return ret
    76  		//A block end can carry a value, for example if it's a count, any, all etc. XXX
    77  	case o.Func:
    78  		return ret + fmt.Sprintf("%s(%s) = %s", o.FuncName, strings.Join(o.Args, ", "), strings.Join(o.FuncResults, ", "))
    79  	case o.Comparison:
    80  		if o.Negated {
    81  			ret += "NOT "
    82  		}
    83  		ret += fmt.Sprintf("%s == %s -> %s", o.Left, o.Right, o.StrConditionResult)
    84  		return ret
    85  	case o.ConditionIn:
    86  		return ret + fmt.Sprintf("%s in %s -> %s", o.Args[0], o.Args[1], o.StrConditionResult)
    87  	case o.ConditionContains:
    88  		return ret + fmt.Sprintf("%s contains %s -> %s", o.Args[0], o.Args[1], o.StrConditionResult)
    89  	case o.JumpIf && o.IfTrue:
    90  		if o.ConditionResult != nil {
    91  			if *o.ConditionResult {
    92  				return ret + "OR -> false"
    93  			}
    94  			return ret + "OR -> true"
    95  		}
    96  		return ret + "OR(?)"
    97  	case o.JumpIf && o.IfFalse:
    98  		if o.ConditionResult != nil {
    99  			if *o.ConditionResult {
   100  				return ret + "AND -> true"
   101  			}
   102  			return ret + "AND -> false"
   103  		}
   104  		return ret + "AND(?)"
   105  	}
   106  	return ret + ""
   107  }
   108  
   109  func (erp ExprRuntimeDebug) extractCode(ip int, program *vm.Program, parts []string) string {
   110  
   111  	//log.Tracef("# extracting code for ip %d [%s]", ip, parts[1])
   112  	if program.Locations[ip].Line == 0 { //it seems line is zero when it's not actual code (ie. op push at the beginning)
   113  		log.Tracef("zero location ?")
   114  		return ""
   115  	}
   116  	startLine := program.Locations[ip].Line
   117  	startColumn := program.Locations[ip].Column
   118  	lines := strings.Split(program.Source.Content(), "\n")
   119  
   120  	endCol := 0
   121  	endLine := 0
   122  
   123  	for i := ip + 1; i < len(program.Locations); i++ {
   124  		if program.Locations[i].Line > startLine || (program.Locations[i].Line == startLine && program.Locations[i].Column > startColumn) {
   125  			//we didn't had values yet and it's superior to current one, take it
   126  			if endLine == 0 && endCol == 0 {
   127  				endLine = program.Locations[i].Line
   128  				endCol = program.Locations[i].Column
   129  			}
   130  			//however, we are looking for the closest upper one
   131  			if program.Locations[i].Line < endLine || (program.Locations[i].Line == endLine && program.Locations[i].Column < endCol) {
   132  				endLine = program.Locations[i].Line
   133  				endCol = program.Locations[i].Column
   134  			}
   135  
   136  		}
   137  	}
   138  	//maybe it was the last instruction ?
   139  	if endCol == 0 && endLine == 0 {
   140  		endLine = len(lines)
   141  		endCol = len(lines[endLine-1])
   142  	}
   143  	code_snippet := ""
   144  	startLine -= 1 //line count starts at 1
   145  	endLine -= 1
   146  
   147  	for i := startLine; i <= endLine; i++ {
   148  		if i == startLine {
   149  			if startLine != endLine {
   150  				code_snippet += lines[i][startColumn:]
   151  				continue
   152  			}
   153  			code_snippet += lines[i][startColumn:endCol]
   154  			break
   155  		}
   156  		if i == endLine {
   157  			code_snippet += lines[i][:endCol]
   158  			break
   159  		}
   160  		code_snippet += lines[i]
   161  	}
   162  
   163  	log.Tracef("#code extract for ip %d [%s] -> '%s'", ip, parts[1], code_snippet)
   164  	return cleanTextForDebug(code_snippet)
   165  }
   166  
   167  func autoQuote(v any) string {
   168  	switch x := v.(type) {
   169  	case string:
   170  		//let's avoid printing long strings. it can happen ie. when we are debugging expr with `File()` or similar helpers
   171  		if len(x) > 40 {
   172  			return fmt.Sprintf("%q", x[:40]+"...")
   173  		} else {
   174  			return fmt.Sprintf("%q", x)
   175  		}
   176  	default:
   177  		return fmt.Sprintf("%v", x)
   178  	}
   179  }
   180  
   181  func (erp ExprRuntimeDebug) ipDebug(ip int, vm *vm.VM, program *vm.Program, parts []string, outputs []OpOutput) ([]OpOutput, error) {
   182  
   183  	IdxOut := len(outputs)
   184  	prevIdxOut := 0
   185  	currentDepth := 0
   186  
   187  	//when there is a function call or comparison, we need to wait for the next instruction to get the result and "finalize" the previous one
   188  	if IdxOut > 0 {
   189  		prevIdxOut = IdxOut - 1
   190  		currentDepth = outputs[prevIdxOut].CodeDepth
   191  		if outputs[prevIdxOut].Func && !outputs[prevIdxOut].Finalized {
   192  			stack := vm.Stack()
   193  			num_items := 1
   194  			for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
   195  				outputs[prevIdxOut].FuncResults = append(outputs[prevIdxOut].FuncResults, autoQuote(stack[i]))
   196  				num_items--
   197  			}
   198  			outputs[prevIdxOut].Finalized = true
   199  		} else if (outputs[prevIdxOut].Comparison || outputs[prevIdxOut].Condition) && !outputs[prevIdxOut].Finalized {
   200  			stack := vm.Stack()
   201  			outputs[prevIdxOut].StrConditionResult = fmt.Sprintf("%+v", stack)
   202  			if val, ok := stack[0].(bool); ok {
   203  				outputs[prevIdxOut].ConditionResult = new(bool)
   204  				*outputs[prevIdxOut].ConditionResult = val
   205  			}
   206  			outputs[prevIdxOut].Finalized = true
   207  		}
   208  	}
   209  
   210  	erp.Logger.Tracef("[STEP %d:%s] (stack:%+v) (parts:%+v) {depth:%d}", ip, parts[1], vm.Stack(), parts, currentDepth)
   211  	out := OpOutput{}
   212  	out.CodeDepth = currentDepth
   213  	out.Code = erp.extractCode(ip, program, parts)
   214  
   215  	switch parts[1] {
   216  	case "OpBegin":
   217  		out.CodeDepth += IndentStep
   218  		out.BlockStart = true
   219  		outputs = append(outputs, out)
   220  	case "OpEnd":
   221  		out.CodeDepth -= IndentStep
   222  		out.BlockEnd = true
   223  		//OpEnd can carry value, if it's any/all/count etc.
   224  		if len(vm.Stack()) > 0 {
   225  			out.StrConditionResult = fmt.Sprintf("%v", vm.Stack())
   226  		}
   227  		outputs = append(outputs, out)
   228  	case "OpNot":
   229  		//negate the previous condition
   230  		outputs[prevIdxOut].Negated = true
   231  	case "OpTrue": //generated when possible ? (1 == 1)
   232  		out.Condition = true
   233  		out.ConditionResult = new(bool)
   234  		*out.ConditionResult = true
   235  		out.StrConditionResult = "true"
   236  		outputs = append(outputs, out)
   237  	case "OpFalse": //generated when possible ? (1 != 1)
   238  		out.Condition = true
   239  		out.ConditionResult = new(bool)
   240  		*out.ConditionResult = false
   241  		out.StrConditionResult = "false"
   242  		outputs = append(outputs, out)
   243  	case "OpJumpIfTrue": //OR
   244  		stack := vm.Stack()
   245  		out.JumpIf = true
   246  		out.IfTrue = true
   247  		out.StrConditionResult = fmt.Sprintf("%v", stack[0])
   248  
   249  		if val, ok := stack[0].(bool); ok {
   250  			out.ConditionResult = new(bool)
   251  			*out.ConditionResult = val
   252  		}
   253  		outputs = append(outputs, out)
   254  	case "OpJumpIfFalse": //AND
   255  		stack := vm.Stack()
   256  		out.JumpIf = true
   257  		out.IfFalse = true
   258  		out.StrConditionResult = fmt.Sprintf("%v", stack[0])
   259  		if val, ok := stack[0].(bool); ok {
   260  			out.ConditionResult = new(bool)
   261  			*out.ConditionResult = val
   262  		}
   263  		outputs = append(outputs, out)
   264  	case "OpCall1": //Op for function calls
   265  		out.Func = true
   266  		out.FuncName = parts[3]
   267  		stack := vm.Stack()
   268  		num_items := 1
   269  		for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
   270  			out.Args = append(out.Args, autoQuote(stack[i]))
   271  			num_items--
   272  		}
   273  		outputs = append(outputs, out)
   274  	case "OpCall2": //Op for function calls
   275  		out.Func = true
   276  		out.FuncName = parts[3]
   277  		stack := vm.Stack()
   278  		num_items := 2
   279  		for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
   280  			out.Args = append(out.Args, autoQuote(stack[i]))
   281  			num_items--
   282  		}
   283  		outputs = append(outputs, out)
   284  	case "OpCall3": //Op for function calls
   285  		out.Func = true
   286  		out.FuncName = parts[3]
   287  		stack := vm.Stack()
   288  		num_items := 3
   289  		for i := len(stack) - 1; i >= 0 && num_items > 0; i-- {
   290  			out.Args = append(out.Args, autoQuote(stack[i]))
   291  			num_items--
   292  		}
   293  		outputs = append(outputs, out)
   294  	//double check OpCallFast and OpCallTyped
   295  	case "OpCallFast", "OpCallTyped":
   296  		//
   297  	case "OpCallN": //Op for function calls with more than 3 args
   298  		out.Func = true
   299  		out.FuncName = parts[1]
   300  		stack := vm.Stack()
   301  
   302  		//for OpCallN, we get the number of args
   303  		if len(program.Arguments) >= ip {
   304  			nb_args := program.Arguments[ip]
   305  			if nb_args > 0 {
   306  				//we need to skip the top item on stack
   307  				for i := len(stack) - 2; i >= 0 && nb_args > 0; i-- {
   308  					out.Args = append(out.Args, autoQuote(stack[i]))
   309  					nb_args--
   310  				}
   311  			}
   312  		} else { //let's blindly take the items on stack
   313  			for _, val := range vm.Stack() {
   314  				out.Args = append(out.Args, autoQuote(val))
   315  			}
   316  		}
   317  		outputs = append(outputs, out)
   318  	case "OpEqualString", "OpEqual", "OpEqualInt": //comparisons
   319  		stack := vm.Stack()
   320  		out.Comparison = true
   321  		out.Left = autoQuote(stack[0])
   322  		out.Right = autoQuote(stack[1])
   323  		outputs = append(outputs, out)
   324  	case "OpIn": //in operator
   325  		stack := vm.Stack()
   326  		out.Condition = true
   327  		out.ConditionIn = true
   328  		//seems that we tend to receive stack[1] as a map.
   329  		//it is tempting to use reflect to extract keys, but we end up with an array that doesn't match the initial order
   330  		//(because of the random order of the map)
   331  		out.Args = append(out.Args, autoQuote(stack[0]))
   332  		out.Args = append(out.Args, autoQuote(stack[1]))
   333  		outputs = append(outputs, out)
   334  	case "OpContains": //kind OpIn , but reverse
   335  		stack := vm.Stack()
   336  		out.Condition = true
   337  		out.ConditionContains = true
   338  		//seems that we tend to receive stack[1] as a map.
   339  		//it is tempting to use reflect to extract keys, but we end up with an array that doesn't match the initial order
   340  		//(because of the random order of the map)
   341  		out.Args = append(out.Args, autoQuote(stack[0]))
   342  		out.Args = append(out.Args, autoQuote(stack[1]))
   343  		outputs = append(outputs, out)
   344  	}
   345  	return outputs, nil
   346  }
   347  
   348  func (erp ExprRuntimeDebug) ipSeek(ip int) []string {
   349  	for i := 0; i < len(erp.Lines); i++ {
   350  		parts := strings.Split(erp.Lines[i], "\t")
   351  		if parts[0] == strconv.Itoa(ip) {
   352  			return parts
   353  		}
   354  	}
   355  	return nil
   356  }
   357  
   358  func Run(program *vm.Program, env interface{}, logger *log.Entry, debug bool) (any, error) {
   359  	if debug {
   360  		dbgInfo, ret, err := RunWithDebug(program, env, logger)
   361  		DisplayExprDebug(program, dbgInfo, logger, ret)
   362  		return ret, err
   363  	}
   364  	return expr.Run(program, env)
   365  }
   366  
   367  func cleanTextForDebug(text string) string {
   368  	text = strings.Join(strings.Fields(text), " ")
   369  	text = strings.Trim(text, " \t\n")
   370  	return text
   371  }
   372  
   373  func DisplayExprDebug(program *vm.Program, outputs []OpOutput, logger *log.Entry, ret any) {
   374  	logger.Debugf("dbg(result=%v): %s", ret, cleanTextForDebug(program.Source.Content()))
   375  	for _, output := range outputs {
   376  		logger.Debugf("%s", output.String())
   377  	}
   378  }
   379  
   380  // TBD: Based on the level of the logger (ie. trace vs debug) we could decide to add more low level instructions (pop, push, etc.)
   381  func RunWithDebug(program *vm.Program, env interface{}, logger *log.Entry) ([]OpOutput, any, error) {
   382  
   383  	var outputs []OpOutput = []OpOutput{}
   384  	var buf strings.Builder
   385  	var erp ExprRuntimeDebug = ExprRuntimeDebug{
   386  		Logger: logger,
   387  	}
   388  	var debugErr chan error = make(chan error)
   389  	vm := vm.Debug()
   390  	done := false
   391  	program.Opcodes(&buf)
   392  	lines := strings.Split(buf.String(), "\n")
   393  	erp.Lines = lines
   394  
   395  	go func() {
   396  		var err error
   397  		erp.Logger.Tracef("[START] ip 0")
   398  		ops := erp.ipSeek(0)
   399  		if ops == nil {
   400  			debugErr <- fmt.Errorf("failed getting ops for ip 0")
   401  			return
   402  		}
   403  		if outputs, err = erp.ipDebug(0, vm, program, ops, outputs); err != nil {
   404  			debugErr <- fmt.Errorf("error while debugging at ip 0")
   405  		}
   406  		vm.Step()
   407  		for ip := range vm.Position() {
   408  			ops := erp.ipSeek(ip)
   409  			if ops == nil { //we reached the end of the program, we shouldn't throw an error
   410  				erp.Logger.Tracef("[DONE] ip %d", ip)
   411  				debugErr <- nil
   412  				return
   413  			}
   414  			if outputs, err = erp.ipDebug(ip, vm, program, ops, outputs); err != nil {
   415  				debugErr <- fmt.Errorf("error while debugging at ip %d", ip)
   416  				return
   417  			}
   418  			if done {
   419  				debugErr <- nil
   420  				return
   421  			}
   422  			vm.Step()
   423  		}
   424  		debugErr <- nil
   425  	}()
   426  
   427  	var return_error error
   428  	ret, err := vm.Run(program, env)
   429  	done = true
   430  	//if the expr runtime failed, we don't need to wait for the debug to finish
   431  	if err != nil {
   432  		return_error = err
   433  	} else {
   434  		err = <-debugErr
   435  		if err != nil {
   436  			log.Warningf("error while debugging expr: %s", err)
   437  		}
   438  	}
   439  	//the overall result of expression is the result of last op ?
   440  	if len(outputs) > 0 {
   441  		lastOutIdx := len(outputs)
   442  		if lastOutIdx > 0 {
   443  			lastOutIdx -= 1
   444  		}
   445  		switch val := ret.(type) {
   446  		case bool:
   447  			log.Tracef("completing with bool %t", ret)
   448  			//if outputs[lastOutIdx].Comparison {
   449  			outputs[lastOutIdx].StrConditionResult = fmt.Sprintf("%v", ret)
   450  			outputs[lastOutIdx].ConditionResult = new(bool)
   451  			*outputs[lastOutIdx].ConditionResult = val
   452  			outputs[lastOutIdx].Finalized = true
   453  		default:
   454  			log.Tracef("completing with type %T -> %v", ret, ret)
   455  			outputs[lastOutIdx].StrConditionResult = fmt.Sprintf("%v", ret)
   456  			outputs[lastOutIdx].Finalized = true
   457  		}
   458  	} else {
   459  		log.Tracef("no output from expr runtime")
   460  	}
   461  	return outputs, ret, return_error
   462  }