Files
akvorado/inlet/core/classifier.go
Vincent Bernat b5c9774c7e inlet/core: rename addCurrentClassification to withClassificationPatcher
This matches the naming convention of such structs.
2025-05-26 08:00:50 +02:00

408 lines
12 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package core
import (
"fmt"
"reflect"
"regexp"
"strings"
"sync"
"akvorado/common/schema"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/vm"
)
// Global cache for regular expressions. No boundary.
var (
regexCacheLock sync.RWMutex
regexCache = make(map[string]*regexp.Regexp)
)
type classifierContextKey string
// ExporterClassifierRule defines a classification rule for a exporter.
type ExporterClassifierRule struct {
program *vm.Program
}
// exporterInfo contains the information we want to expose about a exporter.
type exporterInfo struct {
IP string
Name string
}
// exporterClassification contains the information about an exporter classification
type exporterClassification struct {
Group string
Role string
Site string
Region string
Tenant string
Reject bool
}
// exporterClassifierEnvironment defines the environment used by the exporter classifier
type exporterClassifierEnvironment struct {
Exporter exporterInfo
CurrentClassification *exporterClassification
}
// exec executes the exporter classifier with the provided exporter.
func (scr *ExporterClassifierRule) exec(si exporterInfo, ec *exporterClassification) error {
env := exporterClassifierEnvironment{
Exporter: si,
CurrentClassification: ec,
}
if _, err := expr.Run(scr.program, env); err != nil {
return fmt.Errorf("unable to execute classifier %q: %w", scr, err)
}
return nil
}
// UnmarshalText compiles a classification rule for a exporter.
func (scr *ExporterClassifierRule) UnmarshalText(text []byte) error {
regexValidator := regexValidator{}
withClassificationPatcher := withClassificationPatcher{}
options := []expr.Option{
expr.Env(exporterClassifierEnvironment{}),
expr.WithContext("Context"),
expr.AsBool(),
expr.Patch(&withClassificationPatcher),
expr.Patch(&regexValidator),
expr.Function(
"Format",
func(params ...any) (any, error) {
return fmt.Sprintf(params[0].(string), params[1:]...), nil
},
new(func(string, ...any) string),
),
expr.Function(
"Reject",
func(params ...any) (any, error) {
ec := params[0].(*exporterClassification)
ec.Reject = true
return false, nil
},
new(func(*exporterClassification) bool),
),
}
options = addExporterClassifyStringFunction(options,
"Classify", func(ec *exporterClassification) *string { return &ec.Group })
options = addExporterClassifyStringFunction(options,
"ClassifyGroup", func(ec *exporterClassification) *string { return &ec.Group })
options = addExporterClassifyStringFunction(options,
"ClassifyRole", func(ec *exporterClassification) *string { return &ec.Role })
options = addExporterClassifyStringFunction(options,
"ClassifySite", func(ec *exporterClassification) *string { return &ec.Site })
options = addExporterClassifyStringFunction(options,
"ClassifyRegion", func(ec *exporterClassification) *string { return &ec.Region })
options = addExporterClassifyStringFunction(options,
"ClassifyTenant", func(ec *exporterClassification) *string { return &ec.Tenant })
program, err := expr.Compile(string(text), options...)
if err != nil {
return fmt.Errorf("cannot compile exporter classifier rule %q: %w", string(text), err)
}
if len(regexValidator.invalidRegexes) > 0 {
return fmt.Errorf("invalid regular expression %q", regexValidator.invalidRegexes[0])
}
scr.program = program
return nil
}
// String turns a exporter classifier rule into a string
func (scr ExporterClassifierRule) String() string {
return scr.program.Source().String()
}
// MarshalText turns a exporter classifier rule into a string
func (scr ExporterClassifierRule) MarshalText() ([]byte, error) {
return []byte(scr.String()), nil
}
// InterfaceClassifierRule defines a classification rule for an interface.
type InterfaceClassifierRule struct {
program *vm.Program
}
// interfaceInfo contains the information we want to expose about an interface.
type interfaceInfo struct {
Index uint32
Name string
Description string
Speed uint32
VLAN uint16
}
// interfaceClassification contains the information about an interface classification
type interfaceClassification struct {
Connectivity string
Provider string
Boundary schema.InterfaceBoundary
Reject bool
Name string
Description string
}
// interfaceClassifierEnvironment defines the environment used by the interface classifier
type interfaceClassifierEnvironment struct {
Exporter exporterInfo
Interface interfaceInfo
CurrentClassification *interfaceClassification
}
// exec executes the exporter classifier with the provided interface.
func (scr *InterfaceClassifierRule) exec(si exporterInfo, ii interfaceInfo, ic *interfaceClassification) error {
env := interfaceClassifierEnvironment{
Exporter: si,
Interface: ii,
CurrentClassification: ic,
}
if _, err := expr.Run(scr.program, env); err != nil {
return fmt.Errorf("unable to execute classifier %q: %w", scr, err)
}
return nil
}
// UnmarshalText compiles a classification rule for an interface.
func (scr *InterfaceClassifierRule) UnmarshalText(text []byte) error {
regexValidator := regexValidator{}
withClassificationPatcher := withClassificationPatcher{}
options := []expr.Option{
expr.Env(interfaceClassifierEnvironment{}),
expr.WithContext("Context"),
expr.AsBool(),
expr.Patch(&withClassificationPatcher),
expr.Patch(&regexValidator),
expr.Function(
"Format",
func(params ...any) (any, error) {
return fmt.Sprintf(params[0].(string), params[1:]...), nil
},
new(func(string, ...any) string),
),
expr.Function(
"Reject",
func(params ...any) (any, error) {
ic := params[0].(*interfaceClassification)
ic.Reject = true
return false, nil
},
new(func(*interfaceClassification) bool),
),
expr.Function(
"ClassifyExternal",
func(params ...any) (any, error) {
ic := params[0].(*interfaceClassification)
if ic.Boundary == schema.InterfaceBoundaryUndefined {
ic.Boundary = schema.InterfaceBoundaryExternal
}
return true, nil
},
new(func(*interfaceClassification) bool),
),
expr.Function(
"ClassifyInternal",
func(params ...any) (any, error) {
ic := params[0].(*interfaceClassification)
if ic.Boundary == schema.InterfaceBoundaryUndefined {
ic.Boundary = schema.InterfaceBoundaryInternal
}
return true, nil
},
new(func(*interfaceClassification) bool),
),
expr.Function(
"SetName",
func(params ...any) (any, error) {
ic := params[0].(*interfaceClassification)
if ic.Name == "" {
ic.Name = params[1].(string)
}
return true, nil
},
new(func(*interfaceClassification, string) bool),
),
expr.Function(
"SetDescription",
func(params ...any) (any, error) {
ic := params[0].(*interfaceClassification)
if ic.Description == "" {
ic.Description = params[1].(string)
}
return true, nil
},
new(func(*interfaceClassification, string) bool),
),
}
options = addInterfaceClassifyStringFunction(options,
"ClassifyProvider", func(ic *interfaceClassification) *string { return &ic.Provider })
options = addInterfaceClassifyStringFunction(options,
"ClassifyConnectivity", func(ic *interfaceClassification) *string { return &ic.Connectivity })
program, err := expr.Compile(string(text), options...)
if err != nil {
return fmt.Errorf("cannot compile interface classifier rule %q: %w", string(text), err)
}
if len(regexValidator.invalidRegexes) > 0 {
return fmt.Errorf("invalid regular expression %q", regexValidator.invalidRegexes[0])
}
scr.program = program
return nil
}
// String turns a interface classifier rule into a string
func (scr InterfaceClassifierRule) String() string {
return scr.program.Source().String()
}
// MarshalText turns a interface classifier rule into a string
func (scr InterfaceClassifierRule) MarshalText() ([]byte, error) {
return []byte(scr.String()), nil
}
var normalizeRegex = regexp.MustCompile("[^a-z0-9.+-]+")
// Normalize a string by putting it lowercase and only keeping safe characters
func normalize(str string) string {
return normalizeRegex.ReplaceAllString(strings.ToLower(str), "")
}
// classifyString is an helper to classify from string to string
func classifyString(input string, output *string) (bool, error) {
if *output == "" {
*output = normalize(input)
}
return true, nil
}
// classifyStringWithRegex is an helper to classify from string and regex to string
func classifyStringWithRegex(input, regex, template string, output *string) (bool, error) {
if *output == "" {
// We may have several readers trying to compile the
// regex the first time. It's not really important.
regexCacheLock.RLock()
compiledRegex, ok := regexCache[regex]
regexCacheLock.RUnlock()
if !ok {
var err error
compiledRegex, err = regexp.Compile(regex)
if err != nil {
return false, fmt.Errorf("cannot compile regex %q: %w", regex, err)
}
regexCacheLock.Lock()
regexCache[regex] = compiledRegex
regexCacheLock.Unlock()
}
result := []byte{}
indexes := compiledRegex.FindSubmatchIndex([]byte(input))
if indexes == nil {
return false, nil
}
result = compiledRegex.ExpandString(result, template, input, indexes)
*output = normalize(string(result))
}
return true, nil
}
// addExporterClassifyStringFunction adds to the list of compile options two
// functions for classifying an aspect of an exporter.
func addExporterClassifyStringFunction(options []expr.Option, name string, fn func(*exporterClassification) *string) []expr.Option {
options = append(options,
expr.Function(
name,
func(params ...any) (any, error) {
ec := params[0].(*exporterClassification)
return classifyString(params[1].(string), fn(ec))
},
new(func(*exporterClassification, string) bool),
),
expr.Function(
fmt.Sprintf("%sRegex", name),
func(params ...any) (any, error) {
ec := params[0].(*exporterClassification)
return classifyStringWithRegex(params[1].(string), params[2].(string), params[3].(string), fn(ec))
},
new(func(*exporterClassification, string, string, string) (bool, error)),
),
)
return options
}
// addInterfaceClassifyStringFunction adds to the list of compile options two
// functions for classifying an aspect of an interface.
func addInterfaceClassifyStringFunction(options []expr.Option, name string, fn func(*interfaceClassification) *string) []expr.Option {
options = append(options,
expr.Function(
name,
func(params ...any) (any, error) {
ic := params[0].(*interfaceClassification)
return classifyString(params[1].(string), fn(ic))
},
new(func(*interfaceClassification, string) bool),
),
expr.Function(
fmt.Sprintf("%sRegex", name),
func(params ...any) (any, error) {
ic := params[0].(*interfaceClassification)
return classifyStringWithRegex(params[1].(string), params[2].(string), params[3].(string), fn(ic))
},
new(func(*interfaceClassification, string, string, string) (bool, error)),
),
)
return options
}
// regexValidator is a patch validate regular expressions at compile-time
type regexValidator struct {
invalidRegexes []string
}
func (r *regexValidator) Visit(node *ast.Node) {
n, ok := (*node).(*ast.CallNode)
if !ok {
return
}
identifier, ok := n.Callee.(*ast.IdentifierNode)
if !ok {
return
}
if !strings.HasSuffix(identifier.Value, "Regex") || len(n.Arguments) != 4 {
return
}
str, ok := n.Arguments[2].(*ast.StringNode)
if !ok {
return
}
if _, err := regexp.Compile(str.Value); err != nil {
r.invalidRegexes = append(r.invalidRegexes, str.Value)
}
}
// withClassificationPatcher is a patch to add the current classification as the
// first argument when the function called expects one.
type withClassificationPatcher struct{}
func (a *withClassificationPatcher) Visit(node *ast.Node) {
switch call := (*node).(type) {
case *ast.CallNode:
fn := call.Callee.Type()
if fn == nil || fn.Kind() != reflect.Func || fn.NumIn() < 1 {
return
}
if fn.In(0).String() == "*core.exporterClassification" || fn.In(0).String() == "*core.interfaceClassification" {
ast.Patch(node, &ast.CallNode{
Callee: call.Callee,
Arguments: append([]ast.Node{
&ast.IdentifierNode{Value: "CurrentClassification"},
}, call.Arguments...),
})
}
}
}