mirror of
https://github.com/activecm/rita
synced 2026-06-08 13:02:45 +00:00
530c835daa
Co-Authored-By: Liza Tsibur <liza@activecountermeasures.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 float32
|
|
}
|
|
|
|
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
|
|
}
|