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

     1  package exprhelpers
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"math"
     8  	"net"
     9  	"net/url"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/antonmedv/expr"
    19  	"github.com/bluele/gcache"
    20  	"github.com/c-robinson/iplib"
    21  	"github.com/cespare/xxhash/v2"
    22  	"github.com/davecgh/go-spew/spew"
    23  	"github.com/prometheus/client_golang/prometheus"
    24  	log "github.com/sirupsen/logrus"
    25  	"github.com/umahmood/haversine"
    26  	"github.com/wasilibs/go-re2"
    27  
    28  	"github.com/crowdsecurity/go-cs-lib/ptr"
    29  
    30  	"github.com/crowdsecurity/crowdsec/pkg/cache"
    31  	"github.com/crowdsecurity/crowdsec/pkg/database"
    32  	"github.com/crowdsecurity/crowdsec/pkg/fflag"
    33  	"github.com/crowdsecurity/crowdsec/pkg/types"
    34  )
    35  
    36  var dataFile map[string][]string
    37  var dataFileRegex map[string][]*regexp.Regexp
    38  var dataFileRe2 map[string][]*re2.Regexp
    39  
    40  // This is used to (optionally) cache regexp results for RegexpInFile operations
    41  var dataFileRegexCache map[string]gcache.Cache = make(map[string]gcache.Cache)
    42  
    43  /*prometheus*/
    44  var RegexpCacheMetrics = prometheus.NewGaugeVec(
    45  	prometheus.GaugeOpts{
    46  		Name: "cs_regexp_cache_size",
    47  		Help: "Entries per regexp cache.",
    48  	},
    49  	[]string{"name"},
    50  )
    51  
    52  var dbClient *database.Client
    53  
    54  var exprFunctionOptions []expr.Option
    55  
    56  var keyValuePattern = regexp.MustCompile(`(?P<key>[^=\s]+)=(?:"(?P<quoted_value>[^"\\]*(?:\\.[^"\\]*)*)"|(?P<value>[^=\s]+)|\s*)`)
    57  
    58  func GetExprOptions(ctx map[string]interface{}) []expr.Option {
    59  	if len(exprFunctionOptions) == 0 {
    60  		exprFunctionOptions = []expr.Option{}
    61  		for _, function := range exprFuncs {
    62  			exprFunctionOptions = append(exprFunctionOptions,
    63  				expr.Function(function.name,
    64  					function.function,
    65  					function.signature...,
    66  				))
    67  		}
    68  	}
    69  	ret := []expr.Option{}
    70  	ret = append(ret, exprFunctionOptions...)
    71  	ret = append(ret, expr.Env(ctx))
    72  	return ret
    73  }
    74  
    75  func Init(databaseClient *database.Client) error {
    76  	dataFile = make(map[string][]string)
    77  	dataFileRegex = make(map[string][]*regexp.Regexp)
    78  	dataFileRe2 = make(map[string][]*re2.Regexp)
    79  	dbClient = databaseClient
    80  
    81  	return nil
    82  }
    83  
    84  func RegexpCacheInit(filename string, CacheCfg types.DataSource) error {
    85  
    86  	//cache is explicitly disabled
    87  	if CacheCfg.Cache != nil && !*CacheCfg.Cache {
    88  		return nil
    89  	}
    90  	//cache is implicitly disabled if no cache config is provided
    91  	if CacheCfg.Strategy == nil && CacheCfg.TTL == nil && CacheCfg.Size == nil {
    92  		return nil
    93  	}
    94  	//cache is enabled
    95  
    96  	if CacheCfg.Size == nil {
    97  		CacheCfg.Size = ptr.Of(50)
    98  	}
    99  
   100  	gc := gcache.New(*CacheCfg.Size)
   101  
   102  	if CacheCfg.Strategy == nil {
   103  		CacheCfg.Strategy = ptr.Of("LRU")
   104  	}
   105  	switch *CacheCfg.Strategy {
   106  	case "LRU":
   107  		gc = gc.LRU()
   108  	case "LFU":
   109  		gc = gc.LFU()
   110  	case "ARC":
   111  		gc = gc.ARC()
   112  	default:
   113  		return fmt.Errorf("unknown cache strategy '%s'", *CacheCfg.Strategy)
   114  	}
   115  
   116  	if CacheCfg.TTL != nil {
   117  		gc.Expiration(*CacheCfg.TTL)
   118  	}
   119  	cache := gc.Build()
   120  	dataFileRegexCache[filename] = cache
   121  	return nil
   122  }
   123  
   124  // UpdateCacheMetrics is called directly by the prom handler
   125  func UpdateRegexpCacheMetrics() {
   126  	RegexpCacheMetrics.Reset()
   127  	for name := range dataFileRegexCache {
   128  		RegexpCacheMetrics.With(prometheus.Labels{"name": name}).Set(float64(dataFileRegexCache[name].Len(true)))
   129  	}
   130  }
   131  
   132  func FileInit(fileFolder string, filename string, fileType string) error {
   133  	log.Debugf("init (folder:%s) (file:%s) (type:%s)", fileFolder, filename, fileType)
   134  	if fileType == "" {
   135  		log.Debugf("ignored file %s%s because no type specified", fileFolder, filename)
   136  		return nil
   137  	}
   138  	ok, err := existsInFileMaps(filename, fileType)
   139  	if ok {
   140  		log.Debugf("ignored file %s%s because already loaded", fileFolder, filename)
   141  		return nil
   142  	}
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	filepath := filepath.Join(fileFolder, filename)
   148  	file, err := os.Open(filepath)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	defer file.Close()
   153  
   154  	scanner := bufio.NewScanner(file)
   155  	for scanner.Scan() {
   156  		if strings.HasPrefix(scanner.Text(), "#") { // allow comments
   157  			continue
   158  		}
   159  		if len(scanner.Text()) == 0 { //skip empty lines
   160  			continue
   161  		}
   162  		switch fileType {
   163  		case "regex", "regexp":
   164  			if fflag.Re2RegexpInfileSupport.IsEnabled() {
   165  				dataFileRe2[filename] = append(dataFileRe2[filename], re2.MustCompile(scanner.Text()))
   166  				continue
   167  			}
   168  			dataFileRegex[filename] = append(dataFileRegex[filename], regexp.MustCompile(scanner.Text()))
   169  		case "string":
   170  			dataFile[filename] = append(dataFile[filename], scanner.Text())
   171  		}
   172  	}
   173  
   174  	if err := scanner.Err(); err != nil {
   175  		return err
   176  	}
   177  	return nil
   178  }
   179  
   180  // Expr helpers
   181  
   182  func Distinct(params ...any) (any, error) {
   183  
   184  	if rt := reflect.TypeOf(params[0]).Kind(); rt != reflect.Slice && rt != reflect.Array {
   185  		return nil, nil
   186  	}
   187  	array := params[0].([]interface{})
   188  	if array == nil {
   189  		return []interface{}{}, nil
   190  	}
   191  
   192  	var exists map[any]bool = make(map[any]bool)
   193  	var ret []interface{} = make([]interface{}, 0)
   194  
   195  	for _, val := range array {
   196  		if _, ok := exists[val]; !ok {
   197  			exists[val] = true
   198  			ret = append(ret, val)
   199  		}
   200  	}
   201  	return ret, nil
   202  
   203  }
   204  
   205  func FlattenDistinct(params ...any) (any, error) {
   206  	return Distinct(flatten(nil, reflect.ValueOf(params))) //nolint:asasalint
   207  }
   208  
   209  func Flatten(params ...any) (any, error) {
   210  	return flatten(nil, reflect.ValueOf(params)), nil
   211  }
   212  
   213  func flatten(args []interface{}, v reflect.Value) []interface{} {
   214  	if v.Kind() == reflect.Interface {
   215  		v = v.Elem()
   216  	}
   217  
   218  	if v.Kind() == reflect.Array || v.Kind() == reflect.Slice {
   219  		for i := 0; i < v.Len(); i++ {
   220  			args = flatten(args, v.Index(i))
   221  		}
   222  	} else {
   223  		args = append(args, v.Interface())
   224  	}
   225  
   226  	return args
   227  }
   228  func existsInFileMaps(filename string, ftype string) (bool, error) {
   229  	ok := false
   230  	var err error
   231  	switch ftype {
   232  	case "regex", "regexp":
   233  		if fflag.Re2RegexpInfileSupport.IsEnabled() {
   234  			_, ok = dataFileRe2[filename]
   235  		} else {
   236  			_, ok = dataFileRegex[filename]
   237  		}
   238  	case "string":
   239  		_, ok = dataFile[filename]
   240  	default:
   241  		err = fmt.Errorf("unknown data type '%s' for : '%s'", ftype, filename)
   242  	}
   243  	return ok, err
   244  }
   245  
   246  //Expr helpers
   247  
   248  // func Get(arr []string, index int) string {
   249  func Get(params ...any) (any, error) {
   250  	arr := params[0].([]string)
   251  	index := params[1].(int)
   252  	if index >= len(arr) {
   253  		return "", nil
   254  	}
   255  	return arr[index], nil
   256  }
   257  
   258  // func Atof(x string) float64 {
   259  func Atof(params ...any) (any, error) {
   260  	x := params[0].(string)
   261  	log.Debugf("debug atof %s", x)
   262  	ret, err := strconv.ParseFloat(x, 64)
   263  	if err != nil {
   264  		log.Warningf("Atof : can't convert float '%s' : %v", x, err)
   265  	}
   266  	return ret, nil
   267  }
   268  
   269  // func Upper(s string) string {
   270  func Upper(params ...any) (any, error) {
   271  	s := params[0].(string)
   272  	return strings.ToUpper(s), nil
   273  }
   274  
   275  // func Lower(s string) string {
   276  func Lower(params ...any) (any, error) {
   277  	s := params[0].(string)
   278  	return strings.ToLower(s), nil
   279  }
   280  
   281  // func Distance(lat1 string, long1 string, lat2 string, long2 string) (float64, error) {
   282  func Distance(params ...any) (any, error) {
   283  	lat1 := params[0].(string)
   284  	long1 := params[1].(string)
   285  	lat2 := params[2].(string)
   286  	long2 := params[3].(string)
   287  	lat1f, err := strconv.ParseFloat(lat1, 64)
   288  	if err != nil {
   289  		log.Warningf("lat1 is not a float : %v", err)
   290  		return 0.0, fmt.Errorf("lat1 is not a float : %v", err)
   291  	}
   292  	long1f, err := strconv.ParseFloat(long1, 64)
   293  	if err != nil {
   294  		log.Warningf("long1 is not a float : %v", err)
   295  		return 0.0, fmt.Errorf("long1 is not a float : %v", err)
   296  	}
   297  	lat2f, err := strconv.ParseFloat(lat2, 64)
   298  	if err != nil {
   299  		log.Warningf("lat2 is not a float : %v", err)
   300  
   301  		return 0.0, fmt.Errorf("lat2 is not a float : %v", err)
   302  	}
   303  	long2f, err := strconv.ParseFloat(long2, 64)
   304  	if err != nil {
   305  		log.Warningf("long2 is not a float : %v", err)
   306  
   307  		return 0.0, fmt.Errorf("long2 is not a float : %v", err)
   308  	}
   309  
   310  	//either set of coordinates is 0,0, return 0 to avoid FPs
   311  	if (lat1f == 0.0 && long1f == 0.0) || (lat2f == 0.0 && long2f == 0.0) {
   312  		log.Warningf("one of the coordinates is 0,0, returning 0")
   313  		return 0.0, nil
   314  	}
   315  
   316  	first := haversine.Coord{Lat: lat1f, Lon: long1f}
   317  	second := haversine.Coord{Lat: lat2f, Lon: long2f}
   318  
   319  	_, km := haversine.Distance(first, second)
   320  	return km, nil
   321  }
   322  
   323  // func QueryEscape(s string) string {
   324  func QueryEscape(params ...any) (any, error) {
   325  	s := params[0].(string)
   326  	return url.QueryEscape(s), nil
   327  }
   328  
   329  // func PathEscape(s string) string {
   330  func PathEscape(params ...any) (any, error) {
   331  	s := params[0].(string)
   332  	return url.PathEscape(s), nil
   333  }
   334  
   335  // func PathUnescape(s string) string {
   336  func PathUnescape(params ...any) (any, error) {
   337  	s := params[0].(string)
   338  	ret, err := url.PathUnescape(s)
   339  	if err != nil {
   340  		log.Debugf("unable to PathUnescape '%s': %+v", s, err)
   341  		return s, nil
   342  	}
   343  	return ret, nil
   344  }
   345  
   346  // func QueryUnescape(s string) string {
   347  func QueryUnescape(params ...any) (any, error) {
   348  	s := params[0].(string)
   349  	ret, err := url.QueryUnescape(s)
   350  	if err != nil {
   351  		log.Debugf("unable to QueryUnescape '%s': %+v", s, err)
   352  		return s, nil
   353  	}
   354  	return ret, nil
   355  }
   356  
   357  // func File(filename string) []string {
   358  func File(params ...any) (any, error) {
   359  	filename := params[0].(string)
   360  	if _, ok := dataFile[filename]; ok {
   361  		return dataFile[filename], nil
   362  	}
   363  	log.Errorf("file '%s' (type:string) not found in expr library", filename)
   364  	log.Errorf("expr library : %s", spew.Sdump(dataFile))
   365  	return []string{}, nil
   366  }
   367  
   368  // func RegexpInFile(data string, filename string) bool {
   369  func RegexpInFile(params ...any) (any, error) {
   370  	data := params[0].(string)
   371  	filename := params[1].(string)
   372  	var hash uint64
   373  	hasCache := false
   374  	matched := false
   375  
   376  	if _, ok := dataFileRegexCache[filename]; ok {
   377  		hasCache = true
   378  		hash = xxhash.Sum64String(data)
   379  		if val, err := dataFileRegexCache[filename].Get(hash); err == nil {
   380  			return val.(bool), nil
   381  		}
   382  	}
   383  
   384  	switch fflag.Re2RegexpInfileSupport.IsEnabled() {
   385  	case true:
   386  		if _, ok := dataFileRe2[filename]; ok {
   387  			for _, re := range dataFileRe2[filename] {
   388  				if re.MatchString(data) {
   389  					matched = true
   390  					break
   391  				}
   392  			}
   393  		} else {
   394  			log.Errorf("file '%s' (type:regexp) not found in expr library", filename)
   395  			log.Errorf("expr library : %s", spew.Sdump(dataFileRe2))
   396  		}
   397  	case false:
   398  		if _, ok := dataFileRegex[filename]; ok {
   399  			for _, re := range dataFileRegex[filename] {
   400  				if re.MatchString(data) {
   401  					matched = true
   402  					break
   403  				}
   404  			}
   405  		} else {
   406  			log.Errorf("file '%s' (type:regexp) not found in expr library", filename)
   407  			log.Errorf("expr library : %s", spew.Sdump(dataFileRegex))
   408  		}
   409  	}
   410  	if hasCache {
   411  		dataFileRegexCache[filename].Set(hash, matched)
   412  	}
   413  	return matched, nil
   414  }
   415  
   416  // func IpInRange(ip string, ipRange string) bool {
   417  func IpInRange(params ...any) (any, error) {
   418  	var err error
   419  	var ipParsed net.IP
   420  	var ipRangeParsed *net.IPNet
   421  
   422  	ip := params[0].(string)
   423  	ipRange := params[1].(string)
   424  
   425  	ipParsed = net.ParseIP(ip)
   426  	if ipParsed == nil {
   427  		log.Debugf("'%s' is not a valid IP", ip)
   428  		return false, nil
   429  	}
   430  	if _, ipRangeParsed, err = net.ParseCIDR(ipRange); err != nil {
   431  		log.Debugf("'%s' is not a valid IP Range", ipRange)
   432  		return false, nil //nolint:nilerr // This helper did not return an error before the move to expr.Function, we keep this behavior for backward compatibility
   433  	}
   434  	if ipRangeParsed.Contains(ipParsed) {
   435  		return true, nil
   436  	}
   437  	return false, nil
   438  }
   439  
   440  // func IsIPV6(ip string) bool {
   441  func IsIPV6(params ...any) (any, error) {
   442  	ip := params[0].(string)
   443  	ipParsed := net.ParseIP(ip)
   444  	if ipParsed == nil {
   445  		log.Debugf("'%s' is not a valid IP", ip)
   446  		return false, nil
   447  	}
   448  
   449  	// If it's a valid IP and can't be converted to IPv4 then it is an IPv6
   450  	return ipParsed.To4() == nil, nil
   451  }
   452  
   453  // func IsIPV4(ip string) bool {
   454  func IsIPV4(params ...any) (any, error) {
   455  	ip := params[0].(string)
   456  	ipParsed := net.ParseIP(ip)
   457  	if ipParsed == nil {
   458  		log.Debugf("'%s' is not a valid IP", ip)
   459  		return false, nil
   460  	}
   461  	return ipParsed.To4() != nil, nil
   462  }
   463  
   464  // func IsIP(ip string) bool {
   465  func IsIP(params ...any) (any, error) {
   466  	ip := params[0].(string)
   467  	ipParsed := net.ParseIP(ip)
   468  	if ipParsed == nil {
   469  		log.Debugf("'%s' is not a valid IP", ip)
   470  		return false, nil
   471  	}
   472  	return true, nil
   473  }
   474  
   475  // func IpToRange(ip string, cidr string) string {
   476  func IpToRange(params ...any) (any, error) {
   477  	ip := params[0].(string)
   478  	cidr := params[1].(string)
   479  	cidr = strings.TrimPrefix(cidr, "/")
   480  	mask, err := strconv.Atoi(cidr)
   481  	if err != nil {
   482  		log.Errorf("bad cidr '%s': %s", cidr, err)
   483  		return "", nil
   484  	}
   485  
   486  	ipAddr := net.ParseIP(ip)
   487  	if ipAddr == nil {
   488  		log.Errorf("can't parse IP address '%s'", ip)
   489  		return "", nil
   490  	}
   491  	ipRange := iplib.NewNet(ipAddr, mask)
   492  	if ipRange.IP() == nil {
   493  		log.Errorf("can't get cidr '%s' of '%s'", cidr, ip)
   494  		return "", nil
   495  	}
   496  	return ipRange.String(), nil
   497  }
   498  
   499  // func TimeNow() string {
   500  func TimeNow(params ...any) (any, error) {
   501  	return time.Now().UTC().Format(time.RFC3339), nil
   502  }
   503  
   504  // func ParseUri(uri string) map[string][]string {
   505  func ParseUri(params ...any) (any, error) {
   506  	uri := params[0].(string)
   507  	ret := make(map[string][]string)
   508  	u, err := url.Parse(uri)
   509  	if err != nil {
   510  		log.Errorf("Could not parse URI: %s", err)
   511  		return ret, nil
   512  	}
   513  	parsed, err := url.ParseQuery(u.RawQuery)
   514  	if err != nil {
   515  		log.Errorf("Could not parse query uri : %s", err)
   516  		return ret, nil
   517  	}
   518  	for k, v := range parsed {
   519  		ret[k] = v
   520  	}
   521  	return ret, nil
   522  }
   523  
   524  // func KeyExists(key string, dict map[string]interface{}) bool {
   525  func KeyExists(params ...any) (any, error) {
   526  	key := params[0].(string)
   527  	dict := params[1].(map[string]interface{})
   528  	_, ok := dict[key]
   529  	return ok, nil
   530  }
   531  
   532  // func GetDecisionsCount(value string) int {
   533  func GetDecisionsCount(params ...any) (any, error) {
   534  	value := params[0].(string)
   535  	if dbClient == nil {
   536  		log.Error("No database config to call GetDecisionsCount()")
   537  		return 0, nil
   538  
   539  	}
   540  	count, err := dbClient.CountDecisionsByValue(value)
   541  	if err != nil {
   542  		log.Errorf("Failed to get decisions count from value '%s'", value)
   543  		return 0, nil //nolint:nilerr // This helper did not return an error before the move to expr.Function, we keep this behavior for backward compatibility
   544  	}
   545  	return count, nil
   546  }
   547  
   548  // func GetDecisionsSinceCount(value string, since string) int {
   549  func GetDecisionsSinceCount(params ...any) (any, error) {
   550  	value := params[0].(string)
   551  	since := params[1].(string)
   552  	if dbClient == nil {
   553  		log.Error("No database config to call GetDecisionsCount()")
   554  		return 0, nil
   555  	}
   556  	sinceDuration, err := time.ParseDuration(since)
   557  	if err != nil {
   558  		log.Errorf("Failed to parse since parameter '%s' : %s", since, err)
   559  		return 0, nil
   560  	}
   561  	sinceTime := time.Now().UTC().Add(-sinceDuration)
   562  	count, err := dbClient.CountDecisionsSinceByValue(value, sinceTime)
   563  	if err != nil {
   564  		log.Errorf("Failed to get decisions count from value '%s'", value)
   565  		return 0, nil //nolint:nilerr // This helper did not return an error before the move to expr.Function, we keep this behavior for backward compatibility
   566  	}
   567  	return count, nil
   568  }
   569  
   570  // func LookupHost(value string) []string {
   571  func LookupHost(params ...any) (any, error) {
   572  	value := params[0].(string)
   573  	addresses, err := net.LookupHost(value)
   574  	if err != nil {
   575  		log.Errorf("Failed to lookup host '%s' : %s", value, err)
   576  		return []string{}, nil
   577  	}
   578  	return addresses, nil
   579  }
   580  
   581  // func ParseUnixTime(value string) (time.Time, error) {
   582  func ParseUnixTime(params ...any) (any, error) {
   583  	value := params[0].(string)
   584  	//Splitting string here as some unix timestamp may have milliseconds and break ParseInt
   585  	i, err := strconv.ParseInt(strings.Split(value, ".")[0], 10, 64)
   586  	if err != nil || i <= 0 {
   587  		return time.Time{}, fmt.Errorf("unable to parse %s as unix timestamp", value)
   588  	}
   589  	return time.Unix(i, 0), nil
   590  }
   591  
   592  // func ParseUnix(value string) string {
   593  func ParseUnix(params ...any) (any, error) {
   594  	value := params[0].(string)
   595  	t, err := ParseUnixTime(value)
   596  	if err != nil {
   597  		log.Error(err)
   598  		return "", nil
   599  	}
   600  	return t.(time.Time).Format(time.RFC3339), nil
   601  }
   602  
   603  // func ToString(value interface{}) string {
   604  func ToString(params ...any) (any, error) {
   605  	value := params[0]
   606  	s, ok := value.(string)
   607  	if !ok {
   608  		return "", nil
   609  	}
   610  	return s, nil
   611  }
   612  
   613  // func GetFromStash(cacheName string, key string) (string, error) {
   614  func GetFromStash(params ...any) (any, error) {
   615  	cacheName := params[0].(string)
   616  	key := params[1].(string)
   617  	return cache.GetKey(cacheName, key)
   618  }
   619  
   620  // func SetInStash(cacheName string, key string, value string, expiration *time.Duration) any {
   621  func SetInStash(params ...any) (any, error) {
   622  	cacheName := params[0].(string)
   623  	key := params[1].(string)
   624  	value := params[2].(string)
   625  	expiration := params[3].(*time.Duration)
   626  	return cache.SetKey(cacheName, key, value, expiration), nil
   627  }
   628  
   629  func Sprintf(params ...any) (any, error) {
   630  	format := params[0].(string)
   631  	return fmt.Sprintf(format, params[1:]...), nil
   632  }
   633  
   634  // func Match(pattern, name string) bool {
   635  func Match(params ...any) (any, error) {
   636  	var matched bool
   637  
   638  	pattern := params[0].(string)
   639  	name := params[1].(string)
   640  
   641  	if pattern == "" {
   642  		return name == "", nil
   643  	}
   644  	if name == "" {
   645  		if pattern == "*" || pattern == "" {
   646  			return true, nil
   647  		}
   648  		return false, nil
   649  	}
   650  	if pattern[0] == '*' {
   651  		for i := 0; i <= len(name); i++ {
   652  			matched, _ := Match(pattern[1:], name[i:])
   653  			if matched.(bool) {
   654  				return matched, nil
   655  			}
   656  		}
   657  		return matched, nil
   658  	}
   659  	if pattern[0] == '?' || pattern[0] == name[0] {
   660  		return Match(pattern[1:], name[1:])
   661  	}
   662  	return matched, nil
   663  }
   664  
   665  func FloatApproxEqual(params ...any) (any, error) {
   666  	float1 := params[0].(float64)
   667  	float2 := params[1].(float64)
   668  
   669  	if math.Abs(float1-float2) < 1e-6 {
   670  		return true, nil
   671  	}
   672  	return false, nil
   673  }
   674  
   675  func B64Decode(params ...any) (any, error) {
   676  	encoded := params[0].(string)
   677  	decoded, err := base64.StdEncoding.DecodeString(encoded)
   678  	if err != nil {
   679  		return "", err
   680  	}
   681  	return string(decoded), nil
   682  }
   683  
   684  func ParseKV(params ...any) (any, error) {
   685  
   686  	blob := params[0].(string)
   687  	target := params[1].(map[string]interface{})
   688  	prefix := params[2].(string)
   689  
   690  	matches := keyValuePattern.FindAllStringSubmatch(blob, -1)
   691  	if matches == nil {
   692  		log.Errorf("could not find any key/value pair in line")
   693  		return nil, fmt.Errorf("invalid input format")
   694  	}
   695  	if _, ok := target[prefix]; !ok {
   696  		target[prefix] = make(map[string]string)
   697  	} else {
   698  		_, ok := target[prefix].(map[string]string)
   699  		if !ok {
   700  			log.Errorf("ParseKV: target is not a map[string]string")
   701  			return nil, fmt.Errorf("target is not a map[string]string")
   702  		}
   703  	}
   704  	for _, match := range matches {
   705  		key := ""
   706  		value := ""
   707  		for i, name := range keyValuePattern.SubexpNames() {
   708  			if name == "key" {
   709  				key = match[i]
   710  			} else if name == "quoted_value" && match[i] != "" {
   711  				value = match[i]
   712  			} else if name == "value" && match[i] != "" {
   713  				value = match[i]
   714  			}
   715  		}
   716  		target[prefix].(map[string]string)[key] = value
   717  	}
   718  	log.Tracef("unmarshaled KV: %+v", target[prefix])
   719  	return nil, nil
   720  }
   721  
   722  func Hostname(params ...any) (any, error) {
   723  	hostname, err := os.Hostname()
   724  	if err != nil {
   725  		return "", err
   726  	}
   727  	return hostname, nil
   728  }