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 }