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

285 lines
9.5 KiB
Go

package viewer
import (
"fmt"
"strings"
"time"
"github.com/activecm/rita/v5/util"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var sideBarStyle = lipgloss.NewStyle()
type modifier struct {
label string
value string
delta float64
}
type sidebarModel struct {
Viewport viewport.Model
Data *Item
Height int
maxTimestamp time.Time
useCurrentTime bool
ScrollEnabled bool
}
func NewSidebarModel(maxTS time.Time, useCurrentTime bool, initialData *Item) sidebarModel {
return sidebarModel{
Viewport: viewport.Model{},
maxTimestamp: maxTS,
useCurrentTime: useCurrentTime,
Data: initialData,
}
}
func (m *sidebarModel) Init() tea.Cmd {
m.Viewport.SetContent(m.getSidebarContents())
return nil
}
type UpdateItem *Item
func (m *sidebarModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case UpdateItem:
m.Data = msg
content := m.getSidebarContents()
numlines := strings.Count(content, "\n") + 1 + 2
numToClear := m.Viewport.Height - numlines
if numToClear > 0 {
spaces := m.Viewport.Width - 2
for i := 0; i < numToClear; i++ {
content += fmt.Sprintf("%*s\n", spaces, "")
}
}
m.Viewport.SetContent(content)
case tea.WindowSizeMsg:
cmds = append(cmds, viewport.Sync(m.Viewport))
}
return m, tea.Batch(cmds...)
}
func (m *sidebarModel) View() string {
borderColor := mauve
if m.ScrollEnabled {
borderColor = green
}
style := sideBarStyle.
// .Width(m.width)
// Height(m.Viewport.Height).
Padding(0, 1).
Border(lipgloss.RoundedBorder()).
BorderForeground(borderColor)
sidebar := style.Render(m.Viewport.View())
return lipgloss.NewStyle().Render(sidebar)
}
// getSidebarContents gets and formats the data to be displayed in the sidebar
func (m *sidebarModel) getSidebarContents() string {
if m.Data == nil {
return lipgloss.NewStyle().Foreground(overlay0).Render("No result found.")
}
// get header
var target string
headerPadding := 2
headerLabelStyle := lipgloss.NewStyle().Padding(0, headerPadding).Background(overlay0).Foreground(defaultTextColor).Bold(true)
headerValueStyle := lipgloss.NewStyle().Padding(0, headerPadding).Background(mauve).Foreground(base).Bold(true)
// handle c2 over dns threats with just a fqdn as the target
if m.Data.GetSrc() == "" {
// dstStyle := lipgloss.NewStyle().Width(m.viewport.Width - (headerPadding * 2))
fqdnLabel := "FQDN"
dstStyle := lipgloss.NewStyle().Width(m.Viewport.Width - len(fqdnLabel) - (headerPadding * 4))
valueStyle := headerValueStyle.Render(Truncate(m.Data.GetDst(), &dstStyle))
target = lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(fqdnLabel), valueStyle)
target = lipgloss.NewStyle().MarginBottom(2).Render(target)
} else {
// handle connection pair, ip -> ip or ip -> fqdn
srcLabel := "SRC"
srcStyle := lipgloss.NewStyle().Width(m.Viewport.Width - len(srcLabel) - (headerPadding * 4))
dstLabel := "DST"
dstStyle := lipgloss.NewStyle().Width(m.Viewport.Width - len(dstLabel) - (headerPadding * 4))
srcValueStyle := headerValueStyle.Render(Truncate(m.Data.GetSrc(), &srcStyle))
dstValueStyle := headerValueStyle.Render(Truncate(m.Data.GetDst(), &dstStyle))
src := lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(srcLabel), srcValueStyle)
dst := lipgloss.JoinHorizontal(lipgloss.Left, headerLabelStyle.Render(dstLabel), dstValueStyle)
target = lipgloss.JoinVertical(lipgloss.Top, lipgloss.NewStyle().MarginBottom(1).Render(src), dst)
}
heading := lipgloss.NewStyle().MarginBottom(1).Render(target)
// get modifiers
sectionStyle := lipgloss.NewStyle().
Foreground(overlay2).
Border(lipgloss.NormalBorder(), false, false, true, false).
BorderForeground(surface0).
Width(m.Viewport.Width)
modifierLabel := sectionStyle.Render("「 Threat Modifiers 」")
modifiers := m.renderModifiers()
dataStyle := lipgloss.NewStyle().Foreground(defaultTextColor)
var connInfoLabel, connCount, bytes string
// display connection count and bytes for everything except C2 over DNS
if m.Data.C2OverDNSScore == 0 {
connInfoLabel = sectionStyle.Render("「 Connection Info 」")
// get connection count
connCountStyle := lipgloss.NewStyle().Background(overlay2).Foreground(base).Bold(true).Padding(0, 2)
connCountHeader := connCountStyle.Render("Connection Count")
connCount = dataStyle.Render(lipgloss.JoinVertical(lipgloss.Top, connCountHeader, fmt.Sprintf("%d", m.Data.Count)))
// get total bytes
bytesHeaderStyle := lipgloss.NewStyle().Background(overlay2).Foreground(base).Bold(true).Padding(0, 2)
bytesHeader := bytesHeaderStyle.Render("Total Bytes")
bytes = dataStyle.Render(lipgloss.JoinVertical(lipgloss.Top, bytesHeader, m.Data.TotalBytesFormatted))
}
// get port:proto:service
portProtoService := m.Data.GetPortProtoService()
// DEBUG SIDEFEED SCROLLING WITH LONG PORT:PROTO:SERVICE
// var portProtoService []string
// for i := 0; i < 20; i++ {
// portProtoService = append(portProtoService, fmt.Sprintf("%d : %s : %s", i, "tcp", "http"))
// }
ports := ""
if len(portProtoService) > 0 {
// create style for header
portsHeaderStyle := lipgloss.NewStyle().Background(overlay2).Foreground(base).Bold(true).Padding(0, 2).MarginTop(1)
// render header
portsHeader := portsHeaderStyle.Render("Port : Proto : Service")
ports = dataStyle.Render(lipgloss.JoinVertical(lipgloss.Top, portsHeader, strings.Join(portProtoService, "\n")))
}
// join contents
return lipgloss.JoinVertical(lipgloss.Top, heading, modifierLabel, modifiers, connInfoLabel, connCount, bytes, ports)
}
// renderModifiers aggregates and formats the modifiers for the currently selected item
// for rendering in the sidebar
func (m *sidebarModel) renderModifiers() string {
modifierList := m.getModifiers()
var modifiers string
var renderedModifiers []string
for _, modifier := range modifierList {
renderedModifier := renderModifier(modifier)
renderedModifiers = append(renderedModifiers, renderedModifier)
}
newlineStyle := lipgloss.NewStyle().PaddingRight(1).BorderForeground(overlay2).Border(lipgloss.NormalBorder(), false, true, false, false)
linebreakStyle := lipgloss.NewStyle().MarginBottom(1)
var modifierLines []string
var currentModifiers string
for i, mod := range renderedModifiers {
if i == 0 {
currentModifiers = newlineStyle.Render(mod)
} else {
newMod := lipgloss.JoinHorizontal(lipgloss.Left, currentModifiers, lipgloss.NewStyle().Padding(0, 1).BorderForeground(overlay2).Border(lipgloss.NormalBorder(), false, true, false, false).Render(mod))
width := lipgloss.Width(newMod)
if m.Viewport.Width <= width {
modifierLines = append(modifierLines, lipgloss.NewStyle().Foreground(defaultTextColor).Render(lipgloss.JoinHorizontal(lipgloss.Left, linebreakStyle.Render(currentModifiers))))
currentModifiers = mod
if i != len(renderedModifiers)-1 {
currentModifiers = newlineStyle.Render(mod)
}
} else {
currentModifiers = newMod
}
}
}
modifierLines = append(modifierLines, linebreakStyle.Render(currentModifiers))
modifiers = lipgloss.JoinVertical(lipgloss.Top, modifierLines...)
return modifiers
}
// getModifiers gets all the modifiers for the currently selected item
func (m *sidebarModel) getModifiers() []modifier {
var modifiers []modifier
prevalence := "N/A"
if m.Data.Prevalence > 0 {
// prevalence = fmt.Sprintf("%1.0f%%", m.Data.Prevalence*100)
// // show two decimal points if the prevalence is less than 1% to avoid displaying 0%
// if m.Data.Prevalence < 0.01 {
// prevalence = fmt.Sprintf("%1.2f%%", m.Data.Prevalence*100)
// }
prevalence = m.Data.GetPrevalence()
}
modifiers = append(modifiers, modifier{label: "Prevalence", value: prevalence, delta: m.Data.PrevalenceScore})
if m.Data.FirstSeen.Compare(time.Unix(0, 0)) == 1 {
relativeTime := util.GetRelativeFirstSeenTimestamp(m.useCurrentTime, m.maxTimestamp)
modifiers = append(modifiers, modifier{label: "First Seen", value: m.Data.GetFirstSeen(relativeTime), delta: m.Data.FirstSeenScore})
}
if m.Data.MissingHostCount > 0 {
modifiers = append(modifiers, modifier{label: "Missing Host Header", value: fmt.Sprintf("Was missing host %dx", m.Data.MissingHostCount), delta: m.Data.MissingHostHeaderScore})
}
if m.Data.ThreatIntelDataSizeScore != 0 {
var label string
if m.Data.ThreatIntelDataSizeScore > 0 {
label = "[High Data]"
} else {
label = "[Low Data]"
}
modifiers = append(modifiers, modifier{label: "Threat Intel " + label, value: m.Data.TotalBytesFormatted, delta: m.Data.ThreatIntelDataSizeScore})
}
if m.Data.C2OverDNSDirectConnScore != 0 {
modifiers = append(modifiers, modifier{label: "No Direct Connections", value: "", delta: 10})
}
for _, mod := range m.Data.Modifiers {
switch mod["modifier_name"] {
case "rare_signature":
modifiers = append(modifiers, modifier{label: "Rare Signature", value: mod["modifier_value"], delta: 10})
case "mime_type_mismatch":
modifiers = append(modifiers, modifier{label: "MIME Type Mismatch", value: "", delta: 10})
}
}
return modifiers
}
// renderModifier formats and styles a single modifier for rendering
func renderModifier(mod modifier) string {
var color lipgloss.AdaptiveColor
switch {
case mod.delta == 0:
color = overlay2
case mod.delta > 0:
color = red
case mod.delta < 0:
color = green
}
header := lipgloss.NewStyle().Background(color).Foreground(base).Bold(true).Padding(0, 2).Render(mod.label)
data := lipgloss.NewStyle().Foreground(defaultTextColor).Render(mod.value)
modifier := lipgloss.JoinVertical(lipgloss.Top, header, data)
return modifier
}