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>
285 lines
9.5 KiB
Go
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
|
|
}
|