mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-07 04:58:03 +02:00
187 lines
5.5 KiB
Go
187 lines
5.5 KiB
Go
package workflow
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var expressionOps = []string{">=", "<=", "==", "!=", " contains ", " matches ", ">", "<"}
|
|
var jsonFuncRe = regexp.MustCompile(`^(jsonpath|jq)\((.*),\s*(['"][^'"]+['"])\)$`)
|
|
var jsonFuncFindRe = regexp.MustCompile(`(jsonpath|jq)\([^)]*\)`)
|
|
var singleTemplateVarRe = regexp.MustCompile(`^\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}$`)
|
|
|
|
func validateConditionExpression(expr string) error {
|
|
expr = strings.TrimSpace(expr)
|
|
if expr == "" {
|
|
return fmt.Errorf("条件表达式不能为空")
|
|
}
|
|
for _, part := range splitBoolExpr(expr, "||") {
|
|
for _, atom := range splitBoolExpr(part, "&&") {
|
|
if err := validateConditionAtom(atom); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateConditionAtom(expr string) error {
|
|
expr = strings.TrimSpace(expr)
|
|
if expr == "" {
|
|
return fmt.Errorf("条件表达式存在空片段")
|
|
}
|
|
if strings.Count(expr, "{{") != strings.Count(expr, "}}") {
|
|
return fmt.Errorf("条件表达式模板括号不匹配: %s", expr)
|
|
}
|
|
if err := validateJSONFunctions(expr); err != nil {
|
|
return err
|
|
}
|
|
if left, right, ok := splitExpressionAtom(expr, " matches "); ok {
|
|
if strings.TrimSpace(left) == "" || strings.TrimSpace(right) == "" {
|
|
return fmt.Errorf("matches 表达式两侧不能为空: %s", expr)
|
|
}
|
|
pattern := cleanComparable(resolveStaticTemplate(right))
|
|
if _, err := regexp.Compile(pattern); err != nil {
|
|
return fmt.Errorf("matches 正则非法: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
for _, op := range expressionOps {
|
|
if op == " matches " {
|
|
continue
|
|
}
|
|
if left, right, ok := splitExpressionAtom(expr, op); ok {
|
|
if strings.TrimSpace(left) == "" || strings.TrimSpace(right) == "" {
|
|
return fmt.Errorf("表达式 %q 两侧不能为空: %s", strings.TrimSpace(op), expr)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func evalCondition(expr string, state *WorkflowLocalState) bool {
|
|
expr = strings.TrimSpace(expr)
|
|
if expr == "" {
|
|
return true
|
|
}
|
|
orParts := splitBoolExpr(expr, "||")
|
|
for _, orPart := range orParts {
|
|
andOK := true
|
|
for _, atom := range splitBoolExpr(orPart, "&&") {
|
|
if !evalConditionAtom(atom, state) {
|
|
andOK = false
|
|
break
|
|
}
|
|
}
|
|
if andOK {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func evalConditionAtom(expr string, state *WorkflowLocalState) bool {
|
|
expr = strings.TrimSpace(expr)
|
|
for _, op := range expressionOps {
|
|
if left, right, ok := splitExpressionAtom(expr, op); ok {
|
|
left = strings.TrimSpace(fmt.Sprint(resolveExpressionOperand(left, state)))
|
|
right = strings.TrimSpace(fmt.Sprint(resolveExpressionOperand(right, state)))
|
|
switch strings.TrimSpace(op) {
|
|
case "==":
|
|
return cleanComparable(left) == cleanComparable(right)
|
|
case "!=":
|
|
return cleanComparable(left) != cleanComparable(right)
|
|
case ">":
|
|
return compareNumeric(left, right, func(a, b float64) bool { return a > b })
|
|
case ">=":
|
|
return compareNumeric(left, right, func(a, b float64) bool { return a >= b })
|
|
case "<":
|
|
return compareNumeric(left, right, func(a, b float64) bool { return a < b })
|
|
case "<=":
|
|
return compareNumeric(left, right, func(a, b float64) bool { return a <= b })
|
|
case "contains":
|
|
return strings.Contains(cleanComparable(left), cleanComparable(right))
|
|
case "matches":
|
|
matched, _ := regexp.MatchString(cleanComparable(right), cleanComparable(left))
|
|
return matched
|
|
}
|
|
}
|
|
}
|
|
resolved := strings.TrimSpace(fmt.Sprint(resolveExpressionOperand(expr, state)))
|
|
v := strings.ToLower(cleanComparable(resolved))
|
|
return v != "" && v != "false" && v != "0" && v != "null"
|
|
}
|
|
|
|
func splitBoolExpr(expr, sep string) []string {
|
|
parts := strings.Split(expr, sep)
|
|
out := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
if s := strings.TrimSpace(part); s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return []string{strings.TrimSpace(expr)}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func splitExpressionAtom(expr, op string) (string, string, bool) {
|
|
if strings.TrimSpace(op) == "contains" || strings.TrimSpace(op) == "matches" {
|
|
idx := strings.Index(expr, op)
|
|
if idx < 0 {
|
|
return "", "", false
|
|
}
|
|
return expr[:idx], expr[idx+len(op):], true
|
|
}
|
|
idx := strings.Index(expr, op)
|
|
if idx < 0 {
|
|
return "", "", false
|
|
}
|
|
return expr[:idx], expr[idx+len(op):], true
|
|
}
|
|
|
|
func compareNumeric(left, right string, cmp func(float64, float64) bool) bool {
|
|
a, errA := strconv.ParseFloat(cleanComparable(left), 64)
|
|
b, errB := strconv.ParseFloat(cleanComparable(right), 64)
|
|
if errA != nil || errB != nil {
|
|
return false
|
|
}
|
|
return cmp(a, b)
|
|
}
|
|
|
|
func resolveStaticTemplate(s string) string {
|
|
return templateVarRe.ReplaceAllString(s, "value")
|
|
}
|
|
|
|
func resolveExpressionOperand(raw string, state *WorkflowLocalState) any {
|
|
raw = strings.TrimSpace(raw)
|
|
if m := jsonFuncRe.FindStringSubmatch(raw); len(m) == 4 {
|
|
inputExpr := strings.TrimSpace(m[2])
|
|
path := strings.Trim(m[3], `"'`)
|
|
input := resolveExpressionOperand(inputExpr, state)
|
|
return evalJSONPathValue(input, path)
|
|
}
|
|
if m := singleTemplateVarRe.FindStringSubmatch(raw); len(m) == 2 {
|
|
return valueFromPath(m[1], state)
|
|
}
|
|
return resolveTemplate(raw, state)
|
|
}
|
|
|
|
func validateJSONFunctions(expr string) error {
|
|
for _, candidate := range jsonFuncFindRe.FindAllString(expr, -1) {
|
|
candidate = strings.TrimSpace(candidate)
|
|
m := jsonFuncRe.FindStringSubmatch(candidate)
|
|
if len(m) != 4 {
|
|
return fmt.Errorf("JSONPath/JQ 函数格式应为 jsonpath(value, \"$.path\") 或 jq(value, \".path\")")
|
|
}
|
|
if err := validateJSONPathSyntax(strings.Trim(m[3], `"'`)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|