Files
Naomi Kramer b923c39e2d Update configuration (#78)
* Update on config structure, functionality, and tests

Co-Authored-By: Naomi Kramer <naomiagoddard@gmail.com>

* Extend subnet type to read/write from db, update tests

Co-Authored-By: Liza Tsibur <liza@activecountermeasures.com>

* updated read file config test and subnet tests

* fixed config and util tests, updated subnet related functions

Co-Authored-By: Naomi Kramer <naomiagoddard@gmail.com>

* Remove error return from GetDefaultConfig

Co-Authored-By: Liza Tsibur <liza@activecountermeasures.com>

* added json tags to database struct

* Updated beacon weights validation for config

* updates to score thresholds validation tags

* changes to config subnet validation and testing

* Update subnet.go

* Write missing host entries to http to populate http_proto

* Updating some fields to uint64

* WIP update some field types

* Update zeek count types and fix tests

* Add clickhouse credentials

* Misc fixes

* Update pointer

* Add ability to mark datasets as sample datasets

* fix column name

* Fix datasets exiting import if hour is empty

* Fix zeek count parsing from TSV files

* Remove storing dns conns in arrays, Fix historical first seen dns lag

* Remove unused columns

* Update config.hjson

* Update config.hjson

* updated impact category score functions to use float64

Co-Authored-By: Naomi Kramer <naomiagoddard@gmail.com>

* Update subnet.go

* Store import version in imports table

* Fix duplicated SNI/IP long connections

* Update subnet_test.go

* Cleanup output

* Rolling files updates (#39)

* Limit number of days to import for rolling datasets

* Fix breaking imports when import was interrupted

* Remove debug output

---------

Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>

* Omit parts of env from output

* Set max for threat intel datasize

* Remove SELinux neutering for QA

* Add network size column

* Fix http_proto for missing host, update tests for missing host fixes

* Add online feeds to default config

* Update sshprep (#45)

* Update sshprep

Co-Authored-By: William Stearns <3538265+william-stearns@users.noreply.github.com>

* Update sshprep

Add Bradley's suggestion of using head -1 to limit to a single address.

---------

Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>
Co-authored-by: William Stearns <3538265+william-stearns@users.noreply.github.com>

* Installer Behavior Tweaks (#41)

* Add --yes flag to add-apt-repository command

* Add missing sudo flags, make sure we're using the SUDO variable instead

* Add ability to perform zone transfers (#48)

* Store zone transfer records

Co-Authored-By: moth <25512187+0x6d6f7468@users.noreply.github.com>

* Update config

* Add tests

* Tests, connectivity test

* Update tests

---------

Co-authored-by: moth <25512187+0x6d6f7468@users.noreply.github.com>

* Support RedHat/RHEL as a valid target (#47)

* Update sshprep

Co-Authored-By: William Stearns <3538265+william-stearns@users.noreply.github.com>

* Supporrt RedHat/RHEL as a valid target

---------

Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>
Co-authored-by: William Stearns <3538265+william-stearns@users.noreply.github.com>
Co-authored-by: moth <moth@blackhillsinfosec.com>

* Fix tests (#49)

* Fix tests

* Update WalkFiles to use UTC

* fixed issue with rolling datasets over 24hours old not getting historical first seen timestamp set (#52)

* Change values from float32 to float64 (#50)

* Switch float32 to float64

* Update threat category calculation to match CalculateBucketedScore (#51)

---------

Co-authored-by: Liza Tsibur <liza@activecountermeasures.com>

* Bump max query execution time default value

* Use string instead of error for ZoneTransferConnectivityErrors struct fields (#61)

* Upgrade Golang to version 1.24 (#59) (#60)

* Replace get_url with shell and curl (#58)

* Update sshprep

Co-Authored-By: William Stearns <3538265+william-stearns@users.noreply.github.com>

* Replace get_url with shell and curl

* Use get_url by default, fall back to curl if it fails

---------

Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>
Co-authored-by: William Stearns <3538265+william-stearns@users.noreply.github.com>
Co-authored-by: moth <moth@blackhillsinfosec.com>

* add automated log transfer, AC-Hunter issue 135 (#62)

* Update sshprep

Co-Authored-By: William Stearns <3538265+william-stearns@users.noreply.github.com>

* add automated log transfer, PR135

* cron requires non-executable permission

* Specify suggested YAML plugin and config in VSCode workspace

* Linting and light cleanup

* Update generate_installer.sh

Download zeek_log_transport.sh to send to the sensor.

* Create cron file if remote zeek installation

* Only run zeek log import steps for remote sensor installations

---------

Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>
Co-authored-by: William Stearns <3538265+william-stearns@users.noreply.github.com>
Co-authored-by: moth <moth@blackhillsinfosec.com>

* Temporarily disable RITA/Zeek log transport until installer is modular (#66)

* Uniform -y flag usage for repo management/package installation; Uniform SUDO variable usage (#68)

* Resolve Installer Side Effects and Formalize RHEL Support (#73)

* Add missing necessary wildcards for RHEL versions

* Remove Ansible task replacing python3-requests to avoid RHEL distro installation side effects

* Update supported distros in README

* Update scoring defaults

* Resolve Ansible Reboot Errors (#75)

* Clean up conditionals; Fix reboot step for Ubuntu

* Suppress erroneous error output on RPM systems, ignore errors on reboot necessity checks

* Ignore missing host rows for openhttp (#76)

* Fix integration tests due to prevalence (#77)

---------

Co-authored-by: Liza Tsibur <liza@activecountermeasures.com>
Co-authored-by: moth <moth@blackhillsinfosec.com>
Co-authored-by: William Stearns <william.l.stearns@gmail.com>
Co-authored-by: William Stearns <3538265+william-stearns@users.noreply.github.com>
Co-authored-by: moth <25512187+0x6d6f7468@users.noreply.github.com>
2025-09-22 16:21:05 -04:00

1045 lines
52 KiB
Go

package config
import (
"fmt"
"log"
"net"
"os"
"slices"
"strings"
"testing"
"github.com/activecm/rita/v5/util"
"github.com/joho/godotenv"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
// defaultConfigPath specifies the path of RITA's static config file
const defaultConfigPath = "../config.hjson"
func TestMain(m *testing.M) {
// load environment variables with panic prevention
if err := godotenv.Overload("../.env", "../integration/test.env"); err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
os.Exit(m.Run())
}
func TestReadFileConfig(t *testing.T) {
dfCfg := defaultConfig()
require.NoError(t, dfCfg.setEnv())
tests := []struct {
name string
configJSON string
expectedConfig *Config
expectedErrs []string
}{
{
name: "Valid Config",
// create a JSON string to write to the temporary file
configJSON: `{
update_check_enabled: false,
http_extensions_file_path: "/path/to/http/extensions",
batch_size: 75000,
max_query_execution_time: 120000,
months_to_keep_historical_first_seen: 6,
filtering: {
internal_subnets: ["11.0.0.0/8", "120.130.140.150/8"],
always_included_subnets: ["13.0.0.0/8", "160.140.150.160/8"],
always_included_domains: ["abc.com", "def.com"],
never_included_subnets: ["12.0.0.0/8", "150.140.150.160/8"],
never_included_domains: ["ghi.com", "jkl.com"],
filter_external_to_internal: false,
},
threat_intel: {
online_feeds: ["https://example.com/feed1", "https://example.com/feed2"]
},
scoring: {
beacon: {
unique_connection_threshold: 10,
timestamp_score_weight: 0.35,
datasize_score_weight: 0.20,
duration_score_weight: 0.35,
histogram_score_weight: 0.10,
duration_min_hours_seen: 10,
duration_consistency_ideal_hours_seen: 15,
histogram_mode_sensitivity: 0.08,
histogram_bimodal_outlier_removal: 2,
histogram_bimodal_min_hours_seen: 15,
score_thresholds: {
base: 0,
low: 1,
medium: 2,
high: 3
},
},
long_connection_score_thresholds: {
base: 1,
low: 2,
medium: 3,
high: 4
},
c2_score_thresholds: {
base: 1,
low: 2,
medium: 3,
high: 4
},
strobe_impact: {
category: "low",
},
threat_intel_impact: {
category: "low",
},
},
modifiers: {
threat_intel_score_increase: 0.1,
threat_intel_datasize_threshold: 100,
prevalence_score_increase: 0.6,
prevalence_increase_threshold: 0.1,
prevalence_score_decrease: 0.1,
prevalence_decrease_threshold: 0.2,
first_seen_score_increase: 0.8,
first_seen_increase_threshold: 10,
first_seen_score_decrease: 0.2,
first_seen_decrease_threshold: 50,
missing_host_count_score_increase: 0.4,
rare_signature_score_increase: 0.4,
c2_over_dns_direct_conn_score_increase: 0.9,
mime_type_mismatch_score_increase: 0.6
},
}`,
expectedConfig: func() *Config {
cfg := &Config{
RITA: RITA{
UpdateCheckEnabled: false,
BatchSize: 75000,
MaxQueryExecutionTime: 120000,
MonthsToKeepHistoricalFirstSeen: 6,
ThreatIntel: ThreatIntel{
OnlineFeeds: []string{"https://example.com/feed1", "https://example.com/feed2"},
},
},
Filtering: Filtering{
InternalSubnets: []util.Subnet{
{IPNet: &net.IPNet{IP: net.IP{11, 0, 0, 0}.To16(), Mask: net.CIDRMask(104, 128)}},
{IPNet: &net.IPNet{IP: net.IP{120, 0, 0, 0}.To16(), Mask: net.CIDRMask(104, 128)}},
},
AlwaysIncludedSubnets: []util.Subnet{
{IPNet: &net.IPNet{IP: net.IP{13, 0, 0, 0}.To16(), Mask: net.CIDRMask(104, 128)}},
{IPNet: &net.IPNet{IP: net.IP{160, 0, 0, 0}.To16(), Mask: net.CIDRMask(104, 128)}},
},
// mandatoryNeverIncludeSubnets are always apended to any neverIncludedSubnet entries
NeverIncludedSubnets: []util.Subnet{
{IPNet: &net.IPNet{IP: net.IP{12, 0, 0, 0}.To16(), Mask: net.CIDRMask(104, 128)}},
{IPNet: &net.IPNet{IP: net.IP{150, 0, 0, 0}.To16(), Mask: net.CIDRMask(104, 128)}},
{IPNet: &net.IPNet{IP: net.IP{0, 0, 0, 0}.To16(), Mask: net.CIDRMask(128, 128)}},
{IPNet: &net.IPNet{IP: net.IP{127, 0, 0, 0}.To16(), Mask: net.CIDRMask(104, 128)}},
{IPNet: &net.IPNet{IP: net.IP{169, 254, 0, 0}.To16(), Mask: net.CIDRMask(112, 128)}},
{IPNet: &net.IPNet{IP: net.IP{224, 0, 0, 0}.To16(), Mask: net.CIDRMask(100, 128)}},
{IPNet: &net.IPNet{IP: net.IP{255, 255, 255, 255}.To16(), Mask: net.CIDRMask(128, 128)}},
{IPNet: &net.IPNet{IP: net.ParseIP("::1"), Mask: net.CIDRMask(128, 128)}},
{IPNet: &net.IPNet{IP: net.IPv6unspecified, Mask: net.CIDRMask(128, 128)}},
{IPNet: &net.IPNet{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(10, 128)}},
{IPNet: &net.IPNet{IP: net.ParseIP("ff00::"), Mask: net.CIDRMask(8, 128)}},
{IPNet: &net.IPNet{IP: net.ParseIP("ff02::2"), Mask: net.CIDRMask(128, 128)}},
},
AlwaysIncludedDomains: []string{"abc.com", "def.com"},
NeverIncludedDomains: []string{"ghi.com", "jkl.com"},
FilterExternalToInternal: false,
},
Scoring: Scoring{
Beacon: BeaconScoring{
UniqueConnectionThreshold: 10,
TimestampScoreWeight: 0.35,
DatasizeScoreWeight: 0.20,
DurationScoreWeight: 0.35,
HistogramScoreWeight: 0.10,
DurationMinHoursSeen: 10,
DurationConsistencyIdealHoursSeen: 15,
HistogramModeSensitivity: 0.08,
HistogramBimodalOutlierRemoval: 2,
HistogramBimodalMinHoursSeen: 15,
ScoreThresholds: ScoreThresholds{Base: 0, Low: 1, Med: 2, High: 3},
},
ThreatScoring: ThreatScoring{
LongConnectionScoreThresholds: ScoreThresholds{Base: 1, Low: 2, Med: 3, High: 4},
C2ScoreThresholds: ScoreThresholds{Base: 1, Low: 2, Med: 3, High: 4},
StrobeImpact: ScoreImpact{Category: LowThreat, Score: LOW_CATEGORY_SCORE},
ThreatIntelImpact: ScoreImpact{Category: LowThreat, Score: LOW_CATEGORY_SCORE},
},
},
Modifiers: Modifiers{
ThreatIntelScoreIncrease: 0.1,
ThreatIntelDataSizeThreshold: 100,
PrevalenceScoreIncrease: 0.6,
PrevalenceIncreaseThreshold: 0.1,
PrevalenceScoreDecrease: 0.1,
PrevalenceDecreaseThreshold: 0.2,
FirstSeenScoreIncrease: 0.8,
FirstSeenIncreaseThreshold: 10,
FirstSeenScoreDecrease: 0.2,
FirstSeenDecreaseThreshold: 50,
MissingHostCountScoreIncrease: 0.4,
RareSignatureScoreIncrease: 0.4,
C2OverDNSDirectConnScoreIncrease: 0.9,
MIMETypeMismatchScoreIncrease: 0.6,
},
}
require.NoError(t, cfg.setEnv())
return cfg
}(),
},
{
name: "Invalid JSON",
configJSON: `{filtering: { internal_subnets: }}`,
expectedErrs: []string{errReadingConfigFile.Error(), "check your syntax"},
},
{
name: "Empty Categories",
configJSON: `{Env: {}, RITA: {}, Filtering: {}, Scoring: {}, Modifiers: {}}`,
expectedConfig: &dfCfg,
},
{
name: "Empty Config Object",
configJSON: `{}`,
expectedConfig: &dfCfg,
},
{
name: "Empty File",
configJSON: ``,
expectedErrs: []string{util.ErrFileIsEmtpy.Error()},
},
{
name: "File Not Found",
expectedErrs: []string{util.ErrFileDoesNotExist.Error()},
},
// the following tests ensure validation for subnets, since those checks are done outside of the validator package and cannot
// be vetted with the other validation tests
{
name: "Internal Subnets - Invalid IPv4",
configJSON: `{filtering: { internal_subnets: ["bingbong", "chickenstrip"]}}`,
expectedErrs: []string{errReadingConfigFile.Error(), util.ErrParseCIDRInvalidIP.Error()},
},
{
name: "Internal Subnets - Invalid IPv6",
configJSON: `{filtering: { internal_subnets: [ "2001:db8::chickenstrip"]}}`,
expectedErrs: []string{errReadingConfigFile.Error(), util.ErrParseCIDRInvalidIP.Error()},
},
{
name: "Internal Subnets - Invalid IPv4-Mapped IPv6",
configJSON: `{filtering: { internal_subnets: ["::ffff:beepboop"]}}`,
expectedErrs: []string{errReadingConfigFile.Error(), util.ErrParseCIDRInvalidIP.Error()},
},
{
name: "Internal Subnets - Invalid IPv4 Mask",
configJSON: `{filtering: { internal_subnets: ["bingbong.com/33"]}}`,
expectedErrs: []string{errReadingConfigFile.Error(), (&net.ParseError{Type: "CIDR address", Text: ""}).Error()},
},
{
name: "Internal Subnets - Invalid IPv4-Mapped IPv6 Mask",
configJSON: `{filtering: { internal_subnets: ["::ffff:192.168.1.0/b"]}}`,
expectedErrs: []string{errReadingConfigFile.Error(), util.ErrParseCIDRInvalidMask.Error()},
},
{
name: "Internal Subnets - Invalid IPv6 Mask",
configJSON: `{filtering: { internal_subnets: ["2001:db8::1/a"]}}`,
expectedErrs: []string{errReadingConfigFile.Error(), (&net.ParseError{Type: "CIDR address", Text: ""}).Error()},
},
{
name: "Internal Subnets Empty",
configJSON: `{filtering: { internal_subnets: []}}`,
expectedErrs: []string{errReadingConfigFile.Error(), "'InternalSubnets' failed on the 'gt' tag"},
},
{
name: "Never Included Subnets Empty",
configJSON: `{filtering: { never_included_subnets: []}}`,
expectedConfig: &dfCfg, // mandatory never include will ensure they are never empty
},
{
name: "Always Included Subnets Empty",
configJSON: `{filtering: { always_included_subnets: []}}`,
expectedConfig: &dfCfg, // always included subnets are optional
},
}
for i, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// create mock file system in memory
afs := afero.NewMemMapFs()
// get config file path
configPath := fmt.Sprintf("test-config-%d.hjson", i)
// create file
if tc.name != "File Not Found" {
require.NoError(t, afero.WriteFile(afs, configPath, []byte(tc.configJSON), 0o775))
defer func() { require.NoError(t, afs.Remove(configPath)) }()
}
// call function
cfg, err := ReadFileConfig(afs, configPath)
if len(tc.expectedErrs) == 0 {
require.NoError(t, err)
// need this so the test doesn't just crash on the next lines if something isn't working as expected
require.NotNil(t, cfg, "Config should not be nil")
require.NotNil(t, tc.expectedConfig, "Expected config should not be nil")
// verify version got set
require.Equal(t, "dev", Version, "version should be 'dev'")
// verify that env variables are not overwritten by JSON
require.Equal(t, tc.expectedConfig.Env, cfg.Env, "Env should match expected value")
// verify that the retrived config matches the expected config (split up for easier debugging)
require.Equal(t, tc.expectedConfig.RITA, cfg.RITA, "RITA should match expected value")
require.Equal(t, tc.expectedConfig.Filtering, cfg.Filtering, "Filtering should match expected value")
require.Equal(t, tc.expectedConfig.Scoring, cfg.Scoring, "Scoring should match expected value")
require.Equal(t, tc.expectedConfig.Modifiers, cfg.Modifiers, "Modifiers should match expected value")
// all together
require.Equal(t, tc.expectedConfig, cfg, "config should match expected value")
} else {
// verify that error was received
require.Error(t, err)
// check validation errors
checkValidationErrs(t, tc.expectedErrs, err.Error())
// check that the config is nil
require.Nil(t, cfg, "Config should be nil")
}
})
}
}
func TestConfig_Validate(t *testing.T) {
type testCase struct {
name string
config func(*Config)
expectedErrs []string
}
tests := []struct {
group string
cases []testCase
}{
{"Valid", []testCase{
{name: "Default", config: func(cfg *Config) {}},
}},
{"Env", []testCase{
{name: "DBConnection Not Host:Port", config: func(cfg *Config) { cfg.Env.DBConnection = "invalid" }, expectedErrs: []string{"'DBConnection' failed on the 'hostname_port' tag"}},
{name: "DBConnection IP:Port", config: func(cfg *Config) { cfg.Env.DBConnection = "192.168.0.1:8123" }, expectedErrs: []string{}},
{name: "DBConnection Docker Service:Port", config: func(cfg *Config) { cfg.Env.DBConnection = "db:8123" }, expectedErrs: []string{}},
{name: "DBConnection localhost:Port", config: func(cfg *Config) { cfg.Env.DBConnection = "localhost:8123" }, expectedErrs: []string{}},
{name: "HTTPExtensionsFilePath Not Valid File", config: func(cfg *Config) { cfg.Env.HTTPExtensionsFilePath = "--" }, expectedErrs: []string{"'HTTPExtensionsFilePath' failed on the 'file' tag"}},
{name: "LogLevel < Min", config: func(cfg *Config) { cfg.Env.LogLevel = -1 }, expectedErrs: []string{"'LogLevel' failed on the 'min' tag"}},
{name: "LogLevel > Max", config: func(cfg *Config) { cfg.Env.LogLevel = 7 }, expectedErrs: []string{"'LogLevel' failed on the 'max' tag"}},
{name: "ThreatIntelCustomFeedsDirectory Not Valid Dir", config: func(cfg *Config) { cfg.Env.ThreatIntelCustomFeedsDirectory = "--" }, expectedErrs: []string{"'ThreatIntelCustomFeedsDirectory' failed on the 'dir' tag"}},
{name: "Empty Struct", config: func(cfg *Config) { cfg.Env = Env{} }, expectedErrs: []string{"'Env' failed on the 'required' tag"}},
}},
{"RITA", []testCase{
{name: "BatchSize < Range", config: func(cfg *Config) { cfg.RITA.BatchSize = 24999 }, expectedErrs: []string{"'BatchSize' failed on the 'gte' tag"}},
{name: "BatchSize > Range", config: func(cfg *Config) { cfg.RITA.BatchSize = 2000001 }, expectedErrs: []string{"'BatchSize' failed on the 'lte' tag"}},
{name: "MaxQueryExecutionTime < Range", config: func(cfg *Config) { cfg.RITA.MaxQueryExecutionTime = 0 }, expectedErrs: []string{"'MaxQueryExecutionTime' failed on the 'gte' tag"}},
{name: "MaxQueryExecutionTime > Range", config: func(cfg *Config) { cfg.RITA.MaxQueryExecutionTime = 2000001 }, expectedErrs: []string{"'MaxQueryExecutionTime' failed on the 'lte' tag"}},
{name: "MonthsToKeepHistoricalFirstSeen < Range", config: func(cfg *Config) { cfg.RITA.MonthsToKeepHistoricalFirstSeen = 0 }, expectedErrs: []string{"'MonthsToKeepHistoricalFirstSeen' failed on the 'gte' tag"}},
{name: "MonthsToKeepHistoricalFirstSeen > Range", config: func(cfg *Config) { cfg.RITA.MonthsToKeepHistoricalFirstSeen = 61 }, expectedErrs: []string{"'MonthsToKeepHistoricalFirstSeen' failed on the 'lte' tag"}},
{name: "ThreatIntel.OnlineFeeds Not URLs", config: func(cfg *Config) { cfg.RITA.ThreatIntel.OnlineFeeds = []string{"invalid"} }, expectedErrs: []string{"'OnlineFeeds[0]' failed on the 'url' tag"}},
{name: "Empty Struct", config: func(cfg *Config) { cfg.RITA = RITA{} }, expectedErrs: []string{"'RITA' failed on the 'required' tag"}},
}},
{"Filtering", []testCase{
// should never be set to empty
{name: "InternalSubnets Nil", config: func(cfg *Config) { cfg.Filtering.InternalSubnets = nil }, expectedErrs: []string{"'InternalSubnets' failed on the 'required' tag"}},
{name: "InternalSubnets Empty", config: func(cfg *Config) { cfg.Filtering.InternalSubnets = []util.Subnet{} }, expectedErrs: []string{"'InternalSubnets' failed on the 'gt' tag"}},
// should never be empty after mandatory subnets are added
{name: "NeverIncludedSubnets Nil", config: func(cfg *Config) { cfg.Filtering.NeverIncludedSubnets = nil }, expectedErrs: []string{"'NeverIncludedSubnets' failed on the 'required' tag"}},
{name: "NeverIncludedSubnets Empty", config: func(cfg *Config) { cfg.Filtering.NeverIncludedSubnets = []util.Subnet{} }, expectedErrs: []string{"'NeverIncludedSubnets' failed on the 'gt' tag"}},
{name: "AlwaysIncludedDomains Not Domains", config: func(cfg *Config) { cfg.Filtering.AlwaysIncludedDomains = []string{"notadomain"} }, expectedErrs: []string{"'AlwaysIncludedDomains[0]' failed on the 'wildcard_fqdn' tag"}},
{name: "AlwaysIncludedDomains Mixed Validity", config: func(cfg *Config) { cfg.Filtering.AlwaysIncludedDomains = []string{"valid.com", "notadomain"} }, expectedErrs: []string{"'AlwaysIncludedDomains[1]' failed on the 'wildcard_fqdn' tag"}},
{name: "NeverIncludedDomains Not Domains", config: func(cfg *Config) { cfg.Filtering.NeverIncludedDomains = []string{"notadomain"} }, expectedErrs: []string{"'NeverIncludedDomains[0]' failed on the 'wildcard_fqdn' tag"}},
{name: "NeverIncludedDomains Mixed Validity", config: func(cfg *Config) { cfg.Filtering.NeverIncludedDomains = []string{"valid.com", "notadomain"} }, expectedErrs: []string{"'NeverIncludedDomains[1]' failed on the 'wildcard_fqdn' tag"}},
{name: "Empty Struct", config: func(cfg *Config) { cfg.Filtering = Filtering{} }, expectedErrs: []string{"'Filtering' failed on the 'required' tag"}},
}},
{"Scoring", []testCase{
{name: "Empty Struct", config: func(cfg *Config) { cfg.Scoring = Scoring{} }, expectedErrs: []string{"'Scoring' failed on the 'required' tag"}},
}},
{"Beacon Scoring", []testCase{
{name: "UniqueConnectionThreshold < Range", config: func(cfg *Config) { cfg.Scoring.Beacon.UniqueConnectionThreshold = 3 }, expectedErrs: []string{"'UniqueConnectionThreshold' failed on the 'gte' tag"}},
{name: "TimestampScoreWeight < Range", config: func(cfg *Config) {
cfg.Scoring.Beacon = BeaconScoring{4, -0.25, 0.75, 0.25, 0.25, 10, 15, 0.08, 2, 15, ScoreThresholds{50, 75, 90, 100}}
}, expectedErrs: []string{"'TimestampScoreWeight' failed on the 'gte' tag"}},
{name: "DatasizeScoreWeight < Range", config: func(cfg *Config) {
cfg.Scoring.Beacon = BeaconScoring{4, 0.75, -0.25, 0.25, 0.25, 10, 15, 0.08, 2, 15, ScoreThresholds{50, 75, 90, 100}}
}, expectedErrs: []string{"'DatasizeScoreWeight' failed on the 'gte' tag"}},
{name: "DurationScoreWeight < Range", config: func(cfg *Config) {
cfg.Scoring.Beacon = BeaconScoring{4, 0.25, 0.75, -0.25, 0.25, 10, 15, 0.08, 2, 15, ScoreThresholds{50, 75, 90, 100}}
}, expectedErrs: []string{"'DurationScoreWeight' failed on the 'gte' tag"}},
{name: "HistogramScoreWeight < Range", config: func(cfg *Config) {
cfg.Scoring.Beacon = BeaconScoring{4, 0.25, 0.25, 0.75, -0.25, 10, 15, 0.08, 2, 15, ScoreThresholds{50, 75, 90, 100}}
}, expectedErrs: []string{"'HistogramScoreWeight' failed on the 'gte' tag"}},
{name: "Score Weight Sum != 1", config: func(cfg *Config) {
cfg.Scoring.Beacon.TimestampScoreWeight = 0.1
cfg.Scoring.Beacon.DatasizeScoreWeight = 0.1
cfg.Scoring.Beacon.DurationScoreWeight = 0.1
cfg.Scoring.Beacon.HistogramScoreWeight = 0.1
}, expectedErrs: []string{"'TimestampScoreWeight' failed on the 'beacon_weights' tag", "'DatasizeScoreWeight' failed on the 'beacon_weights' tag", "'DurationScoreWeight' failed on the 'beacon_weights' tag", "'HistogramScoreWeight' failed on the 'beacon_weights' tag"}},
{name: "DurationMinHoursSeen < Range", config: func(cfg *Config) { cfg.Scoring.Beacon.DurationMinHoursSeen = 0 }, expectedErrs: []string{"'DurationMinHoursSeen' failed on the 'gte' tag"}},
{name: "DurationMinHoursSeen > Range", config: func(cfg *Config) { cfg.Scoring.Beacon.DurationMinHoursSeen = 25 }, expectedErrs: []string{"'DurationMinHoursSeen' failed on the 'lte' tag"}},
{name: "DurationConsistencyIdealHoursSeen < Range", config: func(cfg *Config) { cfg.Scoring.Beacon.DurationConsistencyIdealHoursSeen = 0 }, expectedErrs: []string{"'DurationConsistencyIdealHoursSeen' failed on the 'gte' tag"}},
{name: "DurationConsistencyIdealHoursSeen > Range", config: func(cfg *Config) { cfg.Scoring.Beacon.DurationConsistencyIdealHoursSeen = 25 }, expectedErrs: []string{"'DurationConsistencyIdealHoursSeen' failed on the 'lte' tag"}},
{name: "HistogramModeSensitivity < Range", config: func(cfg *Config) { cfg.Scoring.Beacon.HistogramModeSensitivity = -1 }, expectedErrs: []string{"'HistogramModeSensitivity' failed on the 'gte' tag"}},
{name: "HistogramModeSensitivity > Range", config: func(cfg *Config) { cfg.Scoring.Beacon.HistogramModeSensitivity = 25 }, expectedErrs: []string{"'HistogramModeSensitivity' failed on the 'lte' tag"}},
{name: "HistogramBimodalOutlierRemoval < Range", config: func(cfg *Config) { cfg.Scoring.Beacon.HistogramBimodalOutlierRemoval = -1 }, expectedErrs: []string{"'HistogramBimodalOutlierRemoval' failed on the 'gte' tag"}},
{name: "HistogramBimodalOutlierRemoval > Range", config: func(cfg *Config) { cfg.Scoring.Beacon.HistogramBimodalOutlierRemoval = 25 }, expectedErrs: []string{"'HistogramBimodalOutlierRemoval' failed on the 'lte' tag"}},
{name: "HistogramBimodalMinHoursSeen < Range", config: func(cfg *Config) { cfg.Scoring.Beacon.HistogramBimodalMinHoursSeen = 2 }, expectedErrs: []string{"'HistogramBimodalMinHoursSeen' failed on the 'gte' tag"}},
{name: "HistogramBimodalMinHoursSeen > Range", config: func(cfg *Config) { cfg.Scoring.Beacon.HistogramBimodalMinHoursSeen = 25 }, expectedErrs: []string{"'HistogramBimodalMinHoursSeen' failed on the 'lte' tag"}},
{name: "ScoreThresholds Base < Min", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds.Base = -1 }, expectedErrs: []string{"'ScoreThresholds' failed on the 'score_thresholds_range' tag"}},
{name: "ScoreThresholds High > Max", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds.High = 101 }, expectedErrs: []string{"'ScoreThresholds' failed on the 'score_thresholds_range' tag"}},
{name: "ScoreThresholds Base == Low", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds = ScoreThresholds{75, 75, 90, 100} }, expectedErrs: []string{"'Base' failed on the 'ltfield' tag"}},
{name: "ScoreThresholds Base > Low", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds = ScoreThresholds{76, 75, 90, 100} }, expectedErrs: []string{"'Base' failed on the 'ltfield' tag"}},
{name: "ScoreThresholds Low > Med", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds = ScoreThresholds{50, 91, 90, 100} }, expectedErrs: []string{"'Low' failed on the 'ltfield' tag"}},
{name: "ScoreThresholds Low == Med", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds = ScoreThresholds{50, 90, 90, 100} }, expectedErrs: []string{"'Low' failed on the 'ltfield' tag"}},
{name: "ScoreThresholds Med > High", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds = ScoreThresholds{50, 75, 91, 90} }, expectedErrs: []string{"'Med' failed on the 'ltfield' tag"}},
{name: "ScoreThresholds Med == High", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds = ScoreThresholds{50, 75, 90, 90} }, expectedErrs: []string{"'Med' failed on the 'ltfield' tag"}},
{name: "Empty ScoreThresholds", config: func(cfg *Config) { cfg.Scoring.Beacon.ScoreThresholds = ScoreThresholds{} }, expectedErrs: []string{"'Base' failed on the 'ltfield' tag", "'Low' failed on the 'ltfield' tag", "'Med' failed on the 'ltfield' tag"}},
{name: "Empty Struct", config: func(cfg *Config) { cfg.Scoring.Beacon = BeaconScoring{} }, expectedErrs: []string{"'Beacon' failed on the 'required' tag"}},
}},
{"Threat Scoring", []testCase{
{name: "LongConnectionScoreThresholds Base < Min", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds.Base = 0 }, expectedErrs: []string{"'LongConnectionScoreThresholds' failed on the 'score_thresholds_range' tag"}},
{name: "LongConnectionScoreThresholds High > Max", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds.High = (24 * 3600) + 1 }, expectedErrs: []string{"'LongConnectionScoreThresholds' failed on the 'score_thresholds_range' tag"}},
{name: "LongConnectionScoreThresholds Base == Low", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{75, 75, 90, 100} }, expectedErrs: []string{"'Base' failed on the 'ltfield' tag"}},
{name: "LongConnectionScoreThresholds Base > Low", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{76, 75, 90, 100} }, expectedErrs: []string{"'Base' failed on the 'ltfield' tag"}},
{name: "LongConnectionScoreThresholds Low > Med", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{50, 91, 90, 100} }, expectedErrs: []string{"'Low' failed on the 'ltfield' tag"}},
{name: "LongConnectionScoreThresholds Low == Med", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{50, 90, 90, 100} }, expectedErrs: []string{"'Low' failed on the 'ltfield' tag"}},
{name: "LongConnectionScoreThresholds Med > High", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{50, 75, 91, 90} }, expectedErrs: []string{"'Med' failed on the 'ltfield' tag"}},
{name: "LongConnectionScoreThresholds Med == High", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{50, 75, 90, 90} }, expectedErrs: []string{"'Med' failed on the 'ltfield' tag"}},
{name: "Empty LongConnectionScoreThresholds", config: func(cfg *Config) { cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{} }, expectedErrs: []string{"'LongConnectionScoreThresholds' failed on the 'score_thresholds_range' tag"}},
{name: "C2ScoreThresholds Base < Min", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds.Base = 0 }, expectedErrs: []string{"'C2ScoreThresholds' failed on the 'score_thresholds_range' tag"}},
{name: "C2ScoreThresholds High Should Be Unlimited", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds.High = 2147483647 }}, // max int32
{name: "C2ScoreThresholds Base == Low", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds = ScoreThresholds{75, 75, 90, 100} }, expectedErrs: []string{"'Base' failed on the 'ltfield' tag"}},
{name: "C2ScoreThresholds Base > Low", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds = ScoreThresholds{76, 75, 90, 100} }, expectedErrs: []string{"'Base' failed on the 'ltfield' tag"}},
{name: "C2ScoreThresholds Low > Med", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds = ScoreThresholds{50, 91, 90, 100} }, expectedErrs: []string{"'Low' failed on the 'ltfield' tag"}},
{name: "C2ScoreThresholds Low == Med", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds = ScoreThresholds{50, 90, 90, 100} }, expectedErrs: []string{"'Low' failed on the 'ltfield' tag"}},
{name: "C2ScoreThresholds Med > High", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds = ScoreThresholds{50, 75, 91, 90} }, expectedErrs: []string{"'Med' failed on the 'ltfield' tag"}},
{name: "C2ScoreThresholds Med == High", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds = ScoreThresholds{50, 75, 90, 90} }, expectedErrs: []string{"'Med' failed on the 'ltfield' tag"}},
{name: "Empty C2ScoreThresholds", config: func(cfg *Config) { cfg.Scoring.C2ScoreThresholds = ScoreThresholds{} }, expectedErrs: []string{"'C2ScoreThresholds' failed on the 'score_thresholds_range' tag"}},
{name: "StrobeImpact Category Invalid", config: func(cfg *Config) { cfg.Scoring.StrobeImpact.Category = "invalid" }, expectedErrs: []string{"'StrobeImpact' failed on the 'impact_category' tag"}},
{name: "StrobeImpact Category Empty", config: func(cfg *Config) { cfg.Scoring.StrobeImpact.Category = "" }, expectedErrs: []string{"'StrobeImpact' failed on the 'impact_category' tag"}},
{name: "ThreatIntelImpact Category Invalid", config: func(cfg *Config) { cfg.Scoring.ThreatIntelImpact.Category = "invalid" }, expectedErrs: []string{"'ThreatIntelImpact' failed on the 'impact_category' tag"}},
{name: "ThreatIntelImpact Category Empty", config: func(cfg *Config) { cfg.Scoring.ThreatIntelImpact.Category = "" }, expectedErrs: []string{"'ThreatIntelImpact' failed on the 'impact_category' tag"}},
{name: "Empty Struct", config: func(cfg *Config) { cfg.Scoring.ThreatScoring = ThreatScoring{} }, expectedErrs: []string{"'ThreatScoring' failed on the 'required' tag"}},
}},
{"Modifiers", []testCase{
{name: "ThreatIntelScoreIncrease < Range", config: func(cfg *Config) { cfg.Modifiers.ThreatIntelScoreIncrease = -0.1 }, expectedErrs: []string{"'ThreatIntelScoreIncrease' failed on the 'gte' tag"}},
{name: "ThreatIntelScoreIncrease > Range", config: func(cfg *Config) { cfg.Modifiers.ThreatIntelScoreIncrease = 1.1 }, expectedErrs: []string{"'ThreatIntelScoreIncrease' failed on the 'lte' tag"}},
{name: "ThreatIntelDataSizeThreshold < Range", config: func(cfg *Config) { cfg.Modifiers.ThreatIntelDataSizeThreshold = 0 }, expectedErrs: []string{"'ThreatIntelDataSizeThreshold' failed on the 'gte' tag"}},
{name: "PrevalenceScoreIncrease < Range", config: func(cfg *Config) { cfg.Modifiers.PrevalenceScoreIncrease = -0.1 }, expectedErrs: []string{"'PrevalenceScoreIncrease' failed on the 'gte' tag"}},
{name: "PrevalenceScoreIncrease > Range", config: func(cfg *Config) { cfg.Modifiers.PrevalenceScoreIncrease = 1.1 }, expectedErrs: []string{"'PrevalenceScoreIncrease' failed on the 'lte' tag"}},
{name: "PrevalenceIncreaseThreshold < Range", config: func(cfg *Config) { cfg.Modifiers.PrevalenceIncreaseThreshold = -0.1 }, expectedErrs: []string{"'PrevalenceIncreaseThreshold' failed on the 'gte' tag"}},
{name: "PrevalenceIncreaseThreshold > Range, PrevalenceDecreaseThreshold", config: func(cfg *Config) { cfg.Modifiers.PrevalenceIncreaseThreshold = 1.1 }, expectedErrs: []string{"'PrevalenceIncreaseThreshold' failed on the 'lte' tag", "'PrevalenceDecreaseThreshold' failed on the 'gtfield' tag"}},
{name: "PrevalenceScoreDecrease < Range", config: func(cfg *Config) { cfg.Modifiers.PrevalenceScoreDecrease = -0.1 }, expectedErrs: []string{"'PrevalenceScoreDecrease' failed on the 'gte' tag"}},
{name: "PrevalenceScoreDecrease > Range", config: func(cfg *Config) { cfg.Modifiers.PrevalenceScoreDecrease = 1.1 }, expectedErrs: []string{"'PrevalenceScoreDecrease' failed on the 'lte' tag"}},
{name: "PrevalenceDecreaseThreshold < Range", config: func(cfg *Config) { cfg.Modifiers.PrevalenceDecreaseThreshold = -0.1 }, expectedErrs: []string{"'PrevalenceDecreaseThreshold' failed on the 'gte' tag"}},
{name: "PrevalenceDecreaseThreshold > Range", config: func(cfg *Config) { cfg.Modifiers.PrevalenceDecreaseThreshold = 1.1 }, expectedErrs: []string{"'PrevalenceDecreaseThreshold' failed on the 'lte' tag"}},
{name: "PrevalenceScoreDecreaseThreshold <= PrevalenceIncreaseThreshold", config: func(cfg *Config) { cfg.Modifiers.PrevalenceIncreaseThreshold = 1 }, expectedErrs: []string{"'PrevalenceDecreaseThreshold' failed on the 'gtfield' tag"}},
{name: "FirstSeenScoreIncrease < Range", config: func(cfg *Config) { cfg.Modifiers.FirstSeenScoreIncrease = -0.1 }, expectedErrs: []string{"'FirstSeenScoreIncrease' failed on the 'gte' tag"}},
{name: "FirstSeenScoreIncrease > Range", config: func(cfg *Config) { cfg.Modifiers.FirstSeenScoreIncrease = 1.1 }, expectedErrs: []string{"'FirstSeenScoreIncrease' failed on the 'lte' tag"}},
{name: "FirstSeenIncreaseThreshold < Range", config: func(cfg *Config) { cfg.Modifiers.FirstSeenIncreaseThreshold = -0.1 }, expectedErrs: []string{"'FirstSeenIncreaseThreshold' failed on the 'gte' tag"}},
{name: "FirstSeenIncreaseThreshold >= FirstSeenDecreaseThreshold", config: func(cfg *Config) { cfg.Modifiers.FirstSeenIncreaseThreshold = 31 }, expectedErrs: []string{"'FirstSeenDecreaseThreshold' failed on the 'gtfield' tag"}},
{name: "FirstSeenScoreDecrease < Range", config: func(cfg *Config) { cfg.Modifiers.FirstSeenScoreDecrease = -0.1 }, expectedErrs: []string{"'FirstSeenScoreDecrease' failed on the 'gte' tag"}},
{name: "FirstSeenScoreDecrease > Range", config: func(cfg *Config) { cfg.Modifiers.FirstSeenScoreDecrease = 1.1 }, expectedErrs: []string{"'FirstSeenScoreDecrease' failed on the 'lte' tag"}},
{name: "FirstSeenDecreaseThreshold < Range", config: func(cfg *Config) { cfg.Modifiers.FirstSeenDecreaseThreshold = 0 }, expectedErrs: []string{"'FirstSeenDecreaseThreshold' failed on the 'gte' tag"}},
{name: "FirstSeenDecreaseThreshold > Range", config: func(cfg *Config) { cfg.Modifiers.FirstSeenDecreaseThreshold = 91 }, expectedErrs: []string{"'FirstSeenDecreaseThreshold' failed on the 'lte' tag"}},
{name: "FirstSeenDecreaseThreshold <= FirstSeenIncreaseThreshold", config: func(cfg *Config) { cfg.Modifiers.FirstSeenDecreaseThreshold = 1 }, expectedErrs: []string{"'FirstSeenDecreaseThreshold' failed on the 'gtfield' tag"}},
{name: "MissingHostCountScoreIncrease < Range", config: func(cfg *Config) { cfg.Modifiers.MissingHostCountScoreIncrease = -0.1 }, expectedErrs: []string{"'MissingHostCountScoreIncrease' failed on the 'gte' tag"}},
{name: "MissingHostCountScoreIncrease > Range", config: func(cfg *Config) { cfg.Modifiers.MissingHostCountScoreIncrease = 1.1 }, expectedErrs: []string{"'MissingHostCountScoreIncrease' failed on the 'lte' tag"}},
{name: "RareSignatureScoreIncrease < Range", config: func(cfg *Config) { cfg.Modifiers.RareSignatureScoreIncrease = -0.1 }, expectedErrs: []string{"'RareSignatureScoreIncrease' failed on the 'gte' tag"}},
{name: "RareSignatureScoreIncrease > Range", config: func(cfg *Config) { cfg.Modifiers.RareSignatureScoreIncrease = 1.1 }, expectedErrs: []string{"'RareSignatureScoreIncrease' failed on the 'lte' tag"}},
{name: "C2OverDNSDirectConnScoreIncrease < Range", config: func(cfg *Config) { cfg.Modifiers.C2OverDNSDirectConnScoreIncrease = -0.1 }, expectedErrs: []string{"'C2OverDNSDirectConnScoreIncrease' failed on the 'gte' tag"}},
{name: "C2OverDNSDirectConnScoreIncrease > Range", config: func(cfg *Config) { cfg.Modifiers.C2OverDNSDirectConnScoreIncrease = 1.1 }, expectedErrs: []string{"'C2OverDNSDirectConnScoreIncrease' failed on the 'lte' tag"}},
{name: "MIMETypeMismatchScoreIncrease < Range", config: func(cfg *Config) { cfg.Modifiers.MIMETypeMismatchScoreIncrease = -0.1 }, expectedErrs: []string{"'MIMETypeMismatchScoreIncrease' failed on the 'gte' tag"}},
{name: "MIMETypeMismatchScoreIncrease > Range", config: func(cfg *Config) { cfg.Modifiers.MIMETypeMismatchScoreIncrease = 1.1 }, expectedErrs: []string{"'MIMETypeMismatchScoreIncrease' failed on the 'lte' tag"}},
{name: "Empty Struct", config: func(cfg *Config) { cfg.Modifiers = Modifiers{} }, expectedErrs: []string{"'Modifiers' failed on the 'required' tag"}},
}},
{"Zone Transfer", []testCase{
{name: "Disabled, Empty Domain Name", config: func(cfg *Config) { cfg.ZoneTransfer.DomainName = "" }},
{name: "Disabled, Empty Name Server", config: func(cfg *Config) { cfg.ZoneTransfer.NameServer = "" }},
{name: "Enabled, Empty Domain Name", config: func(cfg *Config) { cfg.ZoneTransfer.DomainName = ""; cfg.ZoneTransfer.Enabled = true }, expectedErrs: []string{"'DomainName' failed on the 'required_if' tag", "'NameServer' failed on the 'required_if' tag"}},
{name: "Enabled, Empty Name Server", config: func(cfg *Config) { cfg.ZoneTransfer.NameServer = ""; cfg.ZoneTransfer.Enabled = true }, expectedErrs: []string{"'DomainName' failed on the 'required_if' tag", "'NameServer' failed on the 'required_if' tag"}},
{name: "Invalid Domain Name", config: func(cfg *Config) {
cfg.ZoneTransfer.DomainName = "192.168.0.1"
cfg.ZoneTransfer.NameServer = "dc1.bug.corp:53"
cfg.ZoneTransfer.Enabled = true
}, expectedErrs: []string{"'DomainName' failed on the 'fqdn' tag"}},
{name: "Invalid Name Server", config: func(cfg *Config) {
cfg.ZoneTransfer.DomainName = "bug.corp."
cfg.ZoneTransfer.NameServer = "bug.corp"
cfg.ZoneTransfer.Enabled = true
}, expectedErrs: []string{"'NameServer' failed on the 'hostname_port' tag"}},
{name: "Disabled but Valid", config: func(cfg *Config) {
cfg.ZoneTransfer.DomainName = "bug.corp."
cfg.ZoneTransfer.NameServer = "dc1.bug.corp:53"
}},
}},
}
for _, test := range tests {
t.Run(test.group, func(t *testing.T) {
for _, tc := range test.cases {
t.Run(tc.name, func(t *testing.T) {
// get default config
cfg := GetDefaultConfig()
// set env
require.NoError(t, cfg.setEnv())
// apply the test case config updates
tc.config(&cfg)
err := cfg.Validate()
if len(tc.expectedErrs) == 0 {
require.NoError(t, err)
} else {
require.Error(t, err)
checkValidationErrs(t, tc.expectedErrs, err.Error())
}
})
}
})
}
}
func TestConfig_ParseImpactCategoryScores(t *testing.T) {
t.Run("Valid Categories", func(t *testing.T) {
cfg := &Config{Scoring: Scoring{ThreatScoring: ThreatScoring{StrobeImpact: ScoreImpact{Category: HighThreat}, ThreatIntelImpact: ScoreImpact{Category: LowThreat}}}}
require.NoError(t, cfg.parseImpactCategoryScores())
require.InDelta(t, float64(HIGH_CATEGORY_SCORE), cfg.Scoring.StrobeImpact.Score, 0.0001, "StrobeImpact.Score should match expected value")
require.InDelta(t, float64(LOW_CATEGORY_SCORE), cfg.Scoring.ThreatIntelImpact.Score, 0.0001, "ThreatIntelImpact.Score should match expected value")
})
t.Run("More Valid Categories", func(t *testing.T) {
cfg := &Config{Scoring: Scoring{ThreatScoring: ThreatScoring{StrobeImpact: ScoreImpact{Category: MediumThreat}, ThreatIntelImpact: ScoreImpact{Category: NoneThreat}}}}
require.NoError(t, cfg.parseImpactCategoryScores())
require.InDelta(t, float64(MEDIUM_CATEGORY_SCORE), cfg.Scoring.StrobeImpact.Score, 0.0001, "StrobeImpact.Score should match expected value")
require.InDelta(t, float64(NONE_CATEGORY_SCORE), cfg.Scoring.ThreatIntelImpact.Score, 0.0001, "ThreatIntelImpact.Score should match expected value")
})
t.Run("Invalid Category for StrobeImpact", func(t *testing.T) {
cfg := &Config{Scoring: Scoring{ThreatScoring: ThreatScoring{StrobeImpact: ScoreImpact{Category: "unknown"}, ThreatIntelImpact: ScoreImpact{Category: LowThreat}}}}
err := cfg.parseImpactCategoryScores()
require.Error(t, err)
require.Contains(t, err.Error(), errInvalidImpactCategory.Error())
})
t.Run("Invalid Category for ThreatIntelImpact", func(t *testing.T) {
cfg := &Config{Scoring: Scoring{ThreatScoring: ThreatScoring{StrobeImpact: ScoreImpact{Category: HighThreat}, ThreatIntelImpact: ScoreImpact{Category: "invalid"}}}}
err := cfg.parseImpactCategoryScores()
require.Error(t, err)
require.Contains(t, err.Error(), errInvalidImpactCategory.Error())
})
}
func TestConfig_Reset(t *testing.T) {
testCases := []struct {
name string
cfg *Config
expectedErr bool
}{
{
name: "Valid Confgig - No Env Changes",
cfg: func() *Config {
cfg := GetDefaultConfig()
require.NoError(t, cfg.setEnv())
cfg.RITA = RITA{false, 25000, 1, 1, ThreatIntel{[]string{"https://chickenstrip.com"}}}
cfg.Filtering.InternalSubnets = []util.Subnet{util.NewSubnet(&net.IPNet{IP: net.IP{10, 0, 0, 0}.To16(), Mask: net.CIDRMask(8+96, 128)})}
cfg.Filtering.FilterExternalToInternal = false
cfg.Scoring.Beacon = BeaconScoring{4, 0.6, 0.1, 0.1, 0.2, 1, 1, 0, 0, 3, ScoreThresholds{0, 1, 2, 3}}
cfg.Scoring.LongConnectionScoreThresholds = ScoreThresholds{1, 2, 3, 4}
cfg.Scoring.C2ScoreThresholds = ScoreThresholds{1, 2, 3, 4}
cfg.Scoring.StrobeImpact = ScoreImpact{Category: LowThreat, Score: LOW_CATEGORY_SCORE}
cfg.Scoring.ThreatIntelImpact = ScoreImpact{Category: LowThreat, Score: LOW_CATEGORY_SCORE}
cfg.Modifiers = Modifiers{0, 1, 0, 0, 0, 0.1, 0, 1, 0, 1.1, 0, 0, 0, 0}
require.NoError(t, cfg.Validate())
return &cfg
}(),
},
{
name: "Invalid Config - No Env Changes",
cfg: func() *Config {
cfg := GetDefaultConfig()
require.NoError(t, cfg.setEnv())
cfg.RITA = RITA{false, 0, 0, 0, ThreatIntel{[]string{"invalid"}}}
cfg.Filtering = Filtering{}
cfg.Scoring.Beacon = BeaconScoring{1, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 1, 1, ScoreThresholds{-1, -2, -3, -4}}
return &cfg
}(),
},
{
name: "Valid Config - Valid Env Changes",
cfg: func() *Config {
cfg := GetDefaultConfig()
require.NoError(t, cfg.setEnv())
cfg.Env.DBConnection = "chickenstrip:9999"
cfg.Env.LogLevel = 6
cfg.RITA = RITA{false, 25000, 1, 1, ThreatIntel{[]string{"https://chickenstrip.com"}}}
cfg.Filtering.InternalSubnets = []util.Subnet{util.NewSubnet(&net.IPNet{IP: net.IP{10, 0, 0, 0}.To16(), Mask: net.CIDRMask(8+96, 128)})}
cfg.Filtering.FilterExternalToInternal = false
require.NoError(t, cfg.Validate())
return &cfg
}(),
},
{
name: "Invalid Config - Invalid Env Changes",
cfg: func() *Config {
cfg := GetDefaultConfig()
require.NoError(t, cfg.setEnv())
cfg.Env.DBConnection = "invalid"
cfg.Env.LogLevel = 10
cfg.RITA = RITA{false, 25000, 1, 1, ThreatIntel{[]string{"https://chickenstrip.com"}}}
cfg.Filtering.InternalSubnets = []util.Subnet{util.NewSubnet(&net.IPNet{IP: net.IP{10, 0, 0, 0}.To16(), Mask: net.CIDRMask(8+96, 128)})}
cfg.Filtering.FilterExternalToInternal = false
return &cfg
}(),
expectedErr: true,
},
{
name: "Invalid Config - Valid Env Changes",
cfg: func() *Config {
cfg := GetDefaultConfig()
require.NoError(t, cfg.setEnv())
cfg.Env.DBConnection = "chickenstrip:9999"
cfg.Env.LogLevel = 6
cfg.RITA = RITA{false, 0, 0, 0, ThreatIntel{[]string{"invalid"}}}
cfg.Filtering = Filtering{}
cfg.Scoring.Beacon = BeaconScoring{1, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 1, 1, ScoreThresholds{-1, -2, -3, -4}}
return &cfg
}(),
},
{
name: "Empty Except for Env",
cfg: func() *Config {
cfg := &Config{}
require.NoError(t, cfg.setEnv())
cfg.Env.DBConnection = "chickenstrip:9999"
cfg.Env.LogLevel = 6
return cfg
}(),
},
{
name: "Empty Struct",
cfg: &Config{},
expectedErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// get default config
origConfig := GetDefaultConfig()
require.NoError(t, origConfig.setEnv())
// get the modified test config
cfg := *tc.cfg
// store environment variables from the modified config
env := cfg.Env
// reset the config
err := cfg.Reset()
if tc.expectedErr {
require.Error(t, err, "resetting config should produce an error")
} else {
require.NoError(t, err, "resetting config should not produce an error")
// verify that if the environment variables were changed, there were NOT reset
if env != origConfig.Env {
require.Equal(t, env, cfg.Env, "env should not have been reset")
require.NotEqual(t, origConfig.Env, cfg.Env, "env should not match default config")
// set orig config env to modified so we can just do a straight comparison on the structs below, since
// all other fields should be the same
origConfig.Env = env
}
// verify that fields were reset to default values
require.Equal(t, origConfig, cfg, "config should match default config")
// verify the config
err = cfg.Validate()
require.NoError(t, err, "validating config should not produce an error")
}
})
}
}
func TestGetDefaultConfig(t *testing.T) {
// get a copy of the default config variable that function is supposed to return
origConfigVar := defaultConfig()
// create *Config object from the default config variable
origCfg := &origConfigVar
// set the environment variables
require.NoError(t, origCfg.setEnv())
// validate
require.NoError(t, origCfg.Validate(), "config should be valid")
// get the default config object from the function
cfg := GetDefaultConfig()
require.NotNil(t, cfg, "config should not be nil")
// set env variables
require.NoError(t, cfg.setEnv())
// validate
require.NoError(t, cfg.Validate(), "config should be valid")
// verify version got set (gets set in function if it wasn't set already)
require.Equal(t, "dev", Version, "version should be 'dev'")
// verify the acquired config is the same as the default config variable
require.Equal(t, origCfg, &cfg, "config should match expected value")
}
func TestValidateScoreThresholdsRange(t *testing.T) {
tests := []struct {
name string
thresholds ScoreThresholds
min int32
max int32
expectedError bool
}{
{
name: "valid thresholds, (0 - 10)",
thresholds: ScoreThresholds{Base: 0, Low: 1, Med: 2, High: 3},
min: 0,
max: 10,
expectedError: false,
},
{
name: "valid beacon thresholds, (0 - 100)",
thresholds: ScoreThresholds{Base: 50, Low: 75, Med: 90, High: 100},
min: 0,
max: 100,
expectedError: false,
},
{
name: "valid long conn thresholds, (0 - 24*3600)",
thresholds: ScoreThresholds{Base: 3600, Low: 4 * 3600, Med: 8 * 3600, High: 12 * 3600},
min: 0,
max: 24 * 3600,
expectedError: false,
},
{
name: "valid c2 thresholds, (0 - no max)",
thresholds: ScoreThresholds{Base: 100, Low: 500, Med: 800, High: 1000},
min: 0,
max: -1,
expectedError: false,
},
{
name: "invalid thresholds (out of range - max)",
thresholds: ScoreThresholds{Base: 0, Low: 1, Med: 2, High: 3},
min: 0,
max: 2,
expectedError: true,
},
{
name: "invalid thresholds (out of range - min)",
thresholds: ScoreThresholds{Base: 0, Low: 1, Med: 2, High: 3},
min: 1,
max: 10,
expectedError: true,
},
// note: score thresholds being in increasing order and unique is validated by the ltfield tags in the struct
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
err := validateScoreThresholdsRange(test.thresholds, test.min, test.max)
require.Equal(test.expectedError, err != nil, "Expected error to be %v, got %v", test.expectedError, err)
})
}
}
func TestValidateImpactCategory(t *testing.T) {
tests := []struct {
name string
impact ImpactCategory
expectedError bool
}{
{
name: "high impact category",
impact: "high",
expectedError: false,
},
{
name: "none impact category",
impact: "none",
expectedError: false,
},
{
name: "invalid impact category",
impact: "iaminvalid",
expectedError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
err := ValidateImpactCategory(test.impact)
require.Equal(test.expectedError, err != nil, "Expected error to be %v, got %v", test.expectedError, err)
})
}
}
func TestGetScoreFromImpactCategory(t *testing.T) {
tests := []struct {
name string
impact ImpactCategory
expectedScore float64
expectedError error
}{
{
name: "high impact category",
impact: "high",
expectedScore: HIGH_CATEGORY_SCORE,
expectedError: nil,
},
{
name: "medium impact category",
impact: "medium",
expectedScore: MEDIUM_CATEGORY_SCORE,
expectedError: nil,
},
{
name: "low impact category",
impact: "low",
expectedScore: LOW_CATEGORY_SCORE,
expectedError: nil,
},
{
name: "none impact category",
impact: "none",
expectedScore: NONE_CATEGORY_SCORE,
expectedError: nil,
},
{
name: "invalid impact category",
impact: "iaminvalid",
expectedScore: 0,
expectedError: errInvalidImpactCategory,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
score, err := GetScoreFromImpactCategory(test.impact)
require.Equal(test.expectedError, err, "error should match expected value")
require.InDelta(test.expectedScore, score, 0.0001, "score should match expected value")
})
}
}
func TestGetImpactCategoryFromScore(t *testing.T) {
tests := []struct {
name string
score float64
expectedImpact ImpactCategory
}{
{
name: "High Category Score - Upper Bound",
// score > MEDIUM_CATEGORY_SCORE
score: float64(HIGH_CATEGORY_SCORE),
expectedImpact: HighThreat,
},
{
name: "High Category Score - Lower Bound",
// score > MEDIUM_CATEGORY_SCORE
score: float64(MEDIUM_CATEGORY_SCORE),
expectedImpact: HighThreat,
},
{
name: "Medium Category Score - Upper Bound",
// score >= LOW_CATEGORY_SCORE && score <= MEDIUM_CATEGORY_SCORE
score: float64(MEDIUM_CATEGORY_SCORE) - 0.001,
expectedImpact: MediumThreat,
},
{
name: "Medium Category Score - Lower Bound",
// score >= LOW_CATEGORY_SCORE && score <= MEDIUM_CATEGORY_SCORE
score: float64(LOW_CATEGORY_SCORE),
expectedImpact: MediumThreat,
},
{
name: "Low Category Score - Upper Bound",
// score >= NONE_CATEGORY_SCORE && score < LOW_CATEGORY_SCORE
score: float64(LOW_CATEGORY_SCORE) - 0.001,
expectedImpact: LowThreat,
},
{
name: "Low Category Score - Lower Bound",
// score >= NONE_CATEGORY_SCORE && score < LOW_CATEGORY_SCORE
score: float64(NONE_CATEGORY_SCORE),
expectedImpact: LowThreat,
},
{
name: "None Category Score",
// score < NONE_CATEGORY_SCORE
score: float64(NONE_CATEGORY_SCORE) - 0.001,
expectedImpact: NoneThreat,
},
{
name: "Invalid Score",
score: -1,
expectedImpact: NoneThreat,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
impact := GetImpactCategoryFromScore(test.score)
require.Equal(test.expectedImpact, impact, "Expected impact to be %v, got %v", test.expectedImpact, impact)
})
}
}
func TestScoreThresholds_ToMap(t *testing.T) {
tests := []struct {
name string
input ScoreThresholds
expected map[string]int32
}{
{
name: "Valid Values",
input: ScoreThresholds{Base: 10, Low: 20, Med: 30, High: 40},
expected: map[string]int32{"base": 10, "low": 20, "med": 30, "high": 40},
},
{
name: "Zero Values",
input: ScoreThresholds{Base: 0, Low: 0, Med: 0, High: 0},
expected: map[string]int32{"base": 0, "low": 0, "med": 0, "high": 0},
},
{
name: "Empty Struct",
input: ScoreThresholds{},
expected: map[string]int32{"base": 0, "low": 0, "med": 0, "high": 0},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := test.input.ToMap()
require.Equal(t, test.expected, result)
})
}
}
func TestScoreImpact_String(t *testing.T) {
tests := []struct {
name string
input ScoreImpact
expected string
}{
{
name: "Valid ImpactCategory",
input: ScoreImpact{Category: HighThreat},
expected: string(HighThreat),
},
{
name: "Empty ImpactCategory",
input: ScoreImpact{Category: ImpactCategory("")},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := test.input.String()
require.Equal(t, test.expected, result)
})
}
}
func TestScoreImpact_Scan(t *testing.T) {
tests := []struct {
name string
input any
expected ScoreImpact
expectErr error
}{
{
name: "Valid Impact Category",
input: string(HighThreat),
expected: ScoreImpact{Category: HighThreat, Score: HIGH_CATEGORY_SCORE},
},
{
name: "Invalid Impact Category",
input: "invalid",
expectErr: errInvalidImpactCategory,
},
{
name: "Empty Impact Category",
input: "",
expectErr: errInvalidImpactCategory,
},
{
name: "Invalid Non-string Input",
input: 123,
expectErr: fmt.Errorf("cannot scan int into ScoreImpact"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var result ScoreImpact
err := result.Scan(test.input)
if test.expectErr != nil {
require.Error(t, err)
require.ErrorContains(t, err, test.expectErr.Error())
} else {
require.NoError(t, err)
require.Equal(t, test.expected, result)
}
})
}
}
func checkValidationErrs(t *testing.T, errs []string, response string) {
t.Helper()
require.NotEmpty(t, response, "error response should not be empty")
// split the response by newlines
responseErrs := strings.Split(response, "\n")
// remove any empty strings
responseErrs = slices.DeleteFunc(responseErrs, func(val string) bool { return val == "" })
require.Len(t, responseErrs, len(errs))
for _, err := range errs {
require.Contains(t, response, err)
}
}