mirror of
https://github.com/activecm/rita
synced 2026-06-08 13:02:45 +00:00
b923c39e2d
* 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>
262 lines
11 KiB
Go
262 lines
11 KiB
Go
package integration_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/activecm/rita/v5/cmd"
|
|
"github.com/activecm/rita/v5/config"
|
|
"github.com/activecm/rita/v5/database"
|
|
"github.com/activecm/rita/v5/util"
|
|
"github.com/activecm/rita/v5/viewer"
|
|
|
|
"github.com/ClickHouse/clickhouse-go/v2"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
/*
|
|
All interesting traffic is coming from 73.54.23.243 to 64.225.56.201
|
|
The zeek sensor was running on 64.225.56.201
|
|
|
|
The logs contain the following data:
|
|
3 days worth of log files
|
|
An SSH session that is open the entire time
|
|
An HTTP connection with a blank "Host:" parameter
|
|
It has a unique user agent string of "'Modzilla/0.0001 (Atari 7800)'"
|
|
HTTP connection is beaconing every 200-350 seconds
|
|
|
|
Last 24 hours
|
|
73.54.23.243 to 64.225.56.201
|
|
count: 331
|
|
missing host header: 330x
|
|
rare signature modifier: Modzilla/0.0001 (Atari 7800)
|
|
|
|
Connections not in the last import
|
|
172.70.114.7 fomobasedcoin.com
|
|
64.225.56.201 venomlaunchpad.com
|
|
|
|
*/
|
|
|
|
type MissingHostSuite ValidDatasetTestSuite
|
|
|
|
func TestMissingHost(t *testing.T) {
|
|
missingHostSuite := new(MissingHostSuite)
|
|
|
|
// set up file system interface
|
|
afs := afero.NewOsFs()
|
|
|
|
cfg, err := config.ReadFileConfig(afs, ConfigPath)
|
|
require.NoError(t, err)
|
|
|
|
dropletSubnet, err := util.ParseSubnet("64.225.56.201/32")
|
|
require.NoError(t, err)
|
|
cfg.Filtering.InternalSubnets = append(cfg.Filtering.InternalSubnets, dropletSubnet)
|
|
cfg.Filtering.FilterExternalToInternal = false
|
|
cfg.Env.DBConnection = dockerInfo.clickhouseConnection
|
|
|
|
require.Contains(t, cfg.Filtering.InternalSubnets, util.NewSubnet(&net.IPNet{IP: net.IP{64, 225, 56, 201}, Mask: net.IPMask{255, 255, 255, 255}}))
|
|
require.False(t, cfg.Filtering.FilterExternalToInternal)
|
|
|
|
// // import data
|
|
results, err := cmd.RunImportCmd(time.Now(), cfg, afs, "../test_data/missing_host", "missing_host", false, true)
|
|
require.NoError(t, err)
|
|
missingHostSuite.importResults = results
|
|
require.Len(t, results.ImportID, 72, "there should be 72 hours of imports, each with their own unique import ID")
|
|
|
|
// connect to database
|
|
db, err := database.ConnectToDB(context.Background(), "missing_host", cfg, nil)
|
|
require.NoError(t, err)
|
|
|
|
// determine which max timestamp to use for relative time calculations
|
|
minTimestamp, maxTimestamp, _, err := db.GetBeaconMinMaxTimestamps()
|
|
require.NoError(t, err)
|
|
|
|
missingHostSuite.maxTimestamp = maxTimestamp
|
|
missingHostSuite.minTimestamp = minTimestamp
|
|
|
|
missingHostSuite.db = db
|
|
missingHostSuite.cfg = cfg
|
|
suite.Run(t, missingHostSuite)
|
|
}
|
|
|
|
// TestThreat verifies the results of the important connection pair in this dataset
|
|
func (it *MissingHostSuite) TestThreat() {
|
|
t := it.T()
|
|
// verify the results of the analysis
|
|
type expectedResults struct {
|
|
src string
|
|
dst string
|
|
finalScore float64
|
|
beaconScore float64
|
|
longConnScore float64
|
|
totalDuration float64
|
|
totalBytes float64
|
|
count uint64
|
|
missingHostCount uint64
|
|
missingHostScore float64
|
|
prevalence float64
|
|
portProtoService []string
|
|
firstSeen time.Time
|
|
}
|
|
expected := expectedResults{
|
|
src: "73.54.23.243", dst: "64.225.56.201",
|
|
finalScore: 0.9000, beaconScore: 0.963, longConnScore: config.HIGH_CATEGORY_SCORE,
|
|
totalDuration: 270036.631115, count: 331, missingHostCount: 330, missingHostScore: 0.1,
|
|
totalBytes: 1679442, portProtoService: []string{"80:tcp:http", "22:tcp:ssh"}, prevalence: 1,
|
|
firstSeen: time.Unix(1713470872, 0).UTC(),
|
|
}
|
|
|
|
min, max, _, err := it.db.GetBeaconMinMaxTimestamps()
|
|
require.NoError(t, err)
|
|
|
|
const predictedStaticMinTS = 1713657591
|
|
const predictedStaticMaxTS = 1713743991
|
|
|
|
require.EqualValues(t, time.Unix(predictedStaticMinTS, 0).UTC(), min, "the min timestamp from GetMinMaxTimestamps should match the expected timestamp")
|
|
require.EqualValues(t, time.Unix(predictedStaticMaxTS, 0).UTC(), max, "the max timestamp from GetMinMaxTimestamps should match the expected timestamp")
|
|
|
|
var count uint64
|
|
ctx := it.db.QueryParameters(clickhouse.Parameters{
|
|
"min_ts": fmt.Sprintf("%d", time.Unix(predictedStaticMinTS, 0).UTC().Unix()),
|
|
"src": expected.src,
|
|
"dst": expected.dst,
|
|
})
|
|
|
|
err = it.db.Conn.QueryRow(ctx, `--sql
|
|
select countIf(missing_host_header = false) from conn
|
|
where src = {src:String} and dst = {dst:String}
|
|
and toStartOfHour(ts) >= toStartOfHour(fromUnixTimestamp({min_ts:Int64}))
|
|
`).Scan(&count)
|
|
require.NoError(t, err, "retrieving the connection count should not error")
|
|
|
|
require.EqualValues(t, expected.count, count, "the number of connections from the conn table should match, expected: %d, got: %d ", expected.count, count)
|
|
|
|
filter := &viewer.Filter{Src: expected.src, Dst: expected.dst}
|
|
min = time.Unix(0, 0)
|
|
query, params, _ := viewer.BuildResultsQuery(filter, 0, 10, min)
|
|
ctx = it.db.QueryParameters(params)
|
|
rows, err := it.db.Conn.Query(ctx, query)
|
|
require.NoError(t, err, "getting the mixtape results shouldn't error")
|
|
|
|
i := 0
|
|
for rows.Next() {
|
|
var res viewer.MixtapeResult
|
|
err := rows.ScanStruct(&res)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected.src, res.Src.String(), "source IP should match")
|
|
require.Equal(t, expected.dst, res.Dst.String(), "destination IP should match")
|
|
require.InDelta(t, expected.finalScore, res.FinalScore, 0.001, "final score should match")
|
|
require.InDelta(t, expected.beaconScore, res.BeaconScore, 0.001, "beacon score should match")
|
|
require.InDelta(t, expected.longConnScore, res.LongConnScore, 0.001, "long connection score should match")
|
|
require.InDelta(t, expected.totalDuration, res.TotalDuration, 0.01, "total duration should match")
|
|
require.InDelta(t, expected.totalBytes, res.TotalBytes, 0.001, "total bytes should match")
|
|
require.EqualValues(t, expected.count, res.Count, "count should match")
|
|
require.EqualValues(t, expected.missingHostCount, res.MissingHostCount, "missing host count should match")
|
|
require.InDelta(t, expected.missingHostScore, res.MissingHostHeaderScore, 0.001, "missing host score should match")
|
|
require.InDelta(t, expected.prevalence, res.Prevalence, 0.001, "prevalence should match")
|
|
require.InDelta(t, -it.cfg.Modifiers.PrevalenceScoreDecrease, res.PrevalenceScore, 0.001, "prevalence score should equal the prevalence decrease config value")
|
|
require.InDelta(t, 0, res.FirstSeenScore, 0.001, "first seen score should equal 0 for a non-rolling dataset")
|
|
require.EqualValues(t, expected.firstSeen.UTC(), res.FirstSeen, "first seen date should match")
|
|
require.InDelta(t, it.cfg.Modifiers.MissingHostCountScoreIncrease, res.MissingHostHeaderScore, 0.001, "missing host header score should equal the missing host header increase score config value")
|
|
require.InDelta(t, it.cfg.Modifiers.RareSignatureScoreIncrease, res.TotalModifierScore, 0.001, "total modifier score should equal the rare signature increase score config value")
|
|
require.ElementsMatch(t, expected.portProtoService, res.PortProtoService, "port:proto:service arrays should match")
|
|
i++
|
|
}
|
|
rows.Close()
|
|
require.EqualValues(t, 1, i, "there should only be one row for an aggregated result in the mixtape: 73.54.23.243 -> 64.225.56.201")
|
|
|
|
// verify modifier for the threat
|
|
// there should only be the rare signature modifier (for the modifiers that are on their own row)
|
|
rows, err = it.db.Conn.Query(ctx, `--sql
|
|
SELECT modifier_name, modifier_value, modifier_score FROM threat_mixtape
|
|
WHERE src = {src:String} AND dst = {dst:String}
|
|
AND modifier_name != ''
|
|
AND import_id = (SELECT argMax(import_id, analyzed_at) FROM threat_mixtape)
|
|
`)
|
|
require.NoError(t, err, "retrieving the modifiers for this threat should not error")
|
|
|
|
i = 0
|
|
for rows.Next() {
|
|
var name, value string
|
|
var score float64
|
|
err = rows.Scan(&name, &value, &score)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "rare_signature", name, "the modifier should be rare signature")
|
|
require.Equal(t, "Modzilla/0.0001 (Atari 7800)", value, "the rare signature should be Modzilla/0.0001 (Atari 7800)")
|
|
require.InDelta(t, it.cfg.Modifiers.RareSignatureScoreIncrease, score, 0.001, "the rare signature score should match the config modifier value")
|
|
i++
|
|
}
|
|
require.EqualValues(t, 1, i, "there should only be one modifier for 73.54.23.243 -> 64.225.56.201")
|
|
|
|
// verify that the threat has the ICMP protocol in port:proto:service (in a previous import)
|
|
ctx = it.db.QueryParameters(clickhouse.Parameters{
|
|
// last seen is the hour that ICMP was seen
|
|
"last_seen": fmt.Sprintf("%d", time.Unix(1713506400, 0).UTC().Unix()),
|
|
"src": expected.src,
|
|
"dst": expected.dst,
|
|
})
|
|
|
|
var portProto []string
|
|
expectedProtoService := []string{"icmp:8/0", "80:tcp:http", "22:tcp:ssh"}
|
|
err = it.db.Conn.QueryRow(ctx, `--sql
|
|
SELECT flatten(groupArray(port_proto_service)) FROM threat_mixtape
|
|
WHERE src = {src:String} AND dst = {dst:String} AND toStartOfHour(last_seen) = fromUnixTimestamp({last_seen:Int64})
|
|
`).Scan(&portProto)
|
|
require.NoError(t, err, "getting the single mixtape result shouldn't error")
|
|
require.ElementsMatch(t, portProto, expectedProtoService, "old port:proto:service array containing icmp should match expected output")
|
|
}
|
|
|
|
// TestTmpTables makes sure that temp tables are getting truncated on each import, including hour imports, so that
|
|
// only the connections from the current import are in the tmp tables
|
|
func (it *MissingHostSuite) TestTmpTables() {
|
|
t := it.T()
|
|
|
|
cases := []struct {
|
|
hash string
|
|
srcTable string
|
|
tmpTable string
|
|
col string
|
|
tld string
|
|
}{
|
|
{srcTable: "conn", tmpTable: "uconn_tmp", hash: "2E20BF90DA74FC0ACEC1A75BED4B4A7C", col: "hash"},
|
|
{srcTable: "ssl", tmpTable: "sniconn_tmp", hash: "B72C70EFE29E20962EC6A2AE74D57149", col: "hash"},
|
|
{srcTable: "http", tmpTable: "sniconn_tmp", hash: "8CA65FDEB4184DEF86589A874F7D4997", col: "hash"},
|
|
{srcTable: "dns", tmpTable: "dns_tmp", hash: "0F3D1D4F2CB470E666996F87208B5FDC", col: "tld", tld: "236.211.203.35.in-addr.arpa"},
|
|
}
|
|
|
|
for _, testCase := range cases {
|
|
hash2 := testCase.hash
|
|
if testCase.tld != "" {
|
|
hash2 = testCase.tld
|
|
}
|
|
|
|
ctx := it.db.QueryParameters(clickhouse.Parameters{
|
|
"src_table": testCase.srcTable,
|
|
"tmp_table": testCase.tmpTable,
|
|
"hash": testCase.hash,
|
|
"col": testCase.col,
|
|
"hash2": hash2,
|
|
})
|
|
var logCount, tmpCount uint64
|
|
err := it.db.Conn.QueryRow(ctx, `--sql
|
|
SELECT count() FROM {src_table:Identifier}
|
|
WHERE hash = unhex({hash:String})
|
|
`).Scan(&logCount)
|
|
require.NoError(t, err)
|
|
require.Positive(t, logCount, "there should a record in the %s table for a connection not used in the last hour, got: %d", testCase.srcTable, logCount)
|
|
|
|
err = it.db.Conn.QueryRow(ctx, `--sql
|
|
SELECT count() FROM {tmp_table:Identifier}
|
|
WHERE {col:Identifier} = unhex({hash2:String})
|
|
`).Scan(&tmpCount)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 0, tmpCount, "there should be no entries in the %s table for a connection only used in one hour, got: %d", testCase.tmpTable, tmpCount)
|
|
}
|
|
}
|