Files
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

275 lines
7.7 KiB
Go

package source
import (
"fmt"
"runtime"
"sort"
"strings"
"time"
"github.com/pranshuparmar/witr/pkg/model"
)
var suspiciousDirs = map[string]bool{"/": true, "/tmp": true, "/var/tmp": true}
var dangerousCapabilities = map[string]bool{
"CAP_SYS_ADMIN": true,
"CAP_SYS_PTRACE": true,
"CAP_NET_RAW": true,
"CAP_DAC_OVERRIDE": true,
"CAP_DAC_READ_SEARCH": true,
"CAP_FOWNER": true,
"CAP_SYS_MODULE": true,
"CAP_SYS_RAWIO": true,
}
func isDangerousCapability(cap string) bool {
return dangerousCapabilities[cap]
}
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 := detectSSH(ancestry); src != nil {
return *src
}
if src := detectShell(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
}
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, restartCount int, srcType ...model.SourceType) []string {
if len(p) == 0 {
return nil
}
var w []string
last := p[len(p)-1]
// Warn on a service that has restarted many times. restartCount is the real
// count from the service manager (e.g. systemd NRestarts), or 0 when unknown.
if restartCount > 5 {
w = append(w, fmt.Sprintf("Service has restarted %d times", restartCount))
}
// 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.Sockets) {
w = append(w, "Process is listening on a public interface")
}
if last.User == "root" {
w = append(w, "Process is running as root")
} else if len(last.Capabilities) > 0 {
var dangerous []string
for _, cap := range last.Capabilities {
if isDangerousCapability(cap) {
dangerous = append(dangerous, cap)
}
}
if len(dangerous) > 0 {
w = append(w, "Process has dangerous capabilities: "+strings.Join(dangerous, ", "))
}
}
var st model.SourceType
if len(srcType) > 0 {
st = srcType[0]
} else {
st = Detect(p).Type
}
// On Windows the ancestry frequently truncates at an orphaned process
// (Windows leaves a stale PPID instead of reparenting to an init process),
// so an unknown source is normal there — not a reliable "unsupervised"
// signal — and this warning would fire on most user processes.
if st == model.SourceUnknown && runtime.GOOS != "windows" {
w = append(w, "No known supervisor or service manager detected")
}
// Warn if process is very old (>90 days). A zero start time means we
// couldn't read it (e.g. protected Windows processes), not that the process
// is ancient — skip the warning rather than emit a false positive.
if !last.StartedAt.IsZero() && time.Since(last.StartedAt).Hours() > 90*24 {
w = append(w, "Process has been running for over 90 days")
}
if suspiciousDirs[last.WorkingDir] {
w = append(w, "Process is running from a suspicious working directory: "+last.WorkingDir)
}
// Warn only when the runtime confirms no healthcheck is configured. Unknown
// ("") — snap/flatpak, unprobed runtimes, non-Linux — does not warn.
if last.ContainerHealthcheck == "absent" {
w = append(w, "Container has no healthcheck configured")
}
// Warn if service name and process name are genuinely unrelated
if last.Service != "" && last.Command != "" {
svcCore := last.Service
for _, suffix := range []string{".service", ".socket", ".timer", ".scope", ".slice", ".plist"} {
svcCore = strings.TrimSuffix(svcCore, suffix)
}
// Compare against a systemd template's base name, not its instance
// (getty@tty1 -> getty), so a template whose binary is named after the
// template (agetty) doesn't read as a mismatch.
if at := strings.IndexByte(svcCore, '@'); at >= 0 {
svcCore = svcCore[:at]
}
svcCore = strings.ToLower(svcCore)
cmdBase := strings.ToLower(last.Command)
if !strings.Contains(svcCore, cmdBase) && !strings.Contains(cmdBase, svcCore) {
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."
}
}