Files
activecm-rita/database/analysis_tables.go
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

484 lines
13 KiB
Go

package database
import (
"context"
clickhouse "github.com/ClickHouse/clickhouse-go/v2"
)
func (db *DB) createThreatMixtapeTable(ctx context.Context) error {
err := db.Conn.Exec(ctx, `--sql
CREATE TABLE IF NOT EXISTS {database:Identifier}.threat_mixtape (
analyzed_at DateTime64(6),
import_id FixedString(16),
hash FixedString(16),
src IPv6,
dst IPv6,
src_nuid UUID,
dst_nuid UUID,
fqdn String,
server_ips Array(IPv6),
proxy_ips Array(IPv6),
total_bytes UInt64,
last_seen DateTime(),
port_proto_service Array(String),
-- counts
count UInt64,
ts_unique UInt64,
proxy_count UInt64,
open_count UInt64,
-- **** THREAT INDICATORS ****
-- BEACONING
beacon_type LowCardinality(String),
beacon_score Float64,
beacon_threat_score Float64,
ts_score Float64,
ds_score Float64,
dur_score Float64,
hist_score Float64,
ts_intervals Array(Int64),
ts_interval_counts Array(Int64),
ds_sizes Array(Int64),
ds_size_counts Array(Int64),
-- LONG CONNECTIONS
total_duration Float64,
long_conn_score Float64,
-- STROBE
strobe_score Float64,
-- C2 OVER DNS
subdomain_count UInt64,
c2_over_dns_score Float64,
c2_over_dns_direct_conn_score Float64,
-- THREAT INTEL
threat_intel Bool,
threat_intel_score Float64,
-- **** MODIFIERS ****
modifier_name LowCardinality(String),
modifier_score Float64,
modifier_value String,
-- PREVALENCE
prevalence_total UInt64,
prevalence Float64,
prevalence_score Float64,
network_size UInt64,
first_seen_historical DateTime(),
first_seen_score Float64,
-- THREAT INTEL DATA SIZE
threat_intel_data_size_score Float64,
-- MISSING HOST HEADER
missing_host_count UInt64,
missing_host_header_score Float64
) ENGINE = MergeTree()
PRIMARY KEY (analyzed_at, dst_nuid, src_nuid, src, fqdn, dst, hash)
ORDER BY (analyzed_at, dst_nuid, src_nuid, src, fqdn, dst, hash)
`)
return err
}
func (db *DB) createHistoricalFirstSeenMaterializedViews(ctx context.Context) error {
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.historical_first_seen_conn_mv
TO metadatabase.historical_first_seen AS
SELECT
if(src_local = true, dst, src) as ip,
'' as fqdn,
minSimpleState(ts) as first_seen,
maxSimpleState(ts) as last_seen
FROM {database:Identifier}.conn
GROUP BY (fqdn, ip)
`); err != nil {
return err
}
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.historical_first_seen_openconn_mv
TO metadatabase.historical_first_seen AS
SELECT
if(src_local = true, dst, src) as ip,
'' as fqdn,
minSimpleState(ts) as first_seen,
maxSimpleState(ts) as last_seen
FROM {database:Identifier}.openconn
GROUP BY (fqdn, ip)
`); err != nil {
return err
}
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.historical_first_seen_ssl_mv
TO metadatabase.historical_first_seen AS
SELECT
'::' as ip,
server_name as fqdn,
minSimpleState(ts) as first_seen,
maxSimpleState(ts) as last_seen
FROM {database:Identifier}.ssl
GROUP BY ( fqdn, ip)
`); err != nil {
return err
}
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.historical_first_seen_openssl_mv
TO metadatabase.historical_first_seen AS
SELECT
'::' as ip,
server_name as fqdn,
minSimpleState(ts) as first_seen,
maxSimpleState(ts) as last_seen
FROM {database:Identifier}.openssl
GROUP BY ( fqdn, ip)
`); err != nil {
return err
}
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.historical_first_seen_http_mv
TO metadatabase.historical_first_seen AS
SELECT
'::' as ip,
host as fqdn,
minSimpleState(ts) as first_seen,
maxSimpleState(ts) as last_seen
FROM {database:Identifier}.http
WHERE host != ''
GROUP BY ( fqdn, ip)
`); err != nil {
return err
}
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.historical_first_seen_openhttp_mv
TO metadatabase.historical_first_seen AS
SELECT
'::' as ip,
host as fqdn,
minSimpleState(ts) as first_seen,
maxSimpleState(ts) as last_seen
FROM {database:Identifier}.openhttp
WHERE host != ''
GROUP BY ( fqdn, ip)
`); err != nil {
return err
}
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.historical_first_seen_dns_mv
TO metadatabase.historical_first_seen AS
SELECT
'::' as ip,
query as fqdn,
minSimpleState(ts) as first_seen,
maxSimpleState(ts) as last_seen
FROM {database:Identifier}.dns
GROUP BY ( fqdn, ip)
`); err != nil {
return err
}
return nil
}
func (db *DB) createMIMETypeURIsTable(ctx context.Context) error {
err := db.Conn.Exec(ctx, `--sql
CREATE TABLE IF NOT EXISTS {database:Identifier}.mime_type_uris (
import_hour DateTime(),
hour DateTime(),
hash FixedString(16),
uri String,
path String,
extension String,
mime_type String,
mismatch_count AggregateFunction(count, UInt64),
)
ENGINE = AggregatingMergeTree()
PRIMARY KEY (hour, hash, uri)
`)
if err != nil {
return err
}
// This view is used to detect MIME type/URI mismatches
// If a HTTP connection's MIME type matches a MIME type in the metadatabase.valid_mime_types table
// and its extension does not match the associated values for that MIME type, then it should be added to this table
err = db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.mime_type_uris_mv
TO {database:Identifier}.mime_type_uris AS
SELECT
toStartOfHour(import_time) as import_hour,
toStartOfHour(ts) as hour,
hash,
uri,
path(uri) as path,
-- get the extension from the path
CASE
-- if the path does not contain a . or ends with a ., then the extension is an empty string
WHEN position(reverse(arrayElement(splitByChar('/', path), -1)), '.') = 0 OR endsWith(path, '.') THEN ''
-- otherwise, split the last segment by . and take the last element as the extension
ELSE splitByChar('.', arrayElement(splitByChar('/', path), -1))[-1]
END AS extension,
dst_mime_types as mime_type,
countState() AS mismatch_count
FROM {database:Identifier}.http h
-- for each uri, get the extension and join it with the valid mime types,
-- keeping only the rows where the extension does not match the valid extension
ARRAY JOIN dst_mime_types
LEFT SEMI JOIN metadatabase.valid_mime_types v ON dst_mime_types = v.mime_type
WHERE uri != '/' AND extension != v.extension
GROUP BY import_hour, hour, hash, uri, path, extension, mime_type
`)
if err != nil {
return err
}
return nil
}
func (db *DB) createRareSignatureTable(ctx context.Context) error {
err := db.Conn.Exec(ctx, `--sql
CREATE TABLE IF NOT EXISTS {database:Identifier}.rare_signatures (
import_hour DateTime(),
hour DateTime(),
src IPv6,
src_nuid UUID,
dst IPv6,
dst_nuid UUID,
fqdn String,
signature String,
is_ja3 Bool,
times_used_dst AggregateFunction(uniqExact, IPv6),
times_used_fqdn AggregateFunction(uniqExact, String)
)
ENGINE = AggregatingMergeTree()
PRIMARY KEY (hour, src_nuid, src, dst, dst_nuid, fqdn, signature )
`)
if err != nil {
return err
}
err = db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.rare_signatures_http_mv
TO {database:Identifier}.rare_signatures AS
SELECT
toStartOfHour(import_time) as import_hour,
toStartOfHour(ts) as hour,
src,
src_nuid,
host AS fqdn,
useragent as signature,
false as is_ja3,
uniqExactState(dst) as times_used_dst,
uniqExactState(host) as times_used_fqdn
FROM {database:Identifier}.http
WHERE length(useragent) > 0 AND length(host) > 0
GROUP BY (import_hour, hour, src, src_nuid, fqdn, signature, is_ja3)
`)
if err != nil {
return err
}
err = db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.rare_signatures_ssl_mv
TO {database:Identifier}.rare_signatures AS
SELECT
toStartOfHour(import_time) as import_hour,
toStartOfHour(ts) as hour,
src,
src_nuid,
server_name AS fqdn,
ja3 as signature,
true as is_ja3,
uniqExactState(dst) as times_used_dst,
uniqExactState(server_name) as times_used_fqdn
FROM {database:Identifier}.ssl
WHERE length(ja3) > 0
GROUP BY (import_hour, hour, src, src_nuid, fqdn, signature, is_ja3)
`)
if err != nil {
return err
}
err = db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.rare_signatures_missing_host_mv
TO {database:Identifier}.rare_signatures AS
SELECT
toStartOfHour(import_time) as import_hour,
toStartOfHour(ts) as hour,
src,
src_nuid,
dst,
dst_nuid,
missing_host_useragent as signature,
false as is_ja3,
uniqExactState(if(src_local, dst, src)) as times_used_dst
FROM {database:Identifier}.conn
WHERE length(missing_host_useragent) > 0 AND missing_host_header = true
GROUP BY (import_hour, hour, src, src_nuid, dst, dst_nuid, signature, is_ja3)
`)
if err != nil {
return err
}
return err
}
func (db *DB) createPortInfoTable(ctx context.Context) error {
if err := db.Conn.Exec(ctx, `--sql
CREATE TABLE IF NOT EXISTS {database:Identifier}.port_info (
import_hour DateTime(),
hour DateTime(),
hash FixedString(16),
src IPv6,
src_nuid UUID,
dst IPv6,
dst_nuid UUID,
fqdn String,
dst_port UInt32,
proto LowCardinality(String),
service LowCardinality(String),
icmp_type Int64,
icmp_code Int64,
conn_state LowCardinality(String),
count AggregateFunction(count, UInt64),
bytes_sent AggregateFunction(sum, UInt64),
bytes_received AggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree()
PRIMARY KEY (hour, hash, dst_port, proto, service, conn_state, icmp_type, icmp_code)
`); err != nil {
return err
}
// conn
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.port_info_ip_mv
TO {database:Identifier}.port_info AS
SELECT
toStartOfHour(import_time) as import_hour,
toStartOfHour(ts) as hour,
hash,
src,
src_nuid,
dst,
dst_nuid,
dst_port,
proto,
service,
if(proto = 'icmp', src_port, 0) as icmp_type,
if(proto = 'icmp', dst_port, 0) as icmp_code,
conn_state,
countState() as count,
sumState(src_ip_bytes) as bytes_sent,
sumState(dst_ip_bytes) as bytes_received
FROM {database:Identifier}.conn
WHERE missing_host_header = false
GROUP BY (import_hour, hour, hash, src, src_nuid, dst, dst_nuid, dst_port, proto, service, icmp_type, icmp_code, conn_state)
`); err != nil {
return err
}
// http
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.port_info_http_mv
TO {database:Identifier}.port_info AS
SELECT
toStartOfHour(import_time) as import_hour,
toStartOfHour(ts) as hour,
hash,
src,
src_nuid,
host as fqdn,
dst_port,
proto,
service,
conn_state,
countStateIf(multi_request = false) as count, -- only count unique zeek_uids, not each multi-request
sumState(src_ip_bytes) as bytes_sent,
sumState(dst_ip_bytes) as bytes_received
FROM {database:Identifier}.http
GROUP BY (import_hour, hour, hash, src, src_nuid, fqdn, dst_port, proto, service, conn_state)
`); err != nil {
return err
}
// ssl
if err := db.Conn.Exec(ctx, `--sql
CREATE MATERIALIZED VIEW IF NOT EXISTS {database:Identifier}.port_info_ssl_mv
TO {database:Identifier}.port_info AS
SELECT
toStartOfHour(import_time) as import_hour,
toStartOfHour(ts) as hour,
hash,
src,
src_nuid,
server_name as fqdn,
dst_port,
proto,
service,
conn_state,
countState() as count,
sumState(src_ip_bytes) as bytes_sent,
sumState(dst_ip_bytes) as bytes_received
FROM {database:Identifier}.ssl
GROUP BY (import_hour, hour, hash, src, src_nuid, fqdn, dst_port, proto, service, conn_state)
`); err != nil {
return err
}
return nil
}
func (db *DB) createSensorDBAnalysisTables() error {
ctx := db.QueryParameters(clickhouse.Parameters{
"database": db.selected,
})
err := db.createThreatMixtapeTable(ctx)
if err != nil {
return err
}
err = db.createRareSignatureTable(ctx)
if err != nil {
return err
}
err = db.createMIMETypeURIsTable(ctx)
if err != nil {
return err
}
err = db.createPortInfoTable(ctx)
if err != nil {
return err
}
// only create historical first seen mvs for rolling datasets
if db.Rolling {
err = db.createHistoricalFirstSeenMaterializedViews(ctx)
if err != nil {
return err
}
}
return nil
}