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

705 lines
25 KiB
Go

package integration_test
import (
"context"
"net"
"testing"
"time"
"github.com/activecm/rita/v5/analysis"
"github.com/activecm/rita/v5/cmd"
"github.com/activecm/rita/v5/config"
"github.com/activecm/rita/v5/database"
"github.com/activecm/rita/v5/progressbar"
"github.com/activecm/rita/v5/util"
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/charmbracelet/bubbles/progress"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"golang.org/x/sync/errgroup"
)
type FilterTestSuite struct {
suite.Suite
cfg *config.Config
}
func TestFilters(t *testing.T) {
suite.Run(t, new(FilterTestSuite))
}
// Reset config after each test since these tests load the config from a file
func (it *FilterTestSuite) SetupSuite() {
t := it.T()
afs := afero.NewOsFs()
cfg, err := config.ReadFileConfig(afs, ConfigPath)
require.NoError(t, err)
it.cfg = cfg
}
func (it *FilterTestSuite) SetupTest() {
t := it.T()
err := it.cfg.Reset()
require.NoError(t, err)
}
func (it *FilterTestSuite) TearDownSuite() {
t := it.T()
err := it.cfg.Reset()
require.NoError(t, err)
}
func (it *FilterTestSuite) TestNeverIncludeSubnets() {
t := it.T()
// set up file system interface
afs := afero.NewMemMapFs()
afs2 := afero.NewOsFs()
err := afero.WriteFile(afs, "testsuite_config.hjson", []byte(`
{
filtering: {
filter_external_to_internal: true,
internal_subnets: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fd00::/8"],
never_included_subnets: ["10.55.100.0/24"],
},
threat_intel: {
online_feeds: ["https://feodotracker.abuse.ch/downloads/ipblocklist.txt"],
custom_feeds_directory: "./deployment/threat_intel_feeds"
},
http_extensions_file_path: "../deployment/http_extensions_list.csv"
}
`), 0755)
require.NoError(t, err)
cfg, err := config.ReadFileConfig(afs, "testsuite_config.hjson")
require.NoError(t, err)
cfg.Env.DBConnection = dockerInfo.clickhouseConnection
it.cfg = cfg
require.Contains(t, cfg.Filtering.NeverIncludedSubnets, util.NewSubnet(&net.IPNet{IP: net.IP{10, 55, 100, 0}, Mask: net.IPMask{255, 255, 255, 0}}))
// // import data
_, err = cmd.RunImportCmd(time.Now(), cfg, afs2, "../test_data/valid_tsv", "never_include_subnet", false, true)
require.NoError(t, err)
// connect to database
db, err := database.ConnectToDB(context.Background(), "never_include_subnet", cfg, nil)
require.NoError(t, err)
var count uint64
// verify that not all connections in 10.0.0.0/8 were filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM conn
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.0.0.0/104') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.0.0.0/104')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 14452-12584, count, "conn table should contain 1868 entries in 10.0.0.0/8, got: %d", count)
// 5531 in 10.55.100.0/24
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM ssl
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.0.0.0/104') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.0.0.0/104')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 5615-(5531), count, "ssl table should contain 63 entries in 10.0.0.0/8, got: %d", count)
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM http
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.0.0.0/104') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.0.0.0/104')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 2004-(1982), count, "http table should contain 22 entries in 10.0.0.0/8, got: %d", count)
// verify that all connections in 10.55.100.0/24 were filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM conn
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "conn table shouldn't contain any entries in 10.55.100.0/24")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM http
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "http table shouldn't contain any entries in 10.55.100.0/24")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM ssl
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "ssl table shouldn't contain any entries in 10.55.100.0/24")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM dns
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "dns table shouldn't contain any entries in 10.55.100.0/24")
}
func (it *FilterTestSuite) TestNeverIncludeDomains() {
t := it.T()
// set up file system interface
afs := afero.NewMemMapFs()
afs2 := afero.NewOsFs()
err := afero.WriteFile(afs, "testsuite_config2.hjson", []byte(`
{
filtering: {
filter_external_to_internal: true,
internal_subnets: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fd00::/8"],
never_included_domains: ["*.microsoft.com", "businessinsider.com"]
},
threat_intel: {
online_feeds: ["https://feodotracker.abuse.ch/downloads/ipblocklist.txt"],
custom_feeds_directory: "./deployment/threat_intel_feeds"
},
http_extensions_file_path: "../deployment/http_extensions_list.csv"
}
`), 0755)
require.NoError(t, err)
cfg, err := config.ReadFileConfig(afs, "testsuite_config2.hjson")
require.NoError(t, err)
cfg.Env.DBConnection = dockerInfo.clickhouseConnection
it.cfg = cfg
require.Contains(t, cfg.Filtering.NeverIncludedDomains, "*.microsoft.com")
require.Contains(t, cfg.Filtering.NeverIncludedDomains, "businessinsider.com")
// // import data
_, err = cmd.RunImportCmd(time.Now(), cfg, afs2, "../test_data/valid_tsv", "never_include_domain", false, true)
require.NoError(t, err)
// connect to database
db, err := database.ConnectToDB(context.Background(), "never_include_domain", cfg, nil)
require.NoError(t, err)
var count uint64
// verify that all connections w/ fqdns ending in .microsoft.com are filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM http
WHERE cutToFirstSignificantSubdomain(host) = 'microsoft.com'
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "http table shouldn't contain any entries with host ending in .microsoft.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM ssl
WHERE cutToFirstSignificantSubdomain(server_name) = 'microsoft.com'
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "ssl table shouldn't contain any entries with server_name ending in .microsoft.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM dns
WHERE cutToFirstSignificantSubdomain(query) = 'microsoft.com'
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "dns table shouldn't contain any entries with query ending in .microsoft.com")
// verify that not all domains in .businessinsider.com are filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM dns
WHERE cutToFirstSignificantSubdomain(query) = 'businessinsider.com'
`).Scan(&count)
require.NoError(t, err)
require.Greater(t, count, uint64(0), "dns table should contain at least one entry with a query ending in .businessinsider.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM http
WHERE cutToFirstSignificantSubdomain(host) = 'businessinsider.com'
`).Scan(&count)
require.NoError(t, err)
require.Greater(t, count, uint64(0), "http table should contain at least one entry with a host ending in .businessinsider.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM ssl
WHERE cutToFirstSignificantSubdomain(server_name) = 'businessinsider.com'
`).Scan(&count)
require.NoError(t, err)
require.Greater(t, count, uint64(0), "ssl table should contain at least one entry with a server_name ending in .businessinsider.com")
// verify that businessinsider.com is filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM dns
WHERE query = 'businessinsider.com'
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "dns table shouldn't contain any entries with a query of .businessinsider.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM http
WHERE host = 'businessinsider.com'
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "http table shouldn't contain any entries with a query of .businessinsider.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM ssl
WHERE server_name = 'businessinsider.com'
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "ssl table shouldn't contain any entries with a query of .businessinsider.com")
}
func (it *FilterTestSuite) TestAlwaysIncludeSubnets() {
t := it.T()
// set up file system interface
afs := afero.NewMemMapFs()
afs2 := afero.NewOsFs()
err := afero.WriteFile(afs, "testsuite_config3.hjson", []byte(`
{
filtering: {
filter_external_to_internal: true,
internal_subnets: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fd00::/8"],
never_included_subnets: ["10.0.0.0/8"],
always_included_subnets: ["10.55.100.0/24"]
},
threat_intel: {
online_feeds: ["https://feodotracker.abuse.ch/downloads/ipblocklist.txt"],
custom_feeds_directory: "./deployment/threat_intel_feeds"
},
http_extensions_file_path: "../deployment/http_extensions_list.csv"
}
`), 0755)
require.NoError(t, err)
cfg, err := config.ReadFileConfig(afs, "testsuite_config3.hjson")
require.NoError(t, err)
cfg.Env.DBConnection = dockerInfo.clickhouseConnection
it.cfg = cfg
require.Contains(t, cfg.Filtering.NeverIncludedSubnets, util.NewSubnet(&net.IPNet{IP: net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0}}), "never included subnets should contain 10.0.0.0/8")
require.Contains(t, cfg.Filtering.AlwaysIncludedSubnets, util.NewSubnet(&net.IPNet{IP: net.IP{10, 55, 100, 0}, Mask: net.IPMask{255, 255, 255, 0}}), "always included subnets should contain 10.55.100.0/24")
// // import data
_, err = cmd.RunImportCmd(time.Now(), cfg, afs2, "../test_data/valid_tsv", "always_include_subnet", false, true)
require.NoError(t, err)
// connect to database
db, err := database.ConnectToDB(context.Background(), "always_include_subnet", cfg, nil)
require.NoError(t, err)
var count uint64
// verify that not all connections in 10.0.0.0/8 were filtered
conn := 12591
http := 1982 + 176 // regular http + http missing host
ssl := 5531
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM conn
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.0.0.0/104') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.0.0.0/104')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, conn, count, "conn table should contain %d entries in 10.0.0.0/8, got: %d", conn, count)
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM ssl
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.0.0.0/104') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.0.0.0/104')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, ssl, count, "ssl table should contain %d entries in 10.0.0.0/8, got: %d", ssl, count)
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM http
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.0.0.0/104') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.0.0.0/104')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, http, count, "http table should contain %d entries in 10.0.0.0/8, got: %d", http, count)
// verify that all connections in 10.55.100.0/24 were filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM conn
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, conn, count, "conn table shouldn contain %d any entries in 10.55.100.0/24, got: %d", conn, count)
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM http
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, http, count, "http table shouldn contain %d entries in 10.55.100.0/24, got: %d", http, count)
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM ssl
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, ssl, count, "ssl table should contain %d entries in 10.55.100.0/24", ssl, count)
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count(DISTINCT hash) FROM dns
WHERE isIPAddressInRange(IPv6NumToString(src), '::ffff:10.55.100.0/120') OR isIPAddressInRange(IPv6NumToString(dst), '::ffff:10.55.100.0/120')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 1, count, "dns table should contain 1 entry in 10.55.100.0/24")
}
func (it *FilterTestSuite) TestAlwaysIncludeDomains() {
t := it.T()
// set up file system interface
afs := afero.NewMemMapFs()
afs2 := afero.NewOsFs()
err := afero.WriteFile(afs, "testsuite_config4.hjson", []byte(`
{
filtering: {
filter_external_to_internal: true,
internal_subnets: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fd00::/8"],
never_included_domains: ["*.microsoft.com", "businessinsider.com"],
always_included_domains: ["*.mp.microsoft.com", "analytics.businessinsider.com"]
},
threat_intel: {
online_feeds: ["https://feodotracker.abuse.ch/downloads/ipblocklist.txt"],
custom_feeds_directory: "./deployment/threat_intel_feeds"
},
http_extensions_file_path: "../deployment/http_extensions_list.csv"
}
`), 0755)
require.NoError(t, err)
cfg, err := config.ReadFileConfig(afs, "testsuite_config4.hjson")
require.NoError(t, err)
cfg.Env.DBConnection = dockerInfo.clickhouseConnection
it.cfg = cfg
require.Contains(t, cfg.Filtering.NeverIncludedDomains, "*.microsoft.com")
require.Contains(t, cfg.Filtering.NeverIncludedDomains, "businessinsider.com")
require.Contains(t, cfg.Filtering.AlwaysIncludedDomains, "*.mp.microsoft.com")
require.Contains(t, cfg.Filtering.AlwaysIncludedDomains, "analytics.businessinsider.com")
// // import data
_, err = cmd.RunImportCmd(time.Now(), cfg, afs2, "../test_data/valid_tsv", "always_include_domain", false, true)
require.NoError(t, err)
// connect to database
db, err := database.ConnectToDB(context.Background(), "always_include_domain", cfg, nil)
require.NoError(t, err)
var count uint64
// verify that all connections w/ fqdns ending in .microsoft.com (but not *.mp.microsoft.com) are filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM http
WHERE cutToFirstSignificantSubdomain(host) = 'microsoft.com' AND NOT endsWith(host, '.mp.microsoft.com')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "http table shouldn't contain any entries with host ending in .microsoft.com (but not *.mp.microsoft.com)")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM ssl
WHERE cutToFirstSignificantSubdomain(server_name) = 'microsoft.com' AND NOT endsWith(server_name, '.mp.microsoft.com')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "ssl table shouldn't contain any entries with server_name ending in .microsoft.com (but not *.mp.microsoft.com)")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM dns
WHERE cutToFirstSignificantSubdomain(query) = 'microsoft.com' AND NOT endsWith(query, '.mp.microsoft.com')
`).Scan(&count)
require.NoError(t, err)
require.EqualValues(t, 0, count, "dns table shouldn't contain any entries with query ending in .microsoft.com (but not *.mp.microsoft.com)")
// verify that connections w/ fqdns ending in *.mp.microsoft.com are not filtered
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM http
WHERE endsWith(host, '.mp.microsoft.com')
`).Scan(&count)
require.NoError(t, err)
require.Greater(t, count, uint64(0), "http table should contain at least 1 entry with host ending in .mp.microsoft.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM ssl
WHERE endsWith(server_name, '.mp.microsoft.com')
`).Scan(&count)
require.NoError(t, err)
require.Greater(t, count, uint64(0), "ssl table should contain at least 1 entry with server name ending in .microsoft.com")
err = db.Conn.QueryRow(db.GetContext(), `
SELECT count() FROM dns
WHERE endsWith(query, '.mp.microsoft.com')
`).Scan(&count)
require.NoError(t, err)
require.Greater(t, count, uint64(0), "dns table should contain at least 1 entry with query ending in .microsoft.com")
tables := []struct {
name string
field string
}{{name: "dns", field: "query"}, {name: "http", field: "host"}, {name: "ssl", field: "server_name"}}
domains := []struct {
d string
shouldBeFiltered bool
}{{d: "businessinsider.com", shouldBeFiltered: true}, {d: "static2.businessinsider.com", shouldBeFiltered: false}}
for _, domain := range domains {
for _, table := range tables {
chCtx := db.QueryParameters(clickhouse.Parameters{
"table": table.name,
"field": table.field,
"domain": domain.d,
})
// verify that not all subdomains in this domain are filtered (wildcard shouldn't apply)
err = db.Conn.QueryRow(chCtx, `
SELECT count() FROM {table:Identifier}
WHERE endsWith({field:Identifier}, {domain:String})
`).Scan(&count)
require.NoError(t, err)
require.Greater(t, count, uint64(0), "%s table should contain at least one entry with a domain that ends in .%s", table.name, domain.d)
// verify that domain is filtered (if it should be)
err = db.Conn.QueryRow(chCtx, `
SELECT count() FROM {table:Identifier}
WHERE {field:Identifier} = {domain:String}
`).Scan(&count)
require.NoError(t, err)
if domain.shouldBeFiltered {
require.EqualValues(t, 0, count, "%s table shouldn't contain any entries with a domain of %s", table, domain.d)
} else {
require.Greater(t, count, uint64(0), "%s table should contain at least one entry with a domain of %s", table.name, domain.d)
}
}
}
}
// TestFilterExternalToInternal also tests ICMP
func (it *FilterTestSuite) TestFilterExternalToInternal() {
t := it.T()
// set up file system interface
afs := afero.NewOsFs()
cfg, err := config.ReadFileConfig(afs, ConfigPath)
require.NoError(t, err)
cfg.Env.DBConnection = dockerInfo.clickhouseConnection
cfg.Filtering.FilterExternalToInternal = false
it.cfg = cfg
require.NoError(t, err, "updating config should not return an error")
require.False(t, cfg.Filtering.FilterExternalToInternal)
// // import data
importResults, err := cmd.RunImportCmd(time.Now(), cfg, afs, "../test_data/valid_tsv", "filter_ext_to_int", false, true)
require.NoError(t, err)
// connect to database
db, err := database.ConnectToDB(context.Background(), "filter_ext_to_int", cfg, nil)
require.NoError(t, err)
// // there are ICMP connections that are only on connections that are external to internal
type protoInfo struct {
PortProtoService string `ch:"port_proto_service"`
ConnCount uint64 `ch:"conn_count"`
BytesSent uint64 `ch:"bytes_sent"`
BytesReceived uint64 `ch:"bytes_received"`
}
type testData struct {
src string
dst string
portInfoList []protoInfo
}
testCases := []testData{
{
src: "165.227.88.15",
dst: "192.168.88.2",
portInfoList: []protoInfo{
{"icmp:3/3", 2, 4858, 0},
},
},
{
src: "66.218.84.141",
dst: "10.55.100.107",
portInfoList: []protoInfo{
{"icmp:3/3", 1, 240, 0},
},
},
{
src: "66.218.84.141",
dst: "10.55.100.104",
portInfoList: []protoInfo{
{"icmp:3/3", 1, 240, 0},
},
},
{
src: "66.218.84.141",
dst: "10.55.100.108",
portInfoList: []protoInfo{
{"icmp:3/3", 1, 240, 0},
},
},
{
src: "67.226.210.13",
dst: "10.55.100.106",
portInfoList: []protoInfo{
{"icmp:3/10", 1, 136, 0},
},
},
{
src: "67.226.210.15",
dst: "10.55.100.107",
portInfoList: []protoInfo{
{"icmp:3/10", 1, 136, 0},
},
},
{
src: "67.226.210.14",
dst: "10.55.100.108",
portInfoList: []protoInfo{
{"icmp:3/10", 1, 136, 0},
},
},
{
src: "24.220.6.168",
dst: "10.55.200.11",
portInfoList: []protoInfo{
{"icmp:3/13", 1, 96, 0},
},
},
}
for _, test := range testCases {
ctx := clickhouse.Context(context.Background(), clickhouse.WithParameters(clickhouse.Parameters{
"src": test.src,
"dst": test.dst,
}))
var res []protoInfo
err = db.Conn.Select(ctx, &res, `
SELECT concat(proto, ':', icmp_type, '/', icmp_code) AS port_proto_service,
countMerge(count) AS conn_count,
sumMerge(bytes_sent) AS bytes_sent,
sumMerge(bytes_received) AS bytes_received
FROM port_info
WHERE src={src:String} AND dst={dst:String}
GROUP BY src, dst, dst_port, proto, service, icmp_type, icmp_code
`)
require.NoError(t, err, "querying proto table should not produce an error")
// ensure that the length of the result list matches the expected value
require.Len(t, res, len(test.portInfoList), "length of result list should match expected value")
// ensure that the result list matches the expected value
require.ElementsMatch(t, test.portInfoList, res, "result list should match expected value")
}
/* ******* Mixtape Propagation *******
The entries that use ICMP don't have enough connections to qualify as beacons, so they don't appear in the mixtape.
In order to test the spagooper query that grabs the ICMP entries within port:proto:service, we have to go through
the results of the IP spagooper.
*/
// set up new analyzer
minTSBeacon, maxTSBeacon, notFromConn, err := db.GetBeaconMinMaxTimestamps()
require.NoError(t, err)
require.False(t, notFromConn, "min and max timestamps should be from conn table")
minTS, maxTS, notFromConn, useCurrentTime, err := db.GetTrueMinMaxTimestamps()
require.NoError(t, err)
require.False(t, notFromConn, "min and max timestamps should be from conn table")
require.False(t, useCurrentTime, "first seen analysis should not use the current time")
analyzer, err := analysis.NewAnalyzer(db, cfg, importResults.ImportID[0], minTS, maxTS, minTSBeacon, maxTSBeacon, useCurrentTime, false)
require.NoError(t, err)
ctx := context.Background()
queryGroup, ctx := errgroup.WithContext(ctx)
// create progress bars
bars := progressbar.New(ctx, []*progressbar.ProgressBar{
progressbar.NewBar("IP Connection Analysis ", 2, progress.New(progress.WithDefaultGradient())),
}, []progressbar.Spinner{})
type foundEntry struct {
PortProtoService string
ConnCount uint64
TotalBytes uint64
}
type resData struct {
src string
dst string
portInfoList []foundEntry
}
var foundIPs []resData
var expectedResData []resData
for _, test := range testCases {
d := resData{
src: test.src,
dst: test.dst,
}
var p []foundEntry
for _, dd := range test.portInfoList {
p = append(p, foundEntry{
PortProtoService: dd.PortProtoService,
ConnCount: dd.ConnCount,
TotalBytes: (dd.BytesSent + dd.BytesReceived) * 2, // multiply by 2 for openconns
})
}
d.portInfoList = p
expectedResData = append(expectedResData, d)
}
queryGroup.Go(func() error {
for entry := range analyzer.UconnChan {
for _, test := range testCases {
if entry.Src.String() == test.src && entry.Dst.String() == test.dst {
f := resData{
src: entry.Src.String(),
dst: entry.Dst.String(),
}
var portProto []foundEntry
for _, p := range entry.PortProtoService {
port := foundEntry{
PortProtoService: p,
ConnCount: entry.Count,
TotalBytes: entry.TotalBytes,
}
portProto = append(portProto, port)
}
f.portInfoList = portProto
foundIPs = append(foundIPs, f)
}
}
}
return nil
})
queryGroup.Go(func() error {
err := analyzer.ScoopIPConns(ctx, bars)
require.NoError(t, err)
close(analyzer.UconnChan)
return err
})
queryGroup.Go(func() error {
_, err := bars.Run()
require.NoError(t, err)
return err
})
if err := queryGroup.Wait(); err != nil {
require.NoError(t, err)
}
require.ElementsMatch(t, expectedResData, foundIPs)
}