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

468 lines
15 KiB
Go

package database
import (
"fmt"
"slices"
"strconv"
"time"
"github.com/activecm/rita/v5/config"
c "github.com/activecm/rita/v5/constants"
zlog "github.com/activecm/rita/v5/logger"
"github.com/activecm/rita/v5/util"
"github.com/ClickHouse/clickhouse-go/v2"
)
type MetaDBImportedFile struct {
Hash *util.FixedString `ch:"hash"`
ImportID *util.FixedString `ch:"import_id"`
Database string `ch:"database"`
Timestamp time.Time `ch:"ts"`
Path string `ch:"path"`
}
type MetaDBImportRecord struct {
ImportID *util.FixedString `ch:"import_id"`
Rolling bool `ch:"rolling"`
Database string `ch:"database"`
Rebuild bool `ch:"rebuild"`
StartedAt int64 `ch:"started_at"`
EndedAt time.Time `ch:"ended_at"`
HoursSeen []time.Time `ch:"hours_seen"`
ImportVersion string `ch:"import_version"`
MinTimestamp time.Time `ch:"min_timestamp"`
MaxTimestamp time.Time `ch:"max_timestamp"`
MinOpenTimestamp time.Time `ch:"min_open_timestamp"`
MaxOpenTimestamp time.Time `ch:"max_open_timestamp"`
}
// createMetaDatabase creates the metadatabase and its tables if any part of it doesn't exist
func (server *ServerConn) createMetaDatabase() error {
if err := server.Conn.Exec(server.ctx, `
CREATE DATABASE IF NOT EXISTS metadatabase
`); err != nil {
return err
}
if err := server.createMetaDatabaseImportsTable(); err != nil {
return err
}
if err := server.createMetaDatabaseFilesTable(); err != nil {
return err
}
if err := server.createMetaDatabaseMinMaxTable(); err != nil {
return err
}
if err := server.createMetaDatabaseSampleDBsTable(); err != nil {
return err
}
if err := server.createMetaDatabasePerformedZoneTransfersTable(); err != nil {
return err
}
if err := server.createMetaDatabaseZoneTransferTable(); err != nil {
return err
}
if err := server.createThreatIntelTables(); err != nil {
return err
}
if err := server.createValidMIMETypeTable(); err != nil {
return err
}
if err := server.createHistoricalFirstSeenTable(); err != nil {
return err
}
return nil
}
// createMetaDatabaseFilesTable creates the metadatabase.files table
func (server *ServerConn) createMetaDatabaseFilesTable() error {
err := server.Conn.Exec(server.ctx, `
CREATE TABLE IF NOT EXISTS metadatabase.files (
hash FixedString(16),
database String,
import_id FixedString(16),
rolling Bool,
ts DateTime(),
path String
)
ENGINE = MergeTree()
PRIMARY KEY (database, import_id, hash, path)
`)
return err
}
// createMetaDatabaseImportsTable creates the metadatabase.imports table
func (server *ServerConn) createMetaDatabaseImportsTable() error {
err := server.Conn.Exec(server.ctx, `
CREATE TABLE IF NOT EXISTS metadatabase.imports (
import_id FixedString(16),
rolling Bool,
database String,
rebuild Bool,
-- started_at is measured in Microseconds
started_at DateTime64(6),
ended_at DateTime(),
hours_seen Array(DateTime()),
import_version String,
min_timestamp DateTime(),
max_timestamp DateTime(),
min_open_timestamp DateTime(),
max_open_timestamp DateTime()
)
ENGINE = MergeTree()
PRIMARY KEY (database, ended_at, started_at, import_id)
`)
return err
}
func (server *ServerConn) createMetaDatabaseMinMaxTable() error {
// err := server.Conn.Exec(server.ctx, `--sql
// CREATE TABLE IF NOT EXISTS metadatabase.min_max_raw (
// database String,
// rolling Bool,
// beacon String,
// min_ts DateTime(),
// max_ts DateTime()
// )
// ENGINE = MergeTree()
// PRIMARY KEY (database)
// `)
// if err != nil {
// return err
// }
err := server.Conn.Exec(server.ctx, `--sql
CREATE TABLE IF NOT EXISTS metadatabase.min_max (
database String,
rolling Bool,
beacon Bool,
min_ts SimpleAggregateFunction(min, DateTime()),
max_ts SimpleAggregateFunction(max, DateTime())
)
ENGINE = AggregatingMergeTree()
PRIMARY KEY (database, beacon)
`)
if err != nil {
return err
}
// if err := server.Conn.Exec(server.ctx, `--sql
// CREATE MATERIALIZED VIEW IF NOT EXISTS metadatabase.min_max_default_mv
// TO metadatabase.min_max AS
// SELECT
// database,
// rolling,
// beacon,
// minSimpleState(min_ts) as min_ts,
// maxSimpleState(max_ts) as max_ts
// FROM metadatabase.min_max_raw
// GROUP BY (database, rolling, beacon)
// `); err != nil {
// return err
// }
return nil
}
func (server *ServerConn) createMetaDatabasePerformedZoneTransfersTable() error {
err := server.Conn.Exec(server.ctx, `
CREATE TABLE IF NOT EXISTS metadatabase.performed_zone_transfers (
domain_name String,
name_server String,
serial_soa UInt32,
mbox String,
is_ixfr Bool, -- for debugging
performed_at DateTime()
)
ENGINE = ReplacingMergeTree(performed_at)
PRIMARY KEY (domain_name, name_server)
`)
return err
}
func (server *ServerConn) createMetaDatabaseZoneTransferTable() error {
err := server.Conn.Exec(server.ctx, `
CREATE TABLE IF NOT EXISTS metadatabase.zone_transfer (
performed_at DateTime(),
domain_name String,
name_server String,
hostname String,
ip IPv6,
ttl Int32, -- RFC2181 defines TTLs as int32
) ENGINE = MergeTree()
PRIMARY KEY (domain_name, name_server, hostname, ip)
ORDER BY (domain_name, name_server, hostname, ip, performed_at)
`)
return err
}
// createMetaDatabaseFilesTable creates the metadatabase.files table
func (server *ServerConn) createMetaDatabaseSampleDBsTable() error {
err := server.Conn.Exec(server.ctx, `
CREATE TABLE IF NOT EXISTS metadatabase.sample_dbs (
name String,
)
ENGINE = ReplacingMergeTree()
PRIMARY KEY (name)
ORDER BY (name)
`)
return err
}
// MarkFileImportedInMetaDB adds the given path to the metadatabase.files table to mark it as being used
func (db *DB) MarkFileImportedInMetaDB(hash util.FixedString, importID util.FixedString, path string) error {
ctx := db.QueryParameters(clickhouse.Parameters{
"hash": hash.Hex(),
"importID": importID.Hex(),
"database": db.selected,
"timestamp": strconv.FormatInt(time.Now().UTC().Unix(), 10),
"path": path,
"rolling": strconv.FormatBool(db.Rolling),
})
err := db.Conn.Exec(ctx, `
INSERT INTO metadatabase.files (hash, import_id, database, rolling, ts, path)
VALUES (unhex({hash:String}), unhex({importID:String}), {database:String}, {rolling:Bool}, {timestamp:Int32}, {path:String})
`)
return err
}
/* *** TRACKING IMPORTS ***
Data in ClickHouse is meant to be append-only. This means that we cannot easily update records.
The metadatabase.imports table acts as a log of events for imports. In order to track the start and completion
of an import, one record is inserted at the beginning of the import, and another record is imported at the end.
*/
// AddImportStartRecordToMetaDB inserts a record into the metadatabase.imports table to mark that an import has started
func (db *DB) AddImportStartRecordToMetaDB(importID util.FixedString) error {
if config.Version == "" {
return fmt.Errorf("cannot add import record to metadb, version is not set")
}
ctx := db.QueryParameters(clickhouse.Parameters{
"importID": importID.Hex(),
"rolling": strconv.FormatBool(db.Rolling),
"database": db.selected,
"rebuild": strconv.FormatBool(db.rebuild),
"importStartedAt": strconv.FormatInt(db.ImportStartedAt.UnixMicro(), 10),
"importVersion": config.Version,
})
err := db.Conn.Exec(ctx, `
INSERT INTO metadatabase.imports (import_id, rolling, database, rebuild, started_at, import_version)
VALUES (unhex({importID:String}), {rolling:Bool}, {database:String}, {rebuild:Bool}, fromUnixTimestamp64Micro({importStartedAt:Int64}), {importVersion:String})
`)
return err
}
// AddImportFinishedRecordToMetaDB inserts a record into the metadatabase.imports table to mark that an import has finished
func (db *DB) AddImportFinishedRecordToMetaDB(importID util.FixedString, minTS, maxTS time.Time) error {
// get min and max timestamps from the imported conn logs
type minMaxRes struct {
Min time.Time `ch:"min_ts"`
Max time.Time `ch:"max_ts"`
}
// get min and max timestamps from the imported open_conn logs
var minMaxOpen minMaxRes
err := db.Conn.QueryRow(db.GetContext(), `
SELECT min(ts) as min_ts, max(ts) as max_ts FROM openconn
`).ScanStruct(&minMaxOpen)
if err != nil {
return err
}
ctx := db.QueryParameters(clickhouse.Parameters{
"importID": importID.Hex(),
"rolling": strconv.FormatBool(db.Rolling),
"database": db.selected,
"importStartedAt": strconv.FormatInt(db.ImportStartedAt.UnixMicro(), 10),
"importEndedAt": strconv.FormatInt(time.Now().Unix(), 10),
"minTs": strconv.FormatInt(minTS.UTC().Unix(), 10),
"maxTs": strconv.FormatInt(maxTS.UTC().Unix(), 10),
"minOpenTs": strconv.FormatInt(minMaxOpen.Min.UTC().Unix(), 10),
"maxOpenTs": strconv.FormatInt(minMaxOpen.Max.UTC().Unix(), 10),
"importVersion": config.Version,
})
err = db.Conn.Exec(ctx, `
INSERT INTO metadatabase.imports (import_id, rolling, database, started_at, ended_at, min_timestamp, max_timestamp, min_open_timestamp, max_open_timestamp, import_version)
VALUES (
unhex({importID:String}),
{rolling:Bool},
{database:String},
fromUnixTimestamp64Micro({importStartedAt:Int64}),
fromUnixTimestamp({importEndedAt:Int32}),
fromUnixTimestamp({minTs:Int32}),
fromUnixTimestamp({maxTs:Int32}),
fromUnixTimestamp({minOpenTs:Int32}),
fromUnixTimestamp({maxOpenTs:Int32}),
{importVersion:String}
)
`)
return err
}
// CheckIfFilesWereAlreadyImported calls checkFileHashes for each log type
func (db *DB) CheckIfFilesWereAlreadyImported(fileMap map[string][]string) (int, error) {
totalFileCount := 0
// loop over each log type in the hour's filemap
for logType, logList := range fileMap {
results, err := db.checkFileHashes(logList)
if err != nil {
return totalFileCount, err
}
fileMap[logType] = results
totalFileCount += len(results)
}
// don't bother validating log combinations if there are no new logs
if totalFileCount == 0 {
return 0, nil
}
// we must validate the log combinations after checking the logs against the metadb
// because if this is a rolling import and an import was killed/failed somehow and the remaining
// logs to import are an invalid combination, they need to be filtered out/skipped or else
// the import can end up hanging
totalFiles := ValidateLogCombinations(fileMap)
return totalFiles, nil
}
// ValidateLogCombinations filters out invalid log combinations and returns the number of valid logs to import
// This function is in this package because it needs to be used by both the cmd package and the import or database package
// and this avoids an import cycle
func ValidateLogCombinations(hourMap map[string][]string) int {
logger := zlog.GetLogger()
totalHourFilesFound := 0
// if there are no conn logs in the hour, we have to skip any SSL and HTTP logs for that hour
if len(hourMap[c.ConnPrefix]) == 0 && (len(hourMap[c.SSLPrefix]) > 0 || len(hourMap[c.HTTPPrefix]) > 0) {
logger.Warn().Msg("SSL / HTTP logs are present, but no conn logs exist, skipping SSL / HTTP logs...")
delete(hourMap, c.SSLPrefix)
delete(hourMap, c.HTTPPrefix)
}
// // if there are no open conn logs in the hour, we have to skip any open SSL and open HTTP logs for that hour
if len(hourMap[c.OpenConnPrefix]) == 0 && (len(hourMap[c.OpenSSLPrefix]) > 0 || len(hourMap[c.OpenHTTPPrefix]) > 0) {
logger.Warn().Msg("Open SSL / open HTTP logs are present, but no conn logs exist, skipping open SSL / open HTTP logs...")
delete(hourMap, c.OpenSSLPrefix)
delete(hourMap, c.OpenHTTPPrefix)
}
// track the total number of files after filtering out invalid file combinations
for zeekType := range hourMap {
// sort the files for each log type, necessary for tests
slices.Sort(hourMap[zeekType])
totalHourFilesFound += len(hourMap[zeekType])
}
return totalHourFilesFound
}
// checkFileHashes filters fileList to only files that haven't already been imported for this dataset
func (db *DB) checkFileHashes(fileList []string) ([]string, error) {
// format array for clickhouse parameters
files := "["
for _, file := range fileList {
files += fmt.Sprintf("'%s',", file)
}
files += "]"
ctx := db.QueryParameters(clickhouse.Parameters{
"database": db.selected,
"files": files,
})
var importedFiles []struct {
Path string `ch:"path"`
}
// query for files in this fileList that have already been imported
err := db.Conn.Select(ctx, &importedFiles, `
SELECT path FROM metadatabase.files WHERE database = {database:String} AND path IN {files:Array(String)}
`)
if err != nil {
return nil, err
}
// convert imported files array into a map
importedFilesMap := make(map[string]bool)
for _, file := range importedFiles {
importedFilesMap[file.Path] = true
}
var nonImportedFiles []string
// build a list of files that haven't been imported
for _, file := range fileList {
if !importedFilesMap[file] {
nonImportedFiles = append(nonImportedFiles, file)
}
}
return nonImportedFiles, err
}
// ClearMetaDBEntriesForDatabase deletes all file and import record entries in the metadatabase for the specified database
func (server *ServerConn) ClearMetaDBEntriesForDatabase(database string) error {
// verify that the metadatabase exists
exists, err := DatabaseExists(server.ctx, server.Conn, "metadatabase")
if err != nil {
return err
}
// clear the imported files and min_max records for the specified database if metadatabase exists
if exists {
if err := server.clearImportedFilesFromMetaDB(database); err != nil {
return err
}
if err := server.clearDatabaseFromMetaDB(database); err != nil {
return err
}
}
return nil
}
// clearImportedFilesFromMetaDB deletes entries in files table for specified database
func (server *ServerConn) clearImportedFilesFromMetaDB(database string) error {
ctx := clickhouse.Context(server.ctx, clickhouse.WithParameters(clickhouse.Parameters{"database": database}))
err := server.Conn.Exec(ctx, `
DELETE FROM metadatabase.files WHERE database = {database:String}
`, database)
return err
}
func (server *ServerConn) clearDatabaseFromMetaDB(database string) error {
ctx := clickhouse.Context(server.ctx, clickhouse.WithParameters(clickhouse.Parameters{"database": database}))
if err := server.Conn.Exec(ctx, `
DELETE FROM metadatabase.min_max WHERE database = {database:String}
`, database); err != nil {
return fmt.Errorf("unable to delete database from metadatabase.min_max: %w", err)
}
if err := server.Conn.Exec(ctx, `
DELETE FROM metadatabase.sample_dbs WHERE name = {database:String}
`, database); err != nil {
return fmt.Errorf("unable to delete database from metadatabase.sample_dbs: %w", err)
}
return nil
}