bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
//go:build linux
package main
// The three things the noise filter needs from the platform: how old a process is,
// which processes hold a listening socket, and what the package database says
// about an executable. Linux can answer all three; delta_other.go answers none
// of them, and deltanoise.go treats "cannot answer" as "report it".
import (
"crypto/md5"
"encoding/hex"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// deltaClockTicks is the kernel's USER_HZ, which /proc/<pid>/stat's start time
// is counted in. Go exposes no sysconf(_SC_CLK_TCK), and the value has been 100
// on every mainstream Linux architecture since the 2.6 era — it is part of the
// /proc ABI rather than a tunable, because changing it would reinterpret every
// field userspace already reads.
//
// A wrong constant here would only ever mis-time the short-lived rule, which
// holds a finding for one pass. It cannot hide one.
const deltaClockTicks = 100
// deltaAgeReader reads the boot clock ONCE and returns the age function for
// this pass. Per-pid it is then a single small read of /proc/<pid>/stat.
func deltaAgeReader() func(int) (time.Duration, bool) {
raw, err := os.ReadFile("/proc/uptime")
if err != nil {
return func(int) (time.Duration, bool) { return 0, false }
}
f := strings.Fields(string(raw))
if len(f) == 0 {
return func(int) (time.Duration, bool) { return 0, false }
}
up, err := strconv.ParseFloat(f[0], 64)
if err != nil || up <= 0 {
return func(int) (time.Duration, bool) { return 0, false }
}
return func(pid int) (time.Duration, bool) {
stat, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
if err != nil {
// GONE IS YOUNG, and the two used to be one answer. The filter runs
// 50-100 ms after the walk — a parent map, the package database and
// two `ss` execs later — so a process that exited in between is
// unreadable here for the most ordinary reason there is. Treating
// that as "cannot tell" reported the shortest-lived processes on the
// box at warn instead of counting them, which is the exact
// population the short-lived rule exists to keep out of the list.
//
// Anything else — a permission error, a /proc that cannot be read —
// stays "cannot tell", which the caller treats as report-it.
if os.IsNotExist(err) {
return 0, true
}
return 0, false
}
ticks, ok := parseProcStatStartTicks(string(stat))
if !ok {
return 0, false
}
started := float64(ticks) / deltaClockTicks
age := up - started
if age < 0 {
// The process started after the uptime read — this pass raced it.
// Zero, not an error: it IS that young.
age = 0
}
return time.Duration(age * float64(time.Second)), true
}
}
// deltaListeningPIDs is the set of processes holding a listening socket, read
// through the observe plane's own socket reader so this and `observe:sockets`
// can never disagree about what is listening.
// deltaSockets is readSockets as a var so a test can hand this a partial table
// — the state a live `ss` timeout produces and nothing else can reproduce.
var deltaSockets = readSockets
func deltaListeningPIDs() (map[int]bool, bool) {
r := deltaSockets()
// PARTIAL IS UNREADABLE, by this file's own rule. readSockets returns
// Supported with HALF a table when `ss` runs out of budget
// (observe_ops_linux.go:160-168), and a half table would let the short-lived
// rule check its listener exemption against a set that may simply not
// contain the socket. An exemption that cannot be checked is not an
// exemption.
// A FAILED RUN JOINS THE PARTIAL ONE, for the same reason: `ss` exiting
// non-zero yields an empty table that would silently answer "nothing is
// listening".
if !r.Supported || r.Partial != "" || r.Failed != "" {
return nil, false
}
out := map[int]bool{}
for _, s := range r.Sockets {
if s.PID > 0 {
out[s.PID] = true
}
}
return out, true
}
// ── the package database ────────────────────────────────────────────────────
const dpkgInfoDir = "/var/lib/dpkg/info"
// deltaPackagedLookup answers, for the executables asked about, which ones dpkg
// owns and still matches byte for byte.
//
// IT IS AN MD5 COMPARISON BECAUSE THAT IS WHAT DPKG RECORDS. `dpkg -V` verifies
// against these same .md5sums files; there is no stronger digest in the
// database to check against. So a chosen-prefix collision against a
// distribution binary would earn a `known` tier here — and that is the whole
// prize, because this sets a TIER and never removes a finding: the level table drops a
// known-tier proc_new from warn to notice, and a process with a model endpoint
// or an agent signature stays high whatever the package database says
// (deltaLevel). The bound on being wrong is one level, not silence.
//
// Returns nil on a box with no dpkg database, which leaves every candidate at
// its undemoted level.
func deltaPackagedLookup(paths []string) map[string]string {
files, err := filepath.Glob(filepath.Join(dpkgInfoDir, "*.md5sums"))
if err != nil || len(files) == 0 {
return nil
}
// The wanted paths, in dpkg's spelling (relative to /, no leading slash),
// mapped back to what the caller asked about.
want := map[string]string{}
for _, p := range paths {
for _, k := range dpkgPathForms(p) {
want[k] = p
}
}
type hit struct{ pkg, sum string }
found := map[string]hit{}
for _, f := range files {
b, err := os.ReadFile(f)
if err != nil {
continue
}
pkg := strings.TrimSuffix(filepath.Base(f), ".md5sums")
for _, line := range strings.Split(string(b), "\n") {
// "<32 hex> <path>" — two spaces, and the path may contain them.
if len(line) < 34 {
continue
}
sum, rest := line[:32], line[32:]
rest = strings.TrimLeft(rest, " ")
orig, ok := want[rest]
if !ok {
continue
}
if _, already := found[orig]; !already {
found[orig] = hit{pkg: pkg, sum: strings.ToLower(sum)}
}
}
}
if len(found) == 0 {
return nil
}
out := map[string]string{}
for path, h := range found {
if fileMD5(path) == h.sum {
out[path] = h.pkg
}
}
return out
}
// dpkgPathForms is the path as dpkg might have recorded it. THE /usr MERGE IS
// WHY THIS IS A LIST: the kernel reports /usr/bin/ls while a package built
// before the merge records bin/ls, and matching only one spelling would leave
// half a distribution's binaries reading as unowned — a filter that quietly
// stops filtering.
func dpkgPathForms(p string) []string {
p = strings.TrimPrefix(p, "/")
if p == "" {
return nil
}
out := []string{p}
switch {
case strings.HasPrefix(p, "usr/"):
out = append(out, strings.TrimPrefix(p, "usr/"))
case strings.HasPrefix(p, "bin/"), strings.HasPrefix(p, "sbin/"), strings.HasPrefix(p, "lib/"):
out = append(out, "usr/"+p)
}
return out
}
func fileMD5(path string) string {
f, err := os.Open(path)
if err != nil {
return ""
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return ""
}
return hex.EncodeToString(h.Sum(nil))
}
// deltaPackagedFn is nil where there is no package database to ask, which is
// what turns the rule off rather than having it answer "not packaged" about
// every file on a box that simply uses a different packager.
func deltaPackagedFn() func([]string) map[string]string {
if fi, err := os.Stat(dpkgInfoDir); err != nil || !fi.IsDir() {
return nil
}
return deltaPackagedLookup
}