mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2026-10-12 02:12:31 +00:00
pkg/acquisition: refact configuration validation and tests (#4187)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Under windows, don't mess with line endings at least for yaml test files,
|
||||
# otherwise the reported line numbers don't match the expected errors.
|
||||
|
||||
**/testdata/**/*.yaml text eol=lf
|
||||
**/testdata/**/*.yml text eol=lf
|
||||
@@ -345,29 +345,6 @@ detect:
|
||||
datasource: {}`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for wizard: datasource configuration is empty",
|
||||
}, {
|
||||
name: "missing acquisition file name",
|
||||
config: `
|
||||
detect:
|
||||
wizard:
|
||||
acquisition_spec:
|
||||
filename: something.yaml
|
||||
datasource:
|
||||
labels:
|
||||
type: something`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for wizard: source field is required",
|
||||
}, {
|
||||
name: "source is unknown",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: wombat.yaml
|
||||
datasource:
|
||||
source: wombat`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: unknown data source wombat",
|
||||
}, {
|
||||
name: "source is misplaced",
|
||||
config: `
|
||||
@@ -379,131 +356,6 @@ detect:
|
||||
source: file`,
|
||||
want: nil,
|
||||
wantErr: "yaml: unmarshal errors:\n line 7: field source not found in type setup.AcquisitionSpec",
|
||||
}, {
|
||||
name: "source is mismatched",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: journalctl.yaml
|
||||
datasource:
|
||||
source: journalctl
|
||||
filename: /path/to/file.log`,
|
||||
want: nil,
|
||||
wantErr: `invalid acquisition spec for foobar: cannot parse: [1:1] unknown field "filename"`,
|
||||
}, {
|
||||
name: "source file: required fields",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: file.yaml
|
||||
datasource:
|
||||
source: file`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: no filename or filenames configuration provided",
|
||||
}, {
|
||||
name: "source journalctl: required fields",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: foobar.yaml
|
||||
datasource:
|
||||
source: journalctl`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: journalctl_filter is required",
|
||||
}, {
|
||||
name: "source cloudwatch: required fields",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: cloudwatch.yaml
|
||||
datasource:
|
||||
source: cloudwatch`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: group_name is mandatory for CloudwatchSource",
|
||||
}, {
|
||||
name: "source syslog: all fields are optional",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: syslog.yaml
|
||||
datasource:
|
||||
source: syslog`,
|
||||
want: &Setup{
|
||||
Plans: []ServicePlan{
|
||||
{
|
||||
Name: "foobar",
|
||||
InstallRecommendation: InstallRecommendation{
|
||||
AcquisitionSpec: AcquisitionSpec{
|
||||
Filename: "syslog.yaml",
|
||||
Datasource: DatasourceConfig{
|
||||
"source": "syslog",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "source docker: required fields",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: docker.yaml
|
||||
datasource:
|
||||
source: docker`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: no containers or services configuration provided",
|
||||
}, {
|
||||
name: "source kinesis: required fields (enhanced fanout=false)",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: kinesis.yaml
|
||||
datasource:
|
||||
source: kinesis`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: stream_name is mandatory when use_enhanced_fanout is false",
|
||||
}, {
|
||||
name: "source kinesis: required fields (enhanced fanout=true)",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: kinesis.yaml
|
||||
datasource:
|
||||
source: kinesis
|
||||
use_enhanced_fanout: true`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: stream_arn is mandatory when use_enhanced_fanout is true",
|
||||
}, {
|
||||
name: "source kafka: required fields",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: kafka.yaml
|
||||
datasource:
|
||||
source: kafka`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: cannot create a kafka reader with an empty list of broker addresses",
|
||||
}, {
|
||||
name: "source loki: required fields",
|
||||
config: `
|
||||
detect:
|
||||
foobar:
|
||||
acquisition_spec:
|
||||
filename: loki.yaml
|
||||
datasource:
|
||||
source: loki`,
|
||||
want: nil,
|
||||
wantErr: "invalid acquisition spec for foobar: loki query is mandatory",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ require (
|
||||
github.com/prometheus/common v0.66.1
|
||||
github.com/r3labs/diff/v2 v2.15.1
|
||||
github.com/sanity-io/litter v1.5.8
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
||||
github.com/segmentio/kafka-go v0.4.48
|
||||
github.com/shirou/gopsutil/v4 v4.25.8
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
|
||||
@@ -148,6 +148,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -508,6 +510,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/segmentio/kafka-go v0.4.48 h1:9jyu9CWK4W5W+SroCe8EffbrRZVqAOkuaLd/ApID4Vs=
|
||||
github.com/segmentio/kafka-go v0.4.48/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
|
||||
|
||||
+133
-62
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cenkalti/backoff/v5"
|
||||
@@ -89,6 +90,9 @@ func DataSourceConfigure(
|
||||
|
||||
if lapiClientAware, ok := dataSrc.(types.LAPIClientAware); ok {
|
||||
cConfig := csconfig.GetConfig()
|
||||
if cConfig.API == nil {
|
||||
return nil, errors.New("crowdsec configuration not loaded while initializing appsec - this is a bug, plese report")
|
||||
}
|
||||
lapiClientAware.SetClientConfig(cConfig.API.Client)
|
||||
}
|
||||
|
||||
@@ -146,7 +150,7 @@ func LoadAcquisitionFromDSN(
|
||||
subLogger := log.StandardLogger().WithField("type", labels["type"])
|
||||
|
||||
if err = dsnConf.ConfigureByDSN(ctx, dsn, labels, subLogger, uniqueID); err != nil {
|
||||
return nil, fmt.Errorf("configuring datasource for %q: %w", dsn, err)
|
||||
return nil, fmt.Errorf("datasource for %q: %w", dsn, err)
|
||||
}
|
||||
|
||||
return dataSrc, nil
|
||||
@@ -202,6 +206,115 @@ func detectType(r io.Reader) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
type ParsedSourceConfig struct {
|
||||
Common configuration.DataSourceCommonCfg
|
||||
Source types.DataSource
|
||||
Transform *vm.Program
|
||||
SourceMissing bool // the "source" field was missing, and detected
|
||||
SourceOverridden string // the "source" field was not missing, but didn't match the detected one
|
||||
}
|
||||
|
||||
var ErrEmptyYAMLDocument = errors.New("empty yaml document")
|
||||
|
||||
// ParseSourceConfig validates and configures one YAML document.
|
||||
//
|
||||
// It does not expand env variables, they must already be expanded.
|
||||
//
|
||||
// - return sentinel error for empty/comment-only documents
|
||||
// - backward-compat source auto-detection (filename/filenames/journalctl_filter)
|
||||
// - validate common fields
|
||||
// - delegate per-source config validation to the appropriate module
|
||||
// - compile transform expression
|
||||
func ParseSourceConfig(ctx context.Context, yamlDoc []byte, metricsLevel metrics.AcquisitionMetricsLevel, hub *cwhub.Hub) (*ParsedSourceConfig, error) {
|
||||
detectedType, err := detectType(bytes.NewReader(yamlDoc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if there are not keys or only comments, the document will be skipped
|
||||
empty, err := csyaml.IsEmptyYAML(bytes.NewReader(yamlDoc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if empty {
|
||||
return nil, ErrEmptyYAMLDocument
|
||||
}
|
||||
|
||||
var sub configuration.DataSourceCommonCfg
|
||||
|
||||
// can't be strict here, the doc contains specific datasource config too but we won't collect them now.
|
||||
if err = yaml.UnmarshalWithOptions(yamlDoc, &sub); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse: %w", errors.New(yaml.FormatError(err, false, false)))
|
||||
}
|
||||
|
||||
parsed := &ParsedSourceConfig{}
|
||||
|
||||
// report that the user did not specify a source
|
||||
if sub.Source == "" {
|
||||
parsed.SourceMissing = true
|
||||
}
|
||||
|
||||
// report that the user specified a source that doesn't match with one detected from the presence of other fields
|
||||
if detectedType != "" {
|
||||
if sub.Source != "" && sub.Source != detectedType {
|
||||
parsed.SourceOverridden = sub.Source
|
||||
}
|
||||
|
||||
sub.Source = detectedType
|
||||
}
|
||||
|
||||
parsed.Common = sub
|
||||
|
||||
// could not detect, alas
|
||||
if sub.Source == "" {
|
||||
return nil, errors.New("missing 'source' field")
|
||||
}
|
||||
|
||||
// pre-check that the source is valid
|
||||
_, err = registry.LookupFactory(sub.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check for labels now, an error for missing labels has lower priority
|
||||
// than missing or unknown source type
|
||||
if len(sub.Labels) == 0 && sub.Source != "docker" {
|
||||
// docker is the only source that does not require labels
|
||||
return nil, errors.New("missing labels")
|
||||
}
|
||||
|
||||
uniqueID := uuid.NewString()
|
||||
sub.UniqueId = uniqueID
|
||||
|
||||
src, err := DataSourceConfigure(ctx, sub, yamlDoc, metricsLevel, hub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("datasource of type %s: %w", sub.Source, err)
|
||||
}
|
||||
parsed.Source = src
|
||||
|
||||
if sub.TransformExpr != "" {
|
||||
vm, err := expr.Compile(sub.TransformExpr, exprhelpers.GetExprOptions(map[string]any{"evt": &pipeline.Event{}})...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while compiling transform expression '%s' for datasource %s: %w", sub.TransformExpr, sub.Source, err)
|
||||
}
|
||||
|
||||
parsed.Transform = vm
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func formatConfigLocation(acquisFile string, withPos bool, idx int) string {
|
||||
ret := acquisFile
|
||||
|
||||
if withPos {
|
||||
ret += " (position " + strconv.Itoa(idx) + ")"
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// sourcesFromFile reads and parses one acquisition file into DataSources.
|
||||
func sourcesFromFile(
|
||||
ctx context.Context,
|
||||
@@ -235,84 +348,42 @@ func sourcesFromFile(
|
||||
idx := -1
|
||||
|
||||
for _, yamlDoc := range documents {
|
||||
detectedType, err := detectType(bytes.NewReader(yamlDoc))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %w", yamlFile.Name(), err)
|
||||
}
|
||||
|
||||
idx += 1
|
||||
|
||||
var sub configuration.DataSourceCommonCfg
|
||||
loc := formatConfigLocation(acquisFile, len(documents) > 1, idx)
|
||||
|
||||
// can't be strict here, the doc contains specific datasource config too but we won't collect them now.
|
||||
if err = yaml.UnmarshalWithOptions(yamlDoc, &sub); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %w", yamlFile.Name(), errors.New(yaml.FormatError(err, false, false)))
|
||||
}
|
||||
parsed, err := ParseSourceConfig(ctx, yamlDoc, metricsLevel, hub)
|
||||
|
||||
// for backward compat ('type' was not mandatory, detect it)
|
||||
if guessType := detectedType; guessType != "" {
|
||||
log.Debugf("datasource type missing in %s (position %d): detected 'source=%s'", acquisFile, idx, guessType)
|
||||
|
||||
if sub.Source != "" && sub.Source != guessType {
|
||||
log.Warnf("datasource type mismatch in %s (position %d): found '%s' but should probably be '%s'", acquisFile, idx, sub.Source, guessType)
|
||||
// report data source detection, it can be required to understand an error
|
||||
if parsed != nil {
|
||||
if parsed.SourceMissing {
|
||||
log.Debugf("%s: datasource type missing, detected 'source=%s'", loc, parsed.Common.Source)
|
||||
}
|
||||
|
||||
sub.Source = guessType
|
||||
}
|
||||
|
||||
// it's an empty item, skip it
|
||||
|
||||
empty, err := csyaml.IsEmptyYAML(bytes.NewReader(yamlDoc))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s (position %d): %w", acquisFile, idx, err)
|
||||
}
|
||||
|
||||
if empty {
|
||||
// there are no keys or only comments, skip the document
|
||||
continue
|
||||
}
|
||||
|
||||
if len(sub.Labels) == 0 {
|
||||
if sub.Source != "docker" {
|
||||
// docker is the only source that can be empty
|
||||
return nil, fmt.Errorf("missing labels in %s (position %d)", acquisFile, idx)
|
||||
if parsed.SourceOverridden != "" {
|
||||
log.Warnf("%s: datasource type mismatch: found '%s' but should probably be '%s'", loc, parsed.SourceOverridden, parsed.Common.Source)
|
||||
}
|
||||
}
|
||||
|
||||
if sub.Source == "" {
|
||||
return nil, fmt.Errorf("missing 'source' field in %s (position %d)", acquisFile, idx)
|
||||
}
|
||||
|
||||
// pre-check that the source is valid
|
||||
_, err = registry.LookupFactory(sub.Source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in file %s (position %d) - %w", acquisFile, idx, err)
|
||||
}
|
||||
|
||||
uniqueID := uuid.NewString()
|
||||
sub.UniqueId = uniqueID
|
||||
|
||||
src, err := DataSourceConfigure(ctx, sub, yamlDoc, metricsLevel, hub)
|
||||
if err != nil {
|
||||
var dserr *DataSourceUnavailableError
|
||||
if errors.As(err, &dserr) {
|
||||
log.Error(err)
|
||||
if errors.Is(err, ErrEmptyYAMLDocument) {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("configuring datasource of type %s from %s (position %d): %w", sub.Source, acquisFile, idx, err)
|
||||
}
|
||||
|
||||
if sub.TransformExpr != "" {
|
||||
vm, err := expr.Compile(sub.TransformExpr, exprhelpers.GetExprOptions(map[string]any{"evt": &pipeline.Event{}})...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while compiling transform expression '%s' for datasource %s in %s (position %d): %w", sub.TransformExpr, sub.Source, acquisFile, idx, err)
|
||||
var dserr *DataSourceUnavailableError
|
||||
if errors.As(err, &dserr) {
|
||||
log.Error(fmt.Errorf("%s: %w", loc, err))
|
||||
continue
|
||||
}
|
||||
|
||||
transformRuntimes[uniqueID] = vm
|
||||
return nil, fmt.Errorf("%s: %w", loc, err)
|
||||
}
|
||||
|
||||
sources = append(sources, src)
|
||||
if parsed.Transform != nil {
|
||||
transformRuntimes[parsed.Common.UniqueId] = parsed.Transform
|
||||
}
|
||||
|
||||
sources = append(sources, parsed.Source)
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
|
||||
@@ -268,7 +268,7 @@ func TestLoadAcquisitionFromFiles(t *testing.T) {
|
||||
Config: csconfig.CrowdsecServiceCfg{
|
||||
AcquisitionFiles: []string{"testdata/missing_labels.yaml"},
|
||||
},
|
||||
ExpectedError: "missing labels in testdata/missing_labels.yaml",
|
||||
ExpectedError: "testdata/missing_labels.yaml: missing labels",
|
||||
},
|
||||
{
|
||||
TestName: "backward_compat",
|
||||
@@ -282,14 +282,14 @@ func TestLoadAcquisitionFromFiles(t *testing.T) {
|
||||
Config: csconfig.CrowdsecServiceCfg{
|
||||
AcquisitionFiles: []string{"testdata/bad_source.yaml"},
|
||||
},
|
||||
ExpectedError: "in file testdata/bad_source.yaml (position 0) - unknown data source does_not_exist",
|
||||
ExpectedError: "testdata/bad_source.yaml: unknown data source does_not_exist",
|
||||
},
|
||||
{
|
||||
TestName: "invalid_filetype_config",
|
||||
Config: csconfig.CrowdsecServiceCfg{
|
||||
AcquisitionFiles: []string{"testdata/bad_filetype.yaml"},
|
||||
},
|
||||
ExpectedError: "configuring datasource of type file from testdata/bad_filetype.yaml",
|
||||
ExpectedError: "testdata/bad_filetype.yaml: datasource of type file: cannot parse FileAcquisition configuration: [2:12] string was used where sequence is expected",
|
||||
},
|
||||
{
|
||||
TestName: "from_env",
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package acquisition
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/goccy/go-yaml"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
wantErrLineRE = regexp.MustCompile(`(?m)^\s*#\s*wantErr:\s*(.*?)\s*$`)
|
||||
wantSchemaErrLineRE = regexp.MustCompile(`(?m)^[ \t]*#[ \t]*schemaErr:[ \t]*([^\r\n]*)[ \t]*$`)
|
||||
)
|
||||
|
||||
func findYAMLFiles(t *testing.T, root string) []string {
|
||||
t.Helper()
|
||||
|
||||
var files []string
|
||||
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(d.Name()))
|
||||
if ext != ".yaml" && ext != ".yml" {
|
||||
return nil
|
||||
}
|
||||
files = append(files, path)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err, "walking %q", root)
|
||||
|
||||
slices.Sort(files)
|
||||
return files
|
||||
}
|
||||
|
||||
func wantErrFromYAML(t *testing.T, fileContent []byte) (want string, found bool) {
|
||||
t.Helper()
|
||||
|
||||
m := wantErrLineRE.FindSubmatch(fileContent)
|
||||
if len(m) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(m[1])), true
|
||||
}
|
||||
|
||||
func wantSchemaErrFromYAML(t *testing.T, fileContent []byte) (want string, found bool) {
|
||||
t.Helper()
|
||||
|
||||
m := wantSchemaErrLineRE.FindSubmatch(fileContent)
|
||||
if len(m) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(m[1])), true
|
||||
}
|
||||
|
||||
func TestParseSourceConfig(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
type suite struct {
|
||||
name string
|
||||
root string
|
||||
expectValid bool
|
||||
}
|
||||
|
||||
type source struct {
|
||||
Source string
|
||||
}
|
||||
|
||||
// load a configuration, appsec needs it
|
||||
_, _, err := csconfig.NewConfig("./testdata/config.yaml", false, false, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// load a hub, appsec needs it
|
||||
hub := cwhub.Hub{}
|
||||
|
||||
suites := []suite{
|
||||
{name: "valid", root: filepath.Join("testdata", "valid"), expectValid: true},
|
||||
{name: "invalid", root: filepath.Join("testdata", "invalid"), expectValid: false},
|
||||
}
|
||||
|
||||
for _, s := range suites {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
files := findYAMLFiles(t, s.root)
|
||||
require.NotEmpty(t, files, "no YAML files found under %q", s.root)
|
||||
|
||||
for _, path := range files {
|
||||
rel, _ := filepath.Rel(s.root, path)
|
||||
|
||||
fileContent, err := os.ReadFile(path)
|
||||
require.NoError(t, err, "read %q", path)
|
||||
|
||||
t.Run(filepath.ToSlash(rel), func(t *testing.T) {
|
||||
var (
|
||||
so source
|
||||
schema string
|
||||
)
|
||||
|
||||
if err = yaml.Unmarshal(fileContent, &so); err == nil {
|
||||
schema = filepath.Join("schemas", so.Source + ".yaml")
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" && strings.Contains(path, "journalctl") {
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" && strings.Contains(path, "wineventlog") {
|
||||
return
|
||||
}
|
||||
|
||||
wantErr, hasWant := wantErrFromYAML(t, fileContent)
|
||||
|
||||
wantSchemaErr, hasWantSchemaErr := wantSchemaErrFromYAML(t, fileContent)
|
||||
|
||||
if s.expectValid {
|
||||
require.False(t, hasWant, "valid config must not include # wantErr: directive")
|
||||
parsed, err := ParseSourceConfig(ctx, fileContent, metrics.AcquisitionMetricsLevelNone, &hub)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parsed)
|
||||
if schema != "" {
|
||||
err = ValidateYAML(fileContent, schema)
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
// XXX: ignore missing schema
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// invalid
|
||||
|
||||
require.True(t, hasWant, "invalid config must include '# wantErr: <exact error>'")
|
||||
require.NotEmpty(t, wantErr, "wantErr directive found but empty")
|
||||
|
||||
parsed, err := ParseSourceConfig(ctx, fileContent, metrics.AcquisitionMetricsLevelNone, &hub)
|
||||
require.Error(t, err, "got no error, expected %q", wantErr)
|
||||
require.Nil(t, parsed)
|
||||
assert.Equal(t, wantErr, err.Error())
|
||||
if schema == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// schema validation
|
||||
|
||||
err = ValidateYAML(fileContent, schema)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// XXX: ignore missing schema, for now
|
||||
return
|
||||
}
|
||||
|
||||
// a "schemaErr" comment must be present, even if empty
|
||||
require.True(t, hasWantSchemaErr, "invalid configurations require an exlicit schemaErr comment. it can be empty string if the schema cannot detect the issue")
|
||||
switch {
|
||||
case err == nil && wantSchemaErr != "":
|
||||
require.Error(t, err, "got no schema error, expected %q", wantSchemaErr)
|
||||
case err != nil && wantSchemaErr == "":
|
||||
require.Error(t, err, "got schema error %q, expected nil", err)
|
||||
case err != nil:
|
||||
assert.Contains(t, err.Error(), wantSchemaErr)
|
||||
default:
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, wantSchemaErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,10 @@ func (w *Source) Configure(_ context.Context, yamlConfig []byte, logger *log.Ent
|
||||
return fmt.Errorf("unable to parse appsec configuration: %w", err)
|
||||
}
|
||||
|
||||
if w.lapiClientConfig.Credentials == nil {
|
||||
return errors.New("missing lapi client credentials")
|
||||
}
|
||||
|
||||
w.lapiURL = fmt.Sprintf("%sv1/decisions/stream", w.lapiClientConfig.Credentials.URL)
|
||||
w.AuthCache = NewAuthCache()
|
||||
|
||||
|
||||
@@ -422,25 +422,6 @@ group_name: test_group
|
||||
stream_name: test_stream`,
|
||||
expectedStartErr: "The specified log group does not exist",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
xxx: cloudwatch
|
||||
labels:
|
||||
type: test_source
|
||||
group_name: test_group
|
||||
stream_name: test_stream`,
|
||||
expectedCfgErr: `[2:1] unknown field "xxx"`,
|
||||
},
|
||||
{
|
||||
name: "missing_group_name",
|
||||
config: `
|
||||
source: cloudwatch
|
||||
aws_region: us-east-1
|
||||
labels:
|
||||
type: test_source
|
||||
stream_name: test_stream`,
|
||||
expectedCfgErr: "group_name is mandatory for CloudwatchSource",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -30,113 +30,6 @@ import (
|
||||
const testContainerName = "docker_test"
|
||||
const testServiceName = "test_service"
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
log.Infof("Test 'TestConfigure'")
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
config: `foobar: asd`,
|
||||
expectedErr: `while parsing DockerAcquisition configuration: [1:1] unknown field "foobar"`,
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: docker`,
|
||||
expectedErr: "no containers or services configuration provided",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
container_name:
|
||||
- toto`,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
check_interval: 10s
|
||||
container_name:
|
||||
- toto`,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
service_name:
|
||||
- web-service`,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
use_container_labels: true
|
||||
container_name:
|
||||
- toto`,
|
||||
expectedErr: "use_container_labels and container_name, container_id, container_id_regexp, container_name_regexp are mutually exclusive",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
use_service_labels: true
|
||||
service_name:
|
||||
- web-service`,
|
||||
expectedErr: "use_service_labels and service_name, service_id, service_id_regexp, service_name_regexp are mutually exclusive",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
container_name_regexp:
|
||||
- "[invalid"`,
|
||||
expectedErr: "container_name_regexp: error parsing regexp: missing closing ]: `[invalid`",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
container_id_regexp:
|
||||
- "*invalid"`,
|
||||
expectedErr: "container_id_regexp: error parsing regexp: missing argument to repetition operator: `*`",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
service_name_regexp:
|
||||
- "(?P<invalid"`,
|
||||
expectedErr: "service_name_regexp: error parsing regexp: invalid named capture: `(?P<invalid`",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: docker
|
||||
service_id_regexp:
|
||||
- "+invalid"`,
|
||||
expectedErr: "service_id_regexp: error parsing regexp: missing argument to repetition operator: `+`",
|
||||
},
|
||||
}
|
||||
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.config, func(t *testing.T) {
|
||||
f := Source{}
|
||||
err := f.Configure(ctx, []byte(tc.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.RequireErrorContains(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureDSN(t *testing.T) {
|
||||
log.Infof("Test 'TestConfigureDSN'")
|
||||
|
||||
|
||||
@@ -21,54 +21,6 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestBadConfiguration(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "extra configuration key",
|
||||
config: "foobar: asd.log",
|
||||
expectedErr: `cannot parse FileAcquisition configuration: [1:1] unknown field "foobar"`,
|
||||
},
|
||||
{
|
||||
name: "missing filenames",
|
||||
config: "mode: tail",
|
||||
expectedErr: "no filename or filenames configuration provided",
|
||||
},
|
||||
{
|
||||
name: "glob syntax error",
|
||||
config: `filename: "[asd-.log"`,
|
||||
expectedErr: "glob failure: syntax error in pattern",
|
||||
},
|
||||
{
|
||||
name: "bad exclude regexp",
|
||||
config: `filenames: ["asd.log"]
|
||||
exclude_regexps: ["as[a-$d"]`,
|
||||
expectedErr: "could not compile regexp as",
|
||||
},
|
||||
{
|
||||
name: "duplicate keys",
|
||||
config: `filenames: ["asd.log"]
|
||||
filenames: ["ase.log"]`,
|
||||
expectedErr: `cannot parse FileAcquisition configuration: [2:1] mapping key "filenames" already defined at [1:1]`,
|
||||
},
|
||||
}
|
||||
|
||||
subLogger := log.WithField("type", fileacquisition.ModuleName)
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := fileacquisition.Source{}
|
||||
err := f.Configure(ctx, []byte(tc.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.RequireErrorContains(t, err, tc.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureDSN(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -505,57 +457,6 @@ exclude_regexps: ["\\.gz$"]`
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
func TestDiscoveryPollConfiguration(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid discovery poll config",
|
||||
config: `
|
||||
filenames:
|
||||
- "tests/test.log"
|
||||
discovery_poll_enable: true
|
||||
discovery_poll_interval: "30s"
|
||||
mode: tail
|
||||
`,
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "invalid poll interval",
|
||||
config: `
|
||||
filenames:
|
||||
- "tests/test.log"
|
||||
discovery_poll_enable: true
|
||||
discovery_poll_interval: "invalid"
|
||||
mode: tail
|
||||
`,
|
||||
wantErr: `cannot parse FileAcquisition configuration: time: invalid duration "invalid"`,
|
||||
},
|
||||
{
|
||||
name: "polling disabled",
|
||||
config: `
|
||||
filenames:
|
||||
- "tests/test.log"
|
||||
discovery_poll_enable: false
|
||||
mode: tail
|
||||
`,
|
||||
wantErr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := &fileacquisition.Source{}
|
||||
err := f.Configure(ctx, []byte(tc.config), log.NewEntry(log.New()), metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.RequireErrorContains(t, err, tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoveryPolling(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
dir := t.TempDir()
|
||||
|
||||
@@ -22,8 +22,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/tomb.v2"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/cstest"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/metrics"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
@@ -33,173 +31,12 @@ const (
|
||||
testHTTPServerAddrTLS = "https://127.0.0.1:8080"
|
||||
)
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
config: `
|
||||
timeout: 1m`,
|
||||
expectedErr: "listen_addr or listen_socket is required",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: wrongpath`,
|
||||
expectedErr: "path must start with /",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: basic_auth`,
|
||||
expectedErr: "basic_auth is selected, but basic_auth is not provided",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers`,
|
||||
expectedErr: "headers is selected, but headers is not provided",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: basic_auth
|
||||
basic_auth:
|
||||
username: 132`,
|
||||
expectedErr: "basic_auth is selected, but password is not provided",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: basic_auth
|
||||
basic_auth:
|
||||
password: 132`,
|
||||
expectedErr: "basic_auth is selected, but username is not provided",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:`,
|
||||
expectedErr: "headers is selected, but headers is not provided",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: toto`,
|
||||
expectedErr: "invalid auth_type: must be one of basic_auth, headers, mtls",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
tls:
|
||||
server_key: key`,
|
||||
expectedErr: "server_cert is required",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
tls:
|
||||
server_cert: cert`,
|
||||
expectedErr: "server_key is required",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: mtls
|
||||
tls:
|
||||
server_cert: cert
|
||||
server_key: key`,
|
||||
expectedErr: "mtls is selected, but ca_cert is not provided",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
max_body_size: 0`,
|
||||
expectedErr: "max_body_size must be positive",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
timeout: toto`,
|
||||
expectedErr: `cannot parse: time: invalid duration "toto"`,
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
custom_status_code: 999`,
|
||||
expectedErr: "invalid HTTP status code",
|
||||
},
|
||||
}
|
||||
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
|
||||
for _, test := range tests {
|
||||
h := Source{}
|
||||
err := h.Configure(ctx, []byte(test.config), subLogger, 0)
|
||||
cstest.AssertErrorContains(t, err, test.expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUuid(t *testing.T) {
|
||||
h := Source{}
|
||||
h.Config.UniqueId = "test"
|
||||
assert.Equal(t, "test", h.GetUuid())
|
||||
}
|
||||
|
||||
func TestUnmarshalConfig(t *testing.T) {
|
||||
h := Source{}
|
||||
err := h.UnmarshalConfig([]byte(`
|
||||
source: http
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: 15
|
||||
auth_type: headers`))
|
||||
cstest.AssertErrorMessage(t, err, "cannot parse: [5:1] found character '\t' that cannot start any token")
|
||||
}
|
||||
|
||||
func TestGetMode(t *testing.T) {
|
||||
h := Source{}
|
||||
h.Config.Mode = "test"
|
||||
@@ -213,7 +50,9 @@ func TestGetName(t *testing.T) {
|
||||
|
||||
func SetupAndRunHTTPSource(t *testing.T, h *Source, config []byte, metricLevel metrics.AcquisitionMetricsLevel) (chan pipeline.Event, *prometheus.Registry, *tomb.Tomb) {
|
||||
ctx := t.Context()
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
subLogger := log.WithFields(log.Fields{
|
||||
"type": ModuleName,
|
||||
})
|
||||
err := h.Configure(ctx, config, subLogger, metricLevel)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -21,44 +21,6 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestBadConfiguration(t *testing.T) {
|
||||
cstest.SkipOnWindows(t)
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
config: `foobar: asd.log`,
|
||||
wantErr: `cannot parse: [1:1] unknown field "foobar"`,
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: journalctl`,
|
||||
wantErr: "journalctl_filter is required",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: cat
|
||||
source: journalctl
|
||||
journalctl_filter:
|
||||
- _UID=42`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.config, func(t *testing.T) {
|
||||
f := Source{}
|
||||
logger, _ := logtest.NewNullLogger()
|
||||
err := f.Configure(ctx, []byte(tc.config), logrus.NewEntry(logger), metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.RequireErrorContains(t, err, tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureDSN(t *testing.T) {
|
||||
cstest.SkipOnWindows(t)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func (s *Source) UnmarshalConfig(yamlConfig []byte) error {
|
||||
|
||||
err := yaml.UnmarshalWithOptions(yamlConfig, &s.Config, yaml.Strict())
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse %s datasource configuration: %s", s.GetName(), yaml.FormatError(err, false, false))
|
||||
return fmt.Errorf("cannot parse: %s", yaml.FormatError(err, false, false))
|
||||
}
|
||||
|
||||
if len(s.Config.Brokers) == 0 {
|
||||
@@ -57,7 +57,7 @@ func (s *Source) UnmarshalConfig(yamlConfig []byte) error {
|
||||
}
|
||||
|
||||
if s.Config.Topic == "" {
|
||||
return fmt.Errorf("cannot create a %s reader with am empty topic", s.GetName())
|
||||
return fmt.Errorf("cannot create a %s reader with an empty topic", s.GetName())
|
||||
}
|
||||
|
||||
if s.Config.Mode == "" {
|
||||
|
||||
@@ -18,69 +18,6 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
config: `
|
||||
foobar: bla
|
||||
source: kafka`,
|
||||
expectedErr: `[2:1] unknown field "foobar"`,
|
||||
},
|
||||
{
|
||||
config: `source: kafka`,
|
||||
expectedErr: "cannot create a kafka reader with an empty list of broker addresses",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kafka
|
||||
brokers:
|
||||
- bla
|
||||
timeout: 5`,
|
||||
expectedErr: "cannot create a kafka reader with am empty topic",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kafka
|
||||
brokers:
|
||||
- bla
|
||||
topic: toto
|
||||
timeout: aa`,
|
||||
expectedErr: "cannot create kafka dialer: strconv.Atoi: parsing \"aa\": invalid syntax",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kafka
|
||||
brokers:
|
||||
- localhost:9092
|
||||
topic: crowdsec`,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kafka
|
||||
brokers:
|
||||
- localhost:9092
|
||||
topic: crowdsec
|
||||
partition: 1
|
||||
group_id: crowdsec`,
|
||||
expectedErr: "cannote create kafka reader: cannot specify both group_id and partition",
|
||||
},
|
||||
}
|
||||
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
|
||||
for _, test := range tests {
|
||||
k := Source{}
|
||||
err := k.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.AssertErrorContains(t, err, test.expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func writeToKafka(ctx context.Context, w *kafka.Writer, logs []string) {
|
||||
for idx, log := range logs {
|
||||
err := w.WriteMessages(ctx, kafka.Message{
|
||||
|
||||
@@ -98,58 +98,6 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestBadConfiguration(t *testing.T) {
|
||||
cstest.SkipOnWindows(t)
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
config: `source: kinesis`,
|
||||
expectedErr: "stream_name is mandatory when use_enhanced_fanout is false",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kinesis
|
||||
max_retries: whatev`,
|
||||
expectedErr: "[3:14] cannot unmarshal string into Go struct field Configuration.MaxRetries of type int",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kinesis
|
||||
use_enhanced_fanout: true`,
|
||||
expectedErr: "stream_arn is mandatory when use_enhanced_fanout is true",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kinesis
|
||||
use_enhanced_fanout: true
|
||||
stream_arn: arn:aws:kinesis:eu-west-1:123456789012:stream/my-stream`,
|
||||
expectedErr: "consumer_name is mandatory when use_enhanced_fanout is true",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: kinesis
|
||||
stream_name: foobar
|
||||
stream_arn: arn:aws:kinesis:eu-west-1:123456789012:stream/my-stream`,
|
||||
expectedErr: "stream_arn and stream_name are mutually exclusive",
|
||||
},
|
||||
}
|
||||
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.config, func(t *testing.T) {
|
||||
f := Source{}
|
||||
err := f.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.AssertErrorContains(t, err, test.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromStream(t *testing.T) {
|
||||
endpoint := cstest.SetAWSTestEnv(t)
|
||||
|
||||
|
||||
@@ -18,50 +18,6 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestBadConfiguration(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
name string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "unknown field",
|
||||
config: `source: k8s-audit
|
||||
foobar: asd.log`,
|
||||
expectedErr: `[2:1] unknown field "foobar"`,
|
||||
},
|
||||
{
|
||||
name: "missing listen_addr",
|
||||
config: `source: k8s-audit`,
|
||||
expectedErr: "listen_addr cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "missing listen_port",
|
||||
config: `source: k8s-audit
|
||||
listen_addr: 0.0.0.0`,
|
||||
expectedErr: "listen_port cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "mismatched types",
|
||||
config: `
|
||||
source: k8s-audit
|
||||
listen_addr: 0.0.0.0
|
||||
listen_port: true
|
||||
`,
|
||||
expectedErr: `[4:14] cannot unmarshal bool into Go struct field Configuration.ListenPort of type int`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
f := Source{}
|
||||
|
||||
err := f.UnmarshalConfig([]byte(test.config))
|
||||
cstest.RequireErrorContains(t, err, test.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidConfig(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
tests := []struct {
|
||||
|
||||
@@ -24,159 +24,6 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestConfiguration(t *testing.T) {
|
||||
log.Infof("Test 'TestConfigure'")
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
expectedErr string
|
||||
password string
|
||||
waitForReady time.Duration
|
||||
delayFor time.Duration
|
||||
noReadyCheck bool
|
||||
testName string
|
||||
}{
|
||||
{
|
||||
config: `foobar: asd`,
|
||||
expectedErr: `[1:1] unknown field "foobar"`,
|
||||
testName: "Unknown field",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki`,
|
||||
expectedErr: "loki query is mandatory",
|
||||
testName: "Missing url",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki
|
||||
url: http://localhost:3100/
|
||||
`,
|
||||
expectedErr: "loki query is mandatory",
|
||||
testName: "Missing query",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki
|
||||
url: http://localhost:3100/
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
testName: "Correct config",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki
|
||||
url: http://localhost:3100/
|
||||
wait_for_ready: 5s
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
testName: "Correct config with wait_for_ready",
|
||||
waitForReady: 5 * time.Second,
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki
|
||||
url: http://localhost:3100/
|
||||
delay_for: 1s
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
testName: "Correct config with delay_for",
|
||||
delayFor: 1 * time.Second,
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki
|
||||
url: http://localhost:3100/
|
||||
no_ready_check: true
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
testName: "Correct config with no_ready_check",
|
||||
noReadyCheck: true,
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki
|
||||
url: http://localhost:3100/
|
||||
auth:
|
||||
username: foo
|
||||
password: bar
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
password: "bar",
|
||||
testName: "Correct config with password",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: loki
|
||||
url: http://localhost:3100/
|
||||
delay_for: 10s
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "delay_for should be a value between 1s and 5s",
|
||||
testName: "Invalid DelayFor",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: loki
|
||||
no_ready_check: 37
|
||||
`,
|
||||
expectedErr: "[3:17] cannot unmarshal uint64 into Go struct field Configuration.NoReadyCheck of type bool",
|
||||
testName: "type mismatch",
|
||||
},
|
||||
}
|
||||
subLogger := log.WithField("type", loki.ModuleName)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
lokiSource := loki.Source{}
|
||||
err := lokiSource.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.AssertErrorContains(t, err, test.expectedErr)
|
||||
|
||||
if test.password != "" {
|
||||
p := lokiSource.Config.Auth.Password
|
||||
if test.password != p {
|
||||
t.Fatalf("Password mismatch : %s != %s", test.password, p)
|
||||
}
|
||||
}
|
||||
|
||||
if test.waitForReady != 0 {
|
||||
if lokiSource.Config.WaitForReady != test.waitForReady {
|
||||
t.Fatalf("Wrong WaitForReady %v != %v", lokiSource.Config.WaitForReady, test.waitForReady)
|
||||
}
|
||||
}
|
||||
|
||||
if test.delayFor != 0 {
|
||||
if lokiSource.Config.DelayFor != test.delayFor {
|
||||
t.Fatalf("Wrong DelayFor %v != %v", lokiSource.Config.DelayFor, test.delayFor)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, test.noReadyCheck, lokiSource.Config.NoReadyCheck)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureDSN(t *testing.T) {
|
||||
log.Infof("Test 'TestConfigureDSN'")
|
||||
|
||||
|
||||
@@ -19,118 +19,10 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/tomb.v2"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/cstest"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/metrics"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestBadConfiguration(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no bucket",
|
||||
config: `
|
||||
source: s3
|
||||
`,
|
||||
expectedErr: "bucket_name is required",
|
||||
},
|
||||
{
|
||||
name: "type mismatch",
|
||||
config: `
|
||||
source: s3
|
||||
max_buffer_size: true
|
||||
`,
|
||||
expectedErr: "[3:18] cannot unmarshal bool into Go struct field Configuration.MaxBufferSize of type int",
|
||||
},
|
||||
{
|
||||
name: "invalid polling method",
|
||||
config: `
|
||||
source: s3
|
||||
bucket_name: foobar
|
||||
polling_method: foobar
|
||||
`,
|
||||
expectedErr: "invalid polling method foobar",
|
||||
},
|
||||
{
|
||||
name: "no sqs name",
|
||||
config: `
|
||||
source: s3
|
||||
bucket_name: foobar
|
||||
polling_method: sqs
|
||||
`,
|
||||
expectedErr: "sqs_name is required when using sqs polling method",
|
||||
},
|
||||
{
|
||||
name: "both bucket and sqs",
|
||||
config: `
|
||||
source: s3
|
||||
bucket_name: foobar
|
||||
polling_method: sqs
|
||||
sqs_name: foobar
|
||||
`,
|
||||
expectedErr: "bucket_name and sqs_name are mutually exclusive",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
f := Source{}
|
||||
|
||||
err := f.Configure(ctx, []byte(test.config), nil, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.RequireErrorContains(t, err, test.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoodConfiguration(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
}{
|
||||
{
|
||||
name: "basic",
|
||||
config: `
|
||||
source: s3
|
||||
bucket_name: foobar
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "polling method",
|
||||
config: `
|
||||
source: s3
|
||||
polling_method: sqs
|
||||
sqs_name: foobar
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "list method",
|
||||
config: `
|
||||
source: s3
|
||||
bucket_name: foobar
|
||||
polling_method: list
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
f := Source{}
|
||||
logger := log.NewEntry(log.New())
|
||||
|
||||
err := f.Configure(ctx, []byte(test.config), logger, metrics.AcquisitionMetricsLevelNone)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockS3Client struct{}
|
||||
|
||||
// We add one hour to trick the listing goroutine into thinking the files are new
|
||||
|
||||
@@ -18,54 +18,6 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
config: `
|
||||
foobar: bla
|
||||
source: syslog`,
|
||||
expectedErr: `[2:1] unknown field "foobar"`,
|
||||
},
|
||||
{
|
||||
config: `source: syslog`,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: syslog
|
||||
listen_port: asd`,
|
||||
expectedErr: "[3:14] cannot unmarshal string into Go struct field Configuration.Port of type int",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: syslog
|
||||
listen_port: 424242`,
|
||||
expectedErr: "invalid port 424242",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
source: syslog
|
||||
listen_addr: 10.0.0`,
|
||||
expectedErr: "invalid listen IP 10.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.config, func(t *testing.T) {
|
||||
s := Source{}
|
||||
err := s.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.AssertErrorContains(t, err, test.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeToSyslog(ctx context.Context, logs []string) error {
|
||||
dialer := &net.Dialer{}
|
||||
|
||||
|
||||
@@ -26,115 +26,6 @@ import (
|
||||
"github.com/crowdsecurity/crowdsec/pkg/pipeline"
|
||||
)
|
||||
|
||||
func TestConfiguration(t *testing.T) {
|
||||
log.Infof("Test 'TestConfigure'")
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
tests := []struct {
|
||||
config string
|
||||
expectedErr string
|
||||
password string
|
||||
waitForReady time.Duration
|
||||
testName string
|
||||
}{
|
||||
{
|
||||
config: `foobar: asd`,
|
||||
expectedErr: `[1:1] unknown field "foobar"`,
|
||||
testName: "Unknown field",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: victorialogs`,
|
||||
expectedErr: "url is mandatory",
|
||||
testName: "Missing url",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: victorialogs
|
||||
url: http://localhost:9428/
|
||||
`,
|
||||
expectedErr: "query is mandatory",
|
||||
testName: "Missing query",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: victorialogs
|
||||
url: http://localhost:9428/
|
||||
query: >
|
||||
{server="demo"}
|
||||
limit: true
|
||||
`,
|
||||
expectedErr: "[7:8] cannot unmarshal bool into Go struct field Configuration.Limit of type int",
|
||||
testName: "mismatched type",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: victorialogs
|
||||
url: http://localhost:9428/
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
testName: "Correct config",
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: victorialogs
|
||||
url: http://localhost:9428/
|
||||
wait_for_ready: 5s
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
testName: "Correct config with wait_for_ready",
|
||||
waitForReady: 5 * time.Second,
|
||||
},
|
||||
{
|
||||
config: `
|
||||
mode: tail
|
||||
source: victorialogs
|
||||
url: http://localhost:9428/
|
||||
auth:
|
||||
username: foo
|
||||
password: bar
|
||||
query: >
|
||||
{server="demo"}
|
||||
`,
|
||||
expectedErr: "",
|
||||
password: "bar",
|
||||
testName: "Correct config with password",
|
||||
},
|
||||
}
|
||||
subLogger := log.WithField("type", victorialogs.ModuleName)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
vlSource := victorialogs.Source{}
|
||||
err := vlSource.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
cstest.AssertErrorContains(t, err, test.expectedErr)
|
||||
|
||||
if test.password != "" {
|
||||
p := vlSource.Config.Auth.Password
|
||||
if test.password != p {
|
||||
t.Fatalf("Password mismatch : %s != %s", test.password, p)
|
||||
}
|
||||
}
|
||||
|
||||
if test.waitForReady != 0 {
|
||||
if vlSource.Config.WaitForReady != test.waitForReady {
|
||||
t.Fatalf("Wrong WaitForReady %v != %v", vlSource.Config.WaitForReady, test.waitForReady)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureDSN(t *testing.T) {
|
||||
log.Infof("Test 'TestConfigureDSN'")
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ event_ids: true`,
|
||||
},
|
||||
}
|
||||
|
||||
subLogger := log.WithField("type", "windowseventlog")
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
for _, test := range tests {
|
||||
f := Source{}
|
||||
err := f.Configure(ctx, []byte(test.config), subLogger, metrics.AcquisitionMetricsLevelNone)
|
||||
@@ -122,7 +122,7 @@ event_level: bla`,
|
||||
expectedErr: "invalid log level",
|
||||
},
|
||||
}
|
||||
subLogger := log.WithField("type", "windowseventlog")
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
for _, test := range tests {
|
||||
t.Run(test.config, func(t *testing.T) {
|
||||
f := Source{}
|
||||
@@ -190,7 +190,7 @@ event_ids:
|
||||
expectedLines: nil,
|
||||
},
|
||||
}
|
||||
subLogger := log.WithField("type", "windowseventlog")
|
||||
subLogger := log.WithField("type", ModuleName)
|
||||
|
||||
evthandler, err := eventlog.Open("Application")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package acquisition
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
// format a compact error without schema location for testing purposes.
|
||||
func compactSchemaErr(err error) error {
|
||||
var ve *jsonschema.ValidationError
|
||||
if !errors.As(err, &ve) {
|
||||
return err
|
||||
}
|
||||
|
||||
out := ve.BasicOutput()
|
||||
if out == nil || len(out.Errors) == 0 {
|
||||
// Fallback; this may include schema URL, but it's better than losing the error.
|
||||
return err
|
||||
}
|
||||
|
||||
msgs := make([]string, 0, len(out.Errors))
|
||||
for _, u := range out.Errors {
|
||||
if u.Error == nil {
|
||||
continue
|
||||
}
|
||||
loc := u.InstanceLocation
|
||||
if loc == "" {
|
||||
loc = "/"
|
||||
}
|
||||
msgs = append(msgs, fmt.Sprintf("%s: %s", loc, u.Error.String()))
|
||||
}
|
||||
|
||||
sort.Strings(msgs)
|
||||
return fmt.Errorf("%s", strings.Join(msgs, "; "))
|
||||
}
|
||||
|
||||
// ValidateYAML validates configYAML against schemaPath.
|
||||
func ValidateYAML(configYAML []byte, schemaPath string) error {
|
||||
if schemaPath == "" {
|
||||
return errors.New("no schema provided")
|
||||
}
|
||||
|
||||
configJSON, err := yaml.YAMLToJSON(configYAML)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config: YAML->JSON: %w", err)
|
||||
}
|
||||
|
||||
configDoc, err := jsonschema.UnmarshalJSON(bytes.NewReader(configJSON))
|
||||
if err != nil {
|
||||
return fmt.Errorf("config: decode JSON: %w", err)
|
||||
}
|
||||
|
||||
c := jsonschema.NewCompiler()
|
||||
c.DefaultDraft(jsonschema.Draft2020)
|
||||
|
||||
schemaYAML, err := os.ReadFile(schemaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read schema %q: %w", schemaPath, err)
|
||||
}
|
||||
|
||||
schemaJSON, err := yaml.YAMLToJSON(schemaYAML)
|
||||
if err != nil {
|
||||
return fmt.Errorf("schema %q: YAML->JSON: %w", schemaPath, err)
|
||||
}
|
||||
|
||||
schemaDoc, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON))
|
||||
if err != nil {
|
||||
return fmt.Errorf("schema %q: decode JSON: %w", schemaPath, err)
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(schemaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("abs %q: %w", schemaPath, err)
|
||||
}
|
||||
|
||||
if err := c.AddResource(abs, schemaDoc); err != nil {
|
||||
return fmt.Errorf("add schema resource %q: %w", abs, err)
|
||||
}
|
||||
|
||||
sch, err := c.Compile(abs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compile schema %q: %w", abs, err)
|
||||
}
|
||||
|
||||
if err := sch.Validate(configDoc); err != nil {
|
||||
var ve *jsonschema.ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
return compactSchemaErr(ve)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
# minimal configuration to satisfy appsec
|
||||
config_paths:
|
||||
data_dir: /path/to/data
|
||||
api:
|
||||
client:
|
||||
insecure_skip_verify: false
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type appsec: unable to parse appsec configuration: appsec_config or appsec_config_path must be set
|
||||
source: appsec
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: appsec
|
||||
log_level: toto
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type appsec: unable to parse appsec configuration: cannot parse appsec configuration: [6:6] cannot unmarshal map[string]interface {} into Go struct field Configuration.AppsecConfig of type string
|
||||
source: appsec
|
||||
labels:
|
||||
type: sometype
|
||||
appsec_config:
|
||||
foo: bar
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: appsec
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type appsec: missing lapi client credentials
|
||||
source: appsec
|
||||
labels:
|
||||
type: sometype
|
||||
appsec_config: something
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type cloudwatch: aws_region is not specified, specify it or aws_config_dir
|
||||
source: cloudwatch
|
||||
labels:
|
||||
type: sometype
|
||||
group_name: testgroup
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: cloudwatch
|
||||
log_level: toto
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type cloudwatch: group_name is mandatory for CloudwatchSource
|
||||
source: cloudwatch
|
||||
labels:
|
||||
type: sometype
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: cloudwatch
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type cloudwatch: cannot parse: [5:1] unknown field "xxx"
|
||||
source: cloudwatch
|
||||
labels:
|
||||
type: sometype
|
||||
xxx: dummy
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: docker
|
||||
log_level: toto
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type docker: unsupported mode server for docker datasource
|
||||
# schemaErr: /mode: value must be one of 'tail', 'cat'
|
||||
mode: server
|
||||
source: docker
|
||||
container_name:
|
||||
- toto
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type docker: container_id_regexp: error parsing regexp: missing argument to repetition operator: `*`
|
||||
# schemaErr:
|
||||
source: docker
|
||||
container_id_regexp:
|
||||
- "*invalid"
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type docker: use_container_labels and container_name, container_id, container_id_regexp, container_name_regexp are mutually exclusive
|
||||
# schemaErr: /: 'allOf' failed; /: 'not' failed
|
||||
source: docker
|
||||
use_container_labels: true
|
||||
container_name:
|
||||
- toto
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type docker: container_name_regexp: error parsing regexp: missing closing ]: `[invalid`
|
||||
# schemaErr:
|
||||
source: docker
|
||||
container_name_regexp:
|
||||
- "[invalid"
|
||||
@@ -0,0 +1,3 @@
|
||||
# wantErr: datasource of type docker: no containers or services configuration provided
|
||||
# schemaErr: /: 'allOf' failed; /: 'anyOf' failed; /: missing property 'container_id'; /: missing property 'container_id_regexp'; /: missing property 'container_name'; /: missing property 'container_name_regexp'; /: missing property 'service_id'; /: missing property 'service_id_regexp'; /: missing property 'service_name'; /: missing property 'service_name_regexp'; /: missing property 'use_container_labels'; /: missing property 'use_service_labels'
|
||||
source: docker
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type docker: service_id_regexp: error parsing regexp: missing argument to repetition operator: `+`
|
||||
# schemaErr:
|
||||
source: docker
|
||||
service_id_regexp:
|
||||
- "+invalid"
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type docker: use_service_labels and service_name, service_id, service_id_regexp, service_name_regexp are mutually exclusive
|
||||
# schemaErr: /: 'allOf' failed; /: 'not' failed
|
||||
source: docker
|
||||
use_service_labels: true
|
||||
service_name:
|
||||
- web-service
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type docker: service_name_regexp: error parsing regexp: invalid named capture: `(?P<invalid`
|
||||
# schemaErr:
|
||||
source: docker
|
||||
service_name_regexp:
|
||||
- "(?P<invalid"
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type docker: while parsing DockerAcquisition configuration: [4:1] unknown field "foobar"
|
||||
# schemaErr: /: additional properties 'foobar' not allowed
|
||||
source: docker
|
||||
foobar: asd.log
|
||||
+1
@@ -0,0 +1 @@
|
||||
# wantErr: empty yaml document
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type file: could not compile regexp as[a-$d: error parsing regexp: invalid character class range: `a-$`
|
||||
source: file
|
||||
labels:
|
||||
type: sometype
|
||||
filename: "[asd.log]"
|
||||
exclude_regexps: ["as[a-$d"]
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: file
|
||||
log_level: toto
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: position 0: [6:1] mapping key "filenames" already defined at [5:1]
|
||||
source: file
|
||||
labels:
|
||||
type: sometype
|
||||
filenames: "[asd.log]"
|
||||
filenames: "[ase.log]"
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type file: no filename or filenames configuration provided
|
||||
source: file
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: position 0: [5:11] sequence end token ']' not found
|
||||
source: file
|
||||
labels:
|
||||
type: sometype
|
||||
filename: [asd-.log
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type file: glob failure: syntax error in pattern
|
||||
source: file
|
||||
labels:
|
||||
type: sometype
|
||||
filename: "[asd-.log"
|
||||
@@ -0,0 +1,9 @@
|
||||
# wantErr: datasource of type file: cannot parse FileAcquisition configuration: time: invalid duration "invalid"
|
||||
source: file
|
||||
labels:
|
||||
type: sometype
|
||||
filenames:
|
||||
- "tests/test.log"
|
||||
discovery_poll_enable: true
|
||||
discovery_poll_interval: invalid
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: file
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type file: cannot parse FileAcquisition configuration: [5:1] unknown field "foobar"
|
||||
source: file
|
||||
labels:
|
||||
type: sometype
|
||||
foobar: asd.log
|
||||
@@ -0,0 +1,7 @@
|
||||
# wantErr: datasource of type http: invalid auth_type: must be one of basic_auth, headers, mtls
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: toto
|
||||
@@ -0,0 +1,7 @@
|
||||
# wantErr: datasource of type http: basic_auth is selected, but basic_auth is not provided
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: basic_auth
|
||||
@@ -0,0 +1,10 @@
|
||||
# wantErr: datasource of type http: mtls is selected, but ca_cert is not provided
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: mtls
|
||||
tls:
|
||||
server_cert: cert
|
||||
server_key: key
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: http
|
||||
log_level: toto
|
||||
@@ -0,0 +1,10 @@
|
||||
# wantErr: datasource of type http: invalid HTTP status code
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
custom_status_code: 999
|
||||
@@ -0,0 +1,10 @@
|
||||
# wantErr: datasource of type http: cannot parse: time: invalid duration "toto"
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
timeout: toto
|
||||
@@ -0,0 +1,7 @@
|
||||
# wantErr: datasource of type http: headers is selected, but headers is not provided
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: http
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type http: listen_addr or listen_socket is required
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,10 @@
|
||||
# wantErr: datasource of type http: max_body_size must be positive
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
max_body_size: 0
|
||||
@@ -0,0 +1,9 @@
|
||||
# wantErr: datasource of type http: basic_auth is selected, but password is not provided
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: basic_auth
|
||||
basic_auth:
|
||||
username: 132
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type http: path must start with /
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: wrongpath
|
||||
@@ -0,0 +1,11 @@
|
||||
# wantErr: datasource of type http: server_cert is required
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
tls:
|
||||
server_key: key
|
||||
@@ -0,0 +1,11 @@
|
||||
# wantErr: datasource of type http: server_key is required
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: headers
|
||||
headers:
|
||||
key: value
|
||||
tls:
|
||||
server_cert: cert
|
||||
@@ -0,0 +1,9 @@
|
||||
# wantErr: datasource of type http: basic_auth is selected, but username is not provided
|
||||
source: http
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 127.0.0.1:8080
|
||||
path: /test
|
||||
auth_type: basic_auth
|
||||
basic_auth:
|
||||
password: xyz
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: journalctl
|
||||
log_level: toto
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type journalctl: journalctl_filter is required
|
||||
source: journalctl
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: journalctl
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type journalctl: cannot parse: [3:1] unknown field "filename"
|
||||
source: journalctl
|
||||
filename: /path/to/file.log
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type kafka: cannot create a kafka reader with an empty list of broker addresses
|
||||
source: kafka
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: kafka
|
||||
log_level: toto
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# wantErr: datasource of type kafka: cannote create kafka reader: cannot specify both group_id and partition
|
||||
source: kafka
|
||||
labels:
|
||||
type: sometype
|
||||
brokers:
|
||||
- localhost:9092
|
||||
topic: crowdsec
|
||||
partition: 1
|
||||
group_id: crowdsec
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: kafka
|
||||
@@ -0,0 +1,8 @@
|
||||
# wantErr: datasource of type kafka: cannot create kafka dialer: strconv.Atoi: parsing "aa": invalid syntax
|
||||
source: kafka
|
||||
labels:
|
||||
type: sometype
|
||||
brokers:
|
||||
- bla
|
||||
topic: toto
|
||||
timeout: aa
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type kafka: cannot create a kafka reader with an empty topic
|
||||
source: kafka
|
||||
labels:
|
||||
type: sometype
|
||||
brokers:
|
||||
- bla
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type kafka: cannot parse: [5:1] unknown field "foobar"
|
||||
source: kafka
|
||||
labels:
|
||||
type: sometype
|
||||
foobar: bla
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: kinesis
|
||||
log_level: toto
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type kinesis: consumer_name is mandatory when use_enhanced_fanout is true
|
||||
source: kinesis
|
||||
labels:
|
||||
type: sometype
|
||||
use_enhanced_fanout: true
|
||||
stream_arn: arn:aws:kinesis:eu-west-1:123456789012:stream/my-stream
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: kinesis
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type kinesis: cannot parse: [5:14] cannot unmarshal string into Go struct field Configuration.MaxRetries of type int
|
||||
source: kinesis
|
||||
labels:
|
||||
type: sometype
|
||||
max_retries: whavev
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type kinesis: stream_arn is mandatory when use_enhanced_fanout is true
|
||||
source: kinesis
|
||||
use_enhanced_fanout: true
|
||||
labels:
|
||||
type: sometype
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type kinesis: stream_arn and stream_name are mutually exclusive
|
||||
source: kinesis
|
||||
labels:
|
||||
type: sometype
|
||||
stream_arn: arn:aws:kinesis:eu-west-1:123456789012:stream/my-stream
|
||||
stream_name: foobar
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type kinesis: stream_name is mandatory when use_enhanced_fanout is false
|
||||
source: kinesis
|
||||
labels:
|
||||
type: sometype
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: k8s-audit
|
||||
log_level: toto
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: k8s-audit
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type k8s-audit: listen_addr cannot be empty
|
||||
source: k8s-audit
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type k8s-audit: cannot parse: [6:14] cannot unmarshal bool into Go struct field Configuration.ListenPort of type int
|
||||
source: k8s-audit
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 0.0.0.0
|
||||
listen_port: true
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type k8s-audit: listen_port cannot be empty
|
||||
source: k8s-audit
|
||||
labels:
|
||||
type: sometype
|
||||
listen_addr: 0.0.0.0
|
||||
@@ -0,0 +1,6 @@
|
||||
# wantErr: datasource of type k8s-audit: cannot parse: [6:1] unknown field "foobar"
|
||||
# schemaErr: foo
|
||||
source: k8s-audit
|
||||
labels:
|
||||
type: sometype
|
||||
foobar: asd.log
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: failed to parse: not a valid logrus Level: "toto"
|
||||
# schemaErr: /log_level: value must be one of 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug', 'trace'
|
||||
source: loki
|
||||
log_level: toto
|
||||
@@ -0,0 +1,7 @@
|
||||
# wantErr: datasource of type loki: delay_for should be a value between 1s and 5s
|
||||
source: loki
|
||||
labels:
|
||||
type: sometype
|
||||
delay_for: 10s
|
||||
query: >
|
||||
{server="demo"}
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: missing labels
|
||||
source: loki
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type loki: cannot parse loki acquisition configuration: [5:17] cannot unmarshal uint64 into Go struct field Configuration.NoReadyCheck of type bool
|
||||
source: loki
|
||||
labels:
|
||||
type: sometype
|
||||
no_ready_check: 37
|
||||
@@ -0,0 +1,4 @@
|
||||
# wantErr: datasource of type loki: loki query is mandatory
|
||||
source: loki
|
||||
labels:
|
||||
type: sometype
|
||||
@@ -0,0 +1,5 @@
|
||||
# wantErr: datasource of type loki: cannot parse loki acquisition configuration: [5:1] unknown field "foobar"
|
||||
source: loki
|
||||
labels:
|
||||
type: sometype
|
||||
foobar: asd
|
||||
@@ -0,0 +1,2 @@
|
||||
# wantErr: failed to parse: [2:1] string was used where mapping is expected
|
||||
blahblah
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user