1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
package source
import (
"sort"
"strings"
"time"
"github.com/pranshuparmar/witr/pkg/model"
)
type envSuspiciousRule struct {
pattern string
match func(key, pattern string) bool
warning string
includeKeys bool
}
var (
envVarRules = []envSuspiciousRule{
{
pattern: "LD_PRELOAD",
match: func(key, pattern string) bool { return key == pattern },
warning: "Process sets LD_PRELOAD (potential library injection)",
},
{
pattern: "DYLD_",
match: strings.HasPrefix,
warning: "Process sets DYLD_* variables (potential library injection)",
includeKeys: true,
},
}
)
func Detect(ancestry []model.Process) model.Source {
// Detection order prioritizes platform-specific init systems
// over generic supervisor detection to avoid false positives
if src := detectContainer(ancestry); src != nil {
return *src
}
if src := detectSystemd(ancestry); src != nil {
return *src
}
if src := detectLaunchd(ancestry); src != nil {
return *src
}
if src := detectBsdRc(ancestry); src != nil {
return *src
}
if src := detectSupervisor(ancestry); src != nil {
return *src
}
if src := detectCron(ancestry); src != nil {
return *src
}
if src := detectWindowsService(ancestry); src != nil {
return *src
}
if src := detectInit(ancestry); src != nil {
return *src
}
if src := detectShell(ancestry); src != nil {
return *src
}
return model.Source{
Type: model.SourceUnknown,
}
}
// env suspicious warnings returns warnings for known env based library injection patterns
func envSuspiciousWarnings(env []string) []string {
matched := make([]bool, len(envVarRules))
matchedKeys := make([]map[string]struct{}, len(envVarRules))
// init per rule key capture only for rules that include keys
for i, rule := range envVarRules {
if rule.includeKeys {
matchedKeys[i] = map[string]struct{}{}
}
}
// scan env entries and record which rules match
for _, entry := range env {
key, value, ok := strings.Cut(entry, "=")
if !ok || value == "" {
continue
}
// check this key against each configured rule
for i, rule := range envVarRules {
if !rule.match(key, rule.pattern) {
continue
}
matched[i] = true
if rule.includeKeys {
matchedKeys[i][key] = struct{}{}
}
}
}
var warnings []string
// emit warnings in the same order as envVarRules
for i, rule := range envVarRules {
if !matched[i] {
continue
}
if !rule.includeKeys {
warnings = append(warnings, rule.warning)
continue
}
keys := make([]string, 0, len(matchedKeys[i]))
// collect all matched keys for this rule
for key := range matchedKeys[i] {
keys = append(keys, key)
}
sort.Strings(keys)
warnings = append(warnings, rule.warning+": "+strings.Join(keys, ", "))
}
return warnings
}
func Warnings(p []model.Process) []string {
var w []string
last := p[len(p)-1]
// Restart count detection (count consecutive same-command entries)
restartCount := 0
lastCmd := ""
for _, proc := range p {
if proc.Command == lastCmd {
restartCount++
}
lastCmd = proc.Command
}
if restartCount > 5 {
w = append(w, "Process or ancestor restarted more than 5 times")
}
// Health warnings
switch last.Health {
case "zombie":
w = append(w, "Process is a zombie (defunct)")
case "stopped":
w = append(w, "Process is stopped (T state)")
case "high-cpu":
w = append(w, "Process is using high CPU (>2h total)")
case "high-mem":
w = append(w, "Process is using high memory (>1GB RSS)")
}
if IsPublicBind(last.BindAddresses) {
w = append(w, "Process is listening on a public interface")
}
if last.User == "root" {
w = append(w, "Process is running as root")
}
if Detect(p).Type == model.SourceUnknown {
w = append(w, "No known supervisor or service manager detected")
}
// Warn if process is very old (>90 days)
if time.Since(last.StartedAt).Hours() > 90*24 {
w = append(w, "Process has been running for over 90 days")
}
// Warn if working dir is suspicious
suspiciousDirs := map[string]bool{"/": true, "/tmp": true, "/var/tmp": true}
if suspiciousDirs[last.WorkingDir] {
w = append(w, "Process is running from a suspicious working directory: "+last.WorkingDir)
}
// Warn if container and no healthcheck (placeholder, as healthcheck not detected)
if last.Container != "" {
w = append(w, "No healthcheck detected for container (best effort)")
}
// Warn if service name and process name mismatch
if last.Service != "" && last.Command != "" && last.Service != last.Command {
w = append(w, "Service name and process name do not match")
}
// Warn if binary is deleted
if last.ExeDeleted {
w = append(w, "Process is running from a deleted binary (potential library injection or pending update)")
}
// Include warnings based on suspicious env variables
w = append(w, envSuspiciousWarnings(last.Env)...)
return w
}
// EnrichSocketInfo provides human-readable explanations and workarounds for socket states
func EnrichSocketInfo(si *model.SocketInfo) {
if si == nil {
return
}
switch si.State {
case "TIME_WAIT":
si.Explanation = "The local OS is holding the port in a protocol-wait state to ensure all packets are received."
si.Workaround = "Wait ~60s for the OS to release it, or enable SO_REUSEADDR in your code."
case "CLOSE_WAIT":
si.Explanation = "The remote end has closed the connection, but the local application hasn't responded."
si.Workaround = "This usually indicates a resource leak in the application. Restart the process."
case "FIN_WAIT_1", "FIN_WAIT_2":
si.Explanation = "The connection is in the process of being closed."
case "ESTABLISHED":
si.Explanation = "The connection is active and data can be transferred."
case "LISTEN":
si.Explanation = "The process is actively waiting for incoming connections."
}
}
|