Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/agentsight/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ fn main() {

// Generate tcpsniff skeleton (no header — reuses sslsniff.h event format)
generate_skeleton(&mut out, "tcpsniff");


// Generate lsmaudit skeleton and bindings
generate_skeleton(&mut out, "lsmaudit");
generate_header(&mut out, "lsmaudit");

// generate_header(&mut out, "frametypes");
// generate_header(&mut out, "errors");
// generate_header(&mut out, "stackdeltatypes");
Expand Down
6 changes: 6 additions & 0 deletions src/agentsight/src/bin/cli/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ pub struct TraceCommand {
#[structopt(long)]
pub enable_filewatch: bool,

/// Enable LSM security-audit probe (observe-only: records outbound
/// connections and file opens for traced agents; requires BPF LSM active)
#[structopt(long)]
pub enable_lsm_audit: bool,

/// Path to JSON configuration file
#[structopt(short, long, default_value = "/etc/agentsight/config.json")]
pub config: String,
Expand Down Expand Up @@ -72,6 +77,7 @@ impl TraceCommand {
let config = AgentsightConfig::new()
.set_verbose(self.verbose)
.set_enable_filewatch(self.enable_filewatch)
.set_enable_lsm_audit(self.enable_lsm_audit)
.set_config_path(std::path::PathBuf::from(&self.config));

// Create AgentSight (auto-attaches probes and starts polling)
Expand Down
1 change: 1 addition & 0 deletions src/agentsight/src/bpf/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ typedef enum {
EVENT_SOURCE_FILEWATCH = 4, // File watch events (filewatch)
EVENT_SOURCE_FILEWRITE = 5, // File write events (filewrite)
EVENT_SOURCE_UDPDNS = 6, // UDP DNS query events (udpdns)
EVENT_SOURCE_LSM = 8, // LSM security-audit events (lsmaudit)
} event_source_t;

// Common event header - every ringbuffer event MUST start with this
Expand Down
163 changes: 163 additions & 0 deletions src/agentsight/src/bpf/lsmaudit.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
// Copyright (c) 2025 AgentSight Project
//
// LSM audit BPF program — observe-only security auditing for traced Agent
// families. It attaches to BPF LSM hooks but NEVER changes the verdict: every
// program returns the incoming `ret` unchanged, so no operation is ever denied.
// (The LSM attach point keeps the door open to future enforcement.)
//
// - lsm/socket_connect: records each outbound connection (dst IP:port).
// This is the signal the other probes miss — sslsniff/tcpsniff/udpdns only
// see TLS / specific ports / DNS, whereas this catches every connect()
// regardless of protocol or port.
// - lsm/file_open: records each file opened (basename + open flags), bounded
// by a per-(pid,inode) LRU so a chatty Agent cannot flood the ring buffer.
// NOTE: bpf_d_path is allowlist-rejected for lsm/file_open (the allowlist
// keys on security_file_open, not the bpf_lsm_file_open attach point), so we
// record the basename via the proven filewrite pattern; full path is future
// work.
//
// Filtered to traced Agent families via the shared traced_processes map
// (is_pid_traced). Emits through the shared ring buffer (EVENT_SOURCE_LSM).
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "lsmaudit.h"
#include "common.h"

#ifndef AF_INET
#define AF_INET 2
#endif
#ifndef AF_INET6
#define AF_INET6 10
#endif

// Per-(pid,inode) dedup for file_open: emit once per file per process until LRU
// eviction, so repeated opens of the same file don't flood the ring buffer.
struct file_open_key {
u64 pid;
u64 ino;
};

struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 8192);
__type(key, struct file_open_key);
__type(value, u8);
} file_open_seen SEC(".maps");

// lsm/socket_connect — one event per outbound connection attempt.
SEC("lsm/socket_connect")
int BPF_PROG(audit_socket_connect, struct socket *sock, struct sockaddr *address,
int addrlen, int ret)
{
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 ns_pid = is_pid_traced(pid);
if (!ns_pid)
return ret; // observe-only: propagate prior verdict unchanged

u16 family = BPF_CORE_READ(address, sa_family);
if (family != AF_INET && family != AF_INET6)
return ret; // skip AF_UNIX and friends

// LSM hook fires before the protocol-level length check, so the sockaddr
// fields beyond what addrlen covers may be uninitialized stack residue.
// Drop the event rather than read garbage as the destination.
if (family == AF_INET && addrlen < (int)sizeof(struct sockaddr_in))
return ret;
if (family == AF_INET6 && addrlen < (int)sizeof(struct sockaddr_in6))
return ret;

struct lsm_audit_event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return ret;

// Zero the whole record so any unwritten byte (especially path[]'s tail
// after the null terminator) cannot leak previous ringbuf contents — the
// ringbuf is shared with sslsniff/filewrite, so leftover bytes can be
// real TLS plaintext or file content.
__builtin_memset(e, 0, sizeof(*e));

e->source = EVENT_SOURCE_LSM;
e->kind = LSM_EVENT_CONNECT;
e->pid = ns_pid;
e->tid = (u32)pid_tgid;
e->uid = bpf_get_current_uid_gid();
e->timestamp_ns = bpf_ktime_get_ns();
e->family = (u8)family;
bpf_get_current_comm(&e->comm, sizeof(e->comm));

if (family == AF_INET) {
struct sockaddr_in *in4 = (struct sockaddr_in *)address;
e->dport = BPF_CORE_READ(in4, sin_port);
u32 a = BPF_CORE_READ(in4, sin_addr.s_addr);
__builtin_memcpy(e->daddr, &a, sizeof(a));
} else {
struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)address;
e->dport = BPF_CORE_READ(in6, sin6_port);
BPF_CORE_READ_INTO(&e->daddr, in6, sin6_addr);
}

bpf_ringbuf_submit(e, 0);
return ret;
}

// lsm/file_open — one event per (pid,inode), deduped via LRU.
SEC("lsm/file_open")
int BPF_PROG(audit_file_open, struct file *file, int ret)
{
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 ns_pid = is_pid_traced(pid);
if (!ns_pid)
return ret; // observe-only: propagate prior verdict unchanged

struct file_open_key key = {
.pid = ns_pid,
.ino = BPF_CORE_READ(file, f_inode, i_ino),
};
if (bpf_map_lookup_elem(&file_open_seen, &key))
return ret; // already recorded this file for this process

// basename from file->f_path.dentry->d_name.name (proven filewrite pattern)
const unsigned char *name = BPF_CORE_READ(file, f_path.dentry, d_name.name);
if (!name)
return ret;

struct lsm_audit_event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return ret;

// Zero the whole record — see audit_socket_connect for rationale (path[]'s
// tail after the null terminator must not leak ringbuf residue).
__builtin_memset(e, 0, sizeof(*e));

e->source = EVENT_SOURCE_LSM;
e->kind = LSM_EVENT_FILE_OPEN;
e->pid = ns_pid;
e->tid = (u32)pid_tgid;
e->uid = bpf_get_current_uid_gid();
e->timestamp_ns = bpf_ktime_get_ns();
e->open_flags = BPF_CORE_READ(file, f_flags);
bpf_get_current_comm(&e->comm, sizeof(e->comm));

int n = bpf_probe_read_kernel_str(e->path, sizeof(e->path), name);
if (n <= 0) {
// Path read failed — discard the slot and DO NOT mark the dedup so a
// future open of the same file still gets a chance to be recorded.
bpf_ringbuf_discard(e, 0);
return ret;
}

bpf_ringbuf_submit(e, 0);

// Mark dedup AFTER successful submit so reserve / read failures above do
// not permanently suppress the file for this pid until LRU eviction.
u8 one = 1;
bpf_map_update_elem(&file_open_seen, &key, &one, BPF_ANY);
return ret;
}

char LICENSE[] SEC("license") = "GPL";
47 changes: 47 additions & 0 deletions src/agentsight/src/bpf/lsmaudit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
// Copyright (c) 2025 AgentSight Project
//
// LSM audit BPF program header
// Observe-only security auditing of traced Agent families via BPF LSM hooks:
// - socket_connect: every outbound connection attempt (dst IP:port)
// - file_open: every file opened (basename + open flags)
#ifndef __LSMAUDIT_H
#define __LSMAUDIT_H

#define LSM_COMM_LEN 16
#define LSM_PATH_LEN 256

typedef signed char s8;
typedef unsigned char u8;
typedef signed short s16;
typedef unsigned short u16;
typedef signed int s32;
typedef unsigned int u32;
typedef signed long long s64;
typedef unsigned long long u64;

enum lsm_event_kind {
LSM_EVENT_CONNECT = 1,
LSM_EVENT_FILE_OPEN = 2,
};

// A single fixed-size record covers both hooks; `kind` selects which fields are
// meaningful. Unused fields are zeroed by the producer.
// NB: named lsm_audit_event (not lsm_event) — the kernel's vmlinux.h already
// declares `enum lsm_event`, which would collide.
struct lsm_audit_event {
u32 source; // EVENT_SOURCE_LSM
u32 pid; // namespace PID of the traced Agent (is_pid_traced result)
u32 tid; // thread id
u32 uid;
u64 timestamp_ns;
u8 kind; // enum lsm_event_kind
u8 family; // connect: AF_INET(2) / AF_INET6(10); file_open: 0
u16 dport; // connect: destination port, network byte order
s32 open_flags; // file_open: file->f_flags; connect: 0
u8 daddr[16]; // connect: IPv4 in [0..4] / IPv6 in [0..16]; file_open: 0
char comm[LSM_COMM_LEN];
char path[LSM_PATH_LEN]; // file_open: file basename; connect: empty
};

#endif /* __LSMAUDIT_H */
10 changes: 10 additions & 0 deletions src/agentsight/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,9 @@ pub struct AgentsightConfig {
pub poll_timeout_ms: u64,
/// Enable file watch probe (monitors .jsonl file opens from traced processes)
pub enable_filewatch: bool,
/// Enable LSM security-audit probe (observe-only socket_connect + file_open).
/// Requires BPF LSM to be active (`bpf` in /sys/kernel/security/lsm).
pub enable_lsm_audit: bool,
/// TCP capture targets for plain HTTP capture (empty = disabled).
/// Each entry specifies destination IP, port, or both.
pub tcp_targets: Vec<TcpTarget>,
Expand Down Expand Up @@ -490,6 +493,7 @@ impl Default for AgentsightConfig {
target_uid: None,
poll_timeout_ms: DEFAULT_POLL_TIMEOUT_MS,
enable_filewatch: false,
enable_lsm_audit: false,
tcp_targets: Vec::new(),

// HTTP/Aggregation defaults
Expand Down Expand Up @@ -580,6 +584,12 @@ impl AgentsightConfig {
self
}

/// Set enable_lsm_audit
pub fn set_enable_lsm_audit(mut self, enable: bool) -> Self {
self.enable_lsm_audit = enable;
self
}

/// Set connection capacity
pub fn set_connection_capacity(mut self, capacity: usize) -> Self {
self.connection_capacity = capacity;
Expand Down
43 changes: 43 additions & 0 deletions src/agentsight/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::probes::procmon::Event as ProcMonEvent;
use crate::probes::filewatch::FileWatchEvent;
use crate::probes::filewrite::FileWriteEvent;
use crate::probes::udpdns::UdpDnsEvent;
use crate::probes::lsmaudit::LsmEvent;

/// Unified event type that can represent any probe event
///
Expand All @@ -16,6 +17,7 @@ pub enum Event {
FileWatch(FileWatchEvent),
FileWrite(FileWriteEvent),
UdpDns(UdpDnsEvent),
Lsm(LsmEvent),
}

impl Event {
Expand All @@ -28,6 +30,7 @@ impl Event {
Event::FileWatch(_) => "FileWatch",
Event::FileWrite(_) => "FileWrite",
Event::UdpDns(_) => "UdpDns",
Event::Lsm(_) => "Lsm",
}
}
}
Expand Down Expand Up @@ -110,6 +113,19 @@ impl Event {
_ => None,
}
}

/// Check if this is an LSM audit event
pub fn is_lsm(&self) -> bool {
matches!(self, Event::Lsm(_))
}

/// Get LSM audit event if this is one
pub fn as_lsm(&self) -> Option<&LsmEvent> {
match self {
Event::Lsm(e) => Some(e),
_ => None,
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -235,4 +251,31 @@ mod tests {
assert!(e.as_filewatch().is_none());
assert!(e.as_filewrite().is_none());
}

fn make_lsm_event() -> LsmEvent {
LsmEvent::FileOpen(crate::probes::lsmaudit::LsmFileOpen {
pid: 4242,
tid: 4242,
uid: 0,
timestamp_ns: 400,
comm: "agent".to_string(),
path: "shadow".to_string(),
open_flags: 0,
})
}

#[test]
fn test_event_type_lsm() {
let e = Event::Lsm(make_lsm_event());
assert_eq!(e.event_type(), "Lsm");
}

#[test]
fn test_is_and_as_lsm() {
let e = Event::Lsm(make_lsm_event());
assert!(e.is_lsm());
assert!(!e.is_ssl());
assert!(e.as_lsm().is_some());
assert!(e.as_ssl().is_none());
}
}
1 change: 1 addition & 0 deletions src/agentsight/src/parser/unified.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ impl Parser {
Event::FileWatch(_) => ParseResult { messages: Vec::new() },
Event::FileWrite(_) => ParseResult { messages: Vec::new() },
Event::UdpDns(_) => ParseResult { messages: Vec::new() },
Event::Lsm(_) => ParseResult { messages: Vec::new() },
}
}

Expand Down
Loading
Loading