mirror of
https://github.com/activecm/rita
synced 2026-06-08 13:02:45 +00:00
2bd5475e76
* Disable permission flag checks on imported logs (#86) * Disable permission flag checks on imported logs * Display individual log errors even if there are no viable logs --------- Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com> * Installer Import Logic Updates (#89) * rename sshprep to sshprep.sh * installer updates removed mass upgrades, cleaned up installer, removed references to unsupported OS's, improved error handling and usage messages * threat intel feed error handling * cleaned up installer scripts and improved helper functions * Update ansible-installer.sh --------- Co-authored-by: Naomi Kramer <naomi@activecountermeasures.com>
493 lines
14 KiB
Go
493 lines
14 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/activecm/rita/v5/config"
|
|
zlog "github.com/activecm/rita/v5/logger"
|
|
|
|
clickhouse "github.com/ClickHouse/clickhouse-go/v2"
|
|
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
type ServerConn struct {
|
|
Conn driver.Conn
|
|
addr string
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
var ErrNoMetaDBImportRecordForDatabase = errors.New("no import record found for database")
|
|
var ErrDatabaseNotFound = errors.New("database does not exist")
|
|
var ErrDatabaseNameEmpty = errors.New("database name cannot be empty")
|
|
var ErrMissingConfig = errors.New("config cannot be nil")
|
|
var ErrImportTwiceNonRolling = errors.New("cannot import more than once to a non-rolling database")
|
|
var errRollingStatusFailure = errors.New("failed to detect rolling status of given import database")
|
|
var errRollingFlagMissing = errors.New("cannot import non-rolling data to a rolling database")
|
|
|
|
// SetUpNewImport creates the database requested for this import and returns a new DB struct for connection to said database
|
|
func SetUpNewImport(afs afero.Fs, cfg *config.Config, dbName string, rollingFlag bool, rebuildFlag bool) (*DB, error) {
|
|
logger := zlog.GetLogger()
|
|
|
|
// validate parameters
|
|
if cfg == nil {
|
|
return nil, ErrMissingConfig
|
|
}
|
|
|
|
if dbName == "" {
|
|
return nil, ErrDatabaseNameEmpty
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// connect to ClickHouse server
|
|
server, err := ConnectToServer(ctx, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// set up metadatabase if it does not exist yet
|
|
err = server.CreateServerDBTables()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = server.createMetaDatabaseTTLs(int(cfg.RITA.MonthsToKeepHistoricalFirstSeen))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// drop database if rebuild flag was passed
|
|
if rebuildFlag {
|
|
err = server.DeleteSensorDB(dbName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logger.Info().Str("database", dbName).Msg("Successfully rebuilt import database")
|
|
}
|
|
|
|
// check rolling status of database
|
|
rolling, err := server.checkRolling(dbName, rollingFlag, rebuildFlag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create sensor database
|
|
db, err := server.createSensorDatabase(cfg, dbName, rolling)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// reset temporary tables
|
|
err = db.ResetTemporaryTables()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// sync threat intel feeds from config
|
|
err = server.syncThreatIntelFeedsFromConfig(afs, cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// import valid MIME types
|
|
err = server.importValidMIMETypes(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// err = server.ParseHints(afs, cfg)
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
|
|
// set rolling flag
|
|
db.Rolling = rollingFlag
|
|
|
|
// set rebuild flag
|
|
db.rebuild = rebuildFlag
|
|
|
|
return db, nil
|
|
|
|
}
|
|
|
|
// QueryParameters generates ClickHouse query parameters by creating a context with the specified parameters in it
|
|
func (server *ServerConn) QueryParameters(params clickhouse.Parameters) context.Context {
|
|
return clickhouse.Context(server.ctx, clickhouse.WithParameters(params))
|
|
}
|
|
|
|
// GetContext returns the context for the database connection
|
|
func (server *ServerConn) GetContext() context.Context {
|
|
return server.ctx
|
|
}
|
|
|
|
// getConn returns the driver connection
|
|
func (server *ServerConn) getConn() driver.Conn {
|
|
return server.Conn
|
|
}
|
|
|
|
func (server *ServerConn) createHistoricalFirstSeenTable() error {
|
|
err := server.Conn.Exec(context.Background(), `--sql
|
|
CREATE TABLE IF NOT EXISTS metadatabase.historical_first_seen (
|
|
ip IPv6,
|
|
fqdn String,
|
|
first_seen SimpleAggregateFunction(min, DateTime()),
|
|
last_seen SimpleAggregateFunction(max, DateTime())
|
|
) ENGINE = AggregatingMergeTree()
|
|
PRIMARY KEY (fqdn, ip)
|
|
`)
|
|
return err
|
|
}
|
|
|
|
// createSensorDatabase creates a database for the specified sensor and returns a connection to it
|
|
func (server *ServerConn) createSensorDatabase(cfg *config.Config, dbName string, rolling bool) (*DB, error) {
|
|
logger := zlog.GetLogger()
|
|
|
|
// create a database named after the specified sensor
|
|
ctx := clickhouse.Context(context.Background(), clickhouse.WithParameters(clickhouse.Parameters{
|
|
"database": dbName,
|
|
}))
|
|
|
|
err := server.Conn.Exec(ctx, "CREATE DATABASE IF NOT EXISTS {database:Identifier}")
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).
|
|
Str("database connection", cfg.Env.DBConnection).
|
|
Msg("failed to create sensor database")
|
|
return nil, err
|
|
}
|
|
|
|
// connect to newly created database
|
|
db, err := ConnectToDB(server.ctx, dbName, cfg, server.cancel)
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).
|
|
Str("database connection", cfg.Env.DBConnection).
|
|
Msg("failed to connect to sensor database")
|
|
return nil, err
|
|
}
|
|
|
|
// set rolling flag
|
|
db.Rolling = rolling
|
|
|
|
// create tables for the newly created database
|
|
err = db.createSensorDBTables()
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).
|
|
Str("database connection", cfg.Env.DBConnection).
|
|
Msg("failed to create tables for import database")
|
|
return nil, err
|
|
}
|
|
// create analysis tables for the newly created database
|
|
err = db.createSensorDBAnalysisTables()
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).
|
|
Str("database connection", cfg.Env.DBConnection).
|
|
Msg("failed to create analysis tables for import database")
|
|
return nil, err
|
|
}
|
|
|
|
// if the database is rolling, create the necessary TTLs on the tables for cleanup
|
|
if db.Rolling {
|
|
if err := db.createLogTableTTLs(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := db.createSnapshotTableTTLs(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// DropMultipleSensorDatabases drops the databases that match the specified wildcard
|
|
// a wildcard can be in the beginning, end, or both
|
|
func (server *ServerConn) DropMultipleSensorDatabases(dbName string, wildcardStart, wildcardEnd bool) (int, error) {
|
|
var query string
|
|
// switch {
|
|
// case wildcardStart && !wildcardEnd:
|
|
// query = "SHOW DATABASES LIKE '%{database:String}'"
|
|
// case !wildcardStart && wildcardEnd:
|
|
// query = "SHOW DATABASES LIKE '{database:String}%'"
|
|
// case wildcardStart && wildcardEnd:
|
|
// query = "SHOW DATABASES LIKE '%{database:String}%'"
|
|
// case !wildcardStart && !wildcardEnd:
|
|
// return 0, errors.New("no wildcard specified for deleting multiple datasets")
|
|
// }
|
|
|
|
// create query to get the databases that match the wildcard
|
|
switch {
|
|
case wildcardStart && wildcardEnd:
|
|
query = fmt.Sprintf("SHOW DATABASES LIKE '%%%s%%'", dbName)
|
|
case wildcardStart:
|
|
query = fmt.Sprintf("SHOW DATABASES LIKE '%%%s'", dbName)
|
|
case wildcardEnd:
|
|
query = fmt.Sprintf("SHOW DATABASES LIKE '%s%%'", dbName)
|
|
default:
|
|
return 0, errors.New("no wildcard specified for deleting multiple datasets")
|
|
}
|
|
|
|
// execute the query
|
|
paramsCtx := clickhouse.Context(server.ctx, clickhouse.WithParameters(clickhouse.Parameters{"database": dbName}))
|
|
rows, err := server.Conn.Query(paramsCtx, query)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
// create a counter to keep track of the number of databases deleted
|
|
var numDeleted int
|
|
|
|
// iterate over the databases that match the wildcard
|
|
for rows.Next() {
|
|
// get the database name
|
|
var foundDB string
|
|
err := rows.Scan(&foundDB)
|
|
if err != nil {
|
|
return numDeleted, err
|
|
}
|
|
|
|
// drop the database
|
|
err = server.DeleteSensorDB(foundDB)
|
|
if err != nil {
|
|
return numDeleted, err
|
|
}
|
|
|
|
// increment the number of databases deleted
|
|
numDeleted++
|
|
}
|
|
|
|
return numDeleted, nil
|
|
}
|
|
|
|
// dropSensorDatabase drops the specified sensor database
|
|
func (server *ServerConn) dropSensorDatabase(dbName string) error {
|
|
logger := zlog.GetLogger()
|
|
err := dropDatabase(server.ctx, server.Conn, dbName)
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).Msg("failed to drop database")
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteSensorDB deletes the specified database along with its associated imported files in metadatabase.files
|
|
func (server *ServerConn) DeleteSensorDB(database string) error {
|
|
// drop the database
|
|
if err := server.dropSensorDatabase(database); err != nil {
|
|
return err
|
|
}
|
|
|
|
// clear entries in metadatabase
|
|
if err := server.ClearMetaDBEntriesForDatabase(database); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetRollingStatus gets the rolling status of a database
|
|
func GetRollingStatus(dbCtx context.Context, conn driver.Conn, dbName string) (bool, error) {
|
|
var result struct {
|
|
Rolling bool `ch:"rolling"`
|
|
}
|
|
|
|
// if import database does not exist, return an error
|
|
exists, err := SensorDatabaseExists(dbCtx, conn, dbName)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !exists {
|
|
return false, ErrDatabaseNotFound
|
|
}
|
|
|
|
// check the rolling status by looking at the most recent rebuild
|
|
ctx := clickhouse.Context(dbCtx, clickhouse.WithParameters(clickhouse.Parameters{"database": dbName}))
|
|
err = conn.QueryRow(ctx, `
|
|
SELECT rolling FROM metadatabase.min_max WHERE database = {database:String}
|
|
ORDER BY max_ts DESC
|
|
LIMIT 1
|
|
`).ScanStruct(&result)
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return result.Rolling, nil
|
|
}
|
|
|
|
// checkRolling checks the rolling status of a database
|
|
func (server *ServerConn) checkRolling(dbName string, rollingFlag bool, rebuildFlag bool) (bool, error) {
|
|
logger := zlog.GetLogger()
|
|
|
|
// get the current rolling status of the database from the imports table (if db already exists)
|
|
rolling, err := GetRollingStatus(server.ctx, server.Conn, dbName)
|
|
|
|
switch {
|
|
// if database doesn't exist, just return the desired rolling status from flag
|
|
case errors.Is(err, ErrDatabaseNotFound) || errors.Is(err, sql.ErrNoRows):
|
|
return rollingFlag, nil
|
|
|
|
// error executing query
|
|
case err != nil:
|
|
logger.Err(err).Str("database", dbName).
|
|
Str("database connection", server.addr).
|
|
Msg(errRollingStatusFailure.Error())
|
|
return rolling, errRollingStatusFailure
|
|
|
|
// command is requesting to import data as rolling, but dataset is not rolling
|
|
case rollingFlag && !rolling && !rebuildFlag:
|
|
logger.Warn().Str("database", dbName).
|
|
Msg(ErrImportTwiceNonRolling.Error())
|
|
return rolling, ErrImportTwiceNonRolling
|
|
|
|
// command is requesting to import data as non-rolling, but dataset is rolling
|
|
case rolling && !rollingFlag && !rebuildFlag:
|
|
logger.Warn().Str("database", dbName).
|
|
Msg(errRollingFlagMissing.Error())
|
|
return rolling, errRollingFlagMissing
|
|
}
|
|
|
|
return rolling, nil
|
|
}
|
|
|
|
type ImportDatabase struct {
|
|
Name string `ch:"database" json:"name"`
|
|
Rolling bool `ch:"rolling" json:"rolling"`
|
|
MinTS time.Time `ch:"min_ts" json:"minTS"`
|
|
MaxTS time.Time `ch:"max_ts" json:"maxTS"`
|
|
IsSampleDB bool `ch:"is_sample" json:"is_sample"`
|
|
}
|
|
|
|
func (server *ServerConn) ListImportDatabases() ([]ImportDatabase, error) {
|
|
logger := zlog.GetLogger()
|
|
|
|
// if metadatabase does not exist, return an empty list
|
|
exists, err := DatabaseExists(server.ctx, server.Conn, "metadatabase")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
|
|
var sensorDBs []ImportDatabase
|
|
|
|
// return list of databases based on min_max table
|
|
query := `
|
|
WITH sample_dbs AS (
|
|
SELECT DISTINCT name FROM metadatabase.sample_dbs
|
|
)
|
|
SELECT database, rolling, greatest(min_ts, timestamp_sub(WEEK, 2, max_ts)) as min_ts, max_ts, notEmpty(sdb.name) as is_sample FROM (
|
|
SELECT database, rolling, min(min_ts) AS min_ts, max(max_ts) AS max_ts FROM metadatabase.min_max
|
|
GROUP BY database, rolling
|
|
ORDER BY max_ts DESC
|
|
) as dbs
|
|
LEFT JOIN sample_dbs sdb ON dbs.database = sdb.name
|
|
`
|
|
err = server.Conn.Select(server.ctx, &sensorDBs, query)
|
|
if err != nil {
|
|
logger.Err(err).Str("database connection", server.addr).Msg("failed to execute import database list query")
|
|
return nil, err
|
|
}
|
|
|
|
return sensorDBs, nil
|
|
}
|
|
|
|
func SensorDatabaseExists(ctx context.Context, conn driver.Conn, dbName string) (bool, error) {
|
|
logger := zlog.GetLogger()
|
|
// check if database actually exists
|
|
dbExists, err := DatabaseExists(ctx, conn, dbName)
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).Msg("failed to check if database exists")
|
|
return false, err
|
|
}
|
|
if !dbExists {
|
|
return false, nil
|
|
}
|
|
|
|
// check if database is listed in metadatabase
|
|
paramsCtx := clickhouse.Context(ctx, clickhouse.WithParameters(clickhouse.Parameters{"database": dbName}))
|
|
|
|
var exists uint64
|
|
err = conn.QueryRow(paramsCtx, "SELECT count() FROM metadatabase.min_max WHERE database = {database:String}").Scan(&exists)
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).Msg("failed to check if database exists in metadatabase")
|
|
return false, err
|
|
}
|
|
return exists > 0, nil
|
|
}
|
|
|
|
// GetFlatDatabaseList returns a list of database names from a list of ImportDatabase structs
|
|
func GetFlatDatabaseList(dbs []ImportDatabase) []string {
|
|
var dbList []string
|
|
for _, db := range dbs {
|
|
dbList = append(dbList, db.Name)
|
|
}
|
|
return dbList
|
|
}
|
|
|
|
func DatabaseExists(ctx context.Context, conn driver.Conn, dbName string) (bool, error) {
|
|
logger := zlog.GetLogger()
|
|
|
|
paramsCtx := clickhouse.Context(ctx, clickhouse.WithParameters(clickhouse.Parameters{"database": dbName}))
|
|
|
|
var exists uint64
|
|
err := conn.QueryRow(paramsCtx, "SELECT count() FROM system.databases WHERE name = {database:String}").Scan(&exists)
|
|
if err != nil {
|
|
logger.Err(err).Str("database", dbName).Msg("failed to check if database exists")
|
|
return false, err
|
|
}
|
|
|
|
return exists > 0, nil
|
|
}
|
|
|
|
// dropDatabase drops the specified database
|
|
func dropDatabase(ctx context.Context, conn driver.Conn, dbName string) error {
|
|
paramsCtx := clickhouse.Context(ctx, clickhouse.WithParameters(clickhouse.Parameters{
|
|
"database": dbName,
|
|
}))
|
|
err := conn.Exec(paramsCtx, "DROP DATABASE IF EXISTS {database:Identifier}")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ConnectToServer connects to the clickhouse server as the default user
|
|
func ConnectToServer(ctx context.Context, cfg *config.Config) (*ServerConn, error) {
|
|
logger := zlog.GetLogger()
|
|
|
|
conn, err := clickhouse.Open(&clickhouse.Options{
|
|
Addr: []string{cfg.Env.DBConnection}, // read from env instead
|
|
Auth: clickhouse.Auth{
|
|
Database: "default",
|
|
Username: cfg.Env.DBUsername,
|
|
Password: cfg.Env.DBPassword,
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
logger.Err(err).Str("database", "default").
|
|
Str("database connection", cfg.Env.DBConnection).
|
|
Str("user", cfg.Env.DBUsername).
|
|
Msg("failed to connect to ClickHouse server")
|
|
return nil, err
|
|
}
|
|
|
|
// ping the server to verify connection
|
|
if err := conn.Ping(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ServerConn{
|
|
Conn: conn,
|
|
addr: cfg.Env.DBConnection,
|
|
ctx: ctx,
|
|
}, nil
|
|
}
|