Skip to content
Open
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
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "stackdog"
version = "0.2.0"
version = "0.2.1"
authors = ["Vasili Pascal <info@try.direct>"]
edition = "2021"
description = "Security platform for Docker containers and Linux servers"
Expand Down Expand Up @@ -55,6 +55,7 @@ zstd = "0.13"

# Stream utilities
futures-util = "0.3"
lettre = { version = "0.11", default-features = false, features = ["tokio1", "tokio1-rustls-tls", "builder", "smtp-transport"] }

# eBPF (Linux only)
[target.'cfg(target_os = "linux")'.dependencies]
Expand All @@ -78,6 +79,8 @@ ebpf = []
# Testing
tokio-test = "0.4"
tempfile = "3"
actix-test = "0.1"
awc = "3"

# Benchmarking
criterion = { version = "0.5", features = ["html_reports"] }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Stackdog Security

![Version](https://img.shields.io/badge/version-0.2.0-blue.svg)
![Version](https://img.shields.io/badge/version-0.2.1-blue.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)
![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg)
![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey.svg)
Expand Down
2 changes: 1 addition & 1 deletion VERSION.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.0
0.2.1
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ services:
echo "Starting Stackdog..."
cargo run --bin stackdog
ports:
- "${APP_PORT:-8080}:${APP_PORT:-8080}"
- "${APP_PORT:-5000}:${APP_PORT:-5000}"
env_file:
- .env
environment:
Expand Down
4 changes: 3 additions & 1 deletion ebpf/.cargo/config → ebpf/.cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
target = ["bpfel-unknown-none"]

[target.bpfel-unknown-none]
rustflags = ["-C", "link-arg=--Bstatic"]

[unstable]
build-std = ["core"]
3 changes: 3 additions & 0 deletions ebpf/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["rust-src"]
9 changes: 7 additions & 2 deletions ebpf/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@
#![no_main]
#![no_std]

#[no_mangle]
pub fn main() {}
mod maps;
mod syscalls;

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {
loop {}
}
125 changes: 120 additions & 5 deletions ebpf/src/maps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,123 @@
//!
//! Shared maps for eBPF programs

// TODO: Implement eBPF maps in TASK-003
// This will include:
// - Event ring buffer for sending events to userspace
// - Hash maps for tracking state
// - Arrays for configuration
use aya_ebpf::{macros::map, maps::RingBuf};

#[repr(C)]
#[derive(Clone, Copy)]
pub union EbpfEventData {
pub execve: ExecveData,
pub connect: ConnectData,
pub openat: OpenatData,
pub ptrace: PtraceData,
pub raw: [u8; 264],
}

impl EbpfEventData {
pub const fn empty() -> Self {
Self { raw: [0u8; 264] }
}
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct EbpfSyscallEvent {
pub pid: u32,
pub uid: u32,
pub syscall_id: u32,
pub _pad: u32,
pub timestamp: u64,
pub comm: [u8; 16],
pub data: EbpfEventData,
}

impl EbpfSyscallEvent {
pub const fn empty() -> Self {
Self {
pid: 0,
uid: 0,
syscall_id: 0,
_pad: 0,
timestamp: 0,
comm: [0u8; 16],
data: EbpfEventData::empty(),
}
}
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct ExecveData {
pub filename_len: u32,
pub filename: [u8; 128],
pub argc: u32,
}

impl ExecveData {
pub const fn empty() -> Self {
Self {
filename_len: 0,
filename: [0u8; 128],
argc: 0,
}
}
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct ConnectData {
pub dst_ip: [u8; 16],
pub dst_port: u16,
pub family: u16,
}

impl ConnectData {
pub const fn empty() -> Self {
Self {
dst_ip: [0u8; 16],
dst_port: 0,
family: 0,
}
}
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct OpenatData {
pub path_len: u32,
pub path: [u8; 256],
pub flags: u32,
}

impl OpenatData {
pub const fn empty() -> Self {
Self {
path_len: 0,
path: [0u8; 256],
flags: 0,
}
}
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct PtraceData {
pub target_pid: u32,
pub request: u32,
pub addr: u64,
pub data: u64,
}

impl PtraceData {
pub const fn empty() -> Self {
Self {
target_pid: 0,
request: 0,
addr: 0,
data: 0,
}
}
}

#[map(name = "EVENTS")]
pub static EVENTS: RingBuf = RingBuf::with_byte_size(256 * 1024, 0);
164 changes: 157 additions & 7 deletions ebpf/src/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,160 @@
//!
//! Tracepoints for monitoring security-relevant syscalls

// TODO: Implement eBPF syscall monitoring programs in TASK-003
// This will include:
// - execve/execveat monitoring
// - connect/accept/bind monitoring
// - open/openat monitoring
// - ptrace monitoring
// - mount/umount monitoring
use aya_ebpf::{
helpers::{
bpf_get_current_comm, bpf_probe_read_user, bpf_probe_read_user_buf,
bpf_probe_read_user_str_bytes,
},
macros::tracepoint,
programs::TracePointContext,
EbpfContext,
};

use crate::maps::{
ConnectData, EbpfEventData, EbpfSyscallEvent, ExecveData, OpenatData, PtraceData, EVENTS,
};

const SYSCALL_ARG_START: usize = 16;
const SYSCALL_ARG_SIZE: usize = 8;

const SYS_EXECVE: u32 = 59;
const SYS_CONNECT: u32 = 42;
const SYS_OPENAT: u32 = 257;
const SYS_PTRACE: u32 = 101;

const AF_INET: u16 = 2;
const AF_INET6: u16 = 10;
const MAX_ARGC_SCAN: usize = 16;

#[tracepoint(name = "sys_enter_execve", category = "syscalls")]
pub fn trace_execve(ctx: TracePointContext) -> i32 {
let _ = unsafe { try_trace_execve(&ctx) };
0
}

#[tracepoint(name = "sys_enter_connect", category = "syscalls")]
pub fn trace_connect(ctx: TracePointContext) -> i32 {
let _ = unsafe { try_trace_connect(&ctx) };
0
}

#[tracepoint(name = "sys_enter_openat", category = "syscalls")]
pub fn trace_openat(ctx: TracePointContext) -> i32 {
let _ = unsafe { try_trace_openat(&ctx) };
0
}

#[tracepoint(name = "sys_enter_ptrace", category = "syscalls")]
pub fn trace_ptrace(ctx: TracePointContext) -> i32 {
let _ = unsafe { try_trace_ptrace(&ctx) };
0
}

unsafe fn try_trace_execve(ctx: &TracePointContext) -> Result<(), i64> {
let filename_ptr = read_u64_arg(ctx, 0)? as *const u8;
let argv_ptr = read_u64_arg(ctx, 1)? as *const u64;
let mut event = base_event(ctx, SYS_EXECVE);
let mut data = ExecveData::empty();

if !filename_ptr.is_null() {
if let Ok(bytes) = bpf_probe_read_user_str_bytes(filename_ptr, &mut data.filename) {
data.filename_len = bytes.len() as u32;
}
}

data.argc = count_argv(argv_ptr).unwrap_or(0);
event.data = EbpfEventData { execve: data };
submit_event(&event)
}

unsafe fn try_trace_connect(ctx: &TracePointContext) -> Result<(), i64> {
let sockaddr_ptr = read_u64_arg(ctx, 1)? as *const u8;
if sockaddr_ptr.is_null() {
return Ok(());
}

let family = bpf_probe_read_user(sockaddr_ptr as *const u16)?;
let mut event = base_event(ctx, SYS_CONNECT);
let mut data = ConnectData::empty();
data.family = family;

if family == AF_INET {
data.dst_port = bpf_probe_read_user(sockaddr_ptr.add(2) as *const u16)?;
let mut addr = [0u8; 4];
bpf_probe_read_user_buf(sockaddr_ptr.add(4), &mut addr)?;
data.dst_ip[..4].copy_from_slice(&addr);
} else if family == AF_INET6 {
data.dst_port = bpf_probe_read_user(sockaddr_ptr.add(2) as *const u16)?;
bpf_probe_read_user_buf(sockaddr_ptr.add(8), &mut data.dst_ip)?;
}

event.data = EbpfEventData { connect: data };
submit_event(&event)
}

unsafe fn try_trace_openat(ctx: &TracePointContext) -> Result<(), i64> {
let pathname_ptr = read_u64_arg(ctx, 1)? as *const u8;
let flags = read_u64_arg(ctx, 2)? as u32;
let mut event = base_event(ctx, SYS_OPENAT);
let mut data = OpenatData::empty();
data.flags = flags;

if !pathname_ptr.is_null() {
if let Ok(bytes) = bpf_probe_read_user_str_bytes(pathname_ptr, &mut data.path) {
data.path_len = bytes.len() as u32;
}
}

event.data = EbpfEventData { openat: data };
submit_event(&event)
}

unsafe fn try_trace_ptrace(ctx: &TracePointContext) -> Result<(), i64> {
let mut event = base_event(ctx, SYS_PTRACE);
let data = PtraceData {
request: read_u64_arg(ctx, 0)? as u32,
target_pid: read_u64_arg(ctx, 1)? as u32,
addr: read_u64_arg(ctx, 2)?,
data: read_u64_arg(ctx, 3)?,
};
event.data = EbpfEventData { ptrace: data };
submit_event(&event)
}

fn base_event(ctx: &TracePointContext, syscall_id: u32) -> EbpfSyscallEvent {
let mut event = EbpfSyscallEvent::empty();
event.pid = ctx.tgid();
event.uid = ctx.uid();
event.syscall_id = syscall_id;
event.timestamp = 0;
if let Ok(comm) = bpf_get_current_comm() {
event.comm = comm;
}
event
}

fn submit_event(event: &EbpfSyscallEvent) -> Result<(), i64> {
EVENTS.output(event, 0)
}

fn read_u64_arg(ctx: &TracePointContext, index: usize) -> Result<u64, i64> {
unsafe { ctx.read_at::<u64>(SYSCALL_ARG_START + index * SYSCALL_ARG_SIZE) }
}

unsafe fn count_argv(argv_ptr: *const u64) -> Result<u32, i64> {
if argv_ptr.is_null() {
return Ok(0);
}

let mut argc = 0u32;
while argc < MAX_ARGC_SCAN as u32 {
let arg_ptr = bpf_probe_read_user(argv_ptr.add(argc as usize))?;
if arg_ptr == 0 {
break;
}
argc += 1;
}

Ok(argc)
}
3 changes: 3 additions & 0 deletions migrations/00000000000000_create_alerts/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ CREATE TABLE IF NOT EXISTS alerts (
CREATE INDEX IF NOT EXISTS idx_alerts_status ON alerts(status);
CREATE INDEX IF NOT EXISTS idx_alerts_severity ON alerts(severity);
CREATE INDEX IF NOT EXISTS idx_alerts_timestamp ON alerts(timestamp);
CREATE INDEX IF NOT EXISTS idx_alerts_container_id
ON alerts(json_extract(metadata, '$.container_id'))
WHERE json_valid(metadata);
4 changes: 4 additions & 0 deletions migrations/00000000000003_create_ip_offenses/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DROP INDEX IF EXISTS idx_ip_offenses_last_seen;
DROP INDEX IF EXISTS idx_ip_offenses_status;
DROP INDEX IF EXISTS idx_ip_offenses_ip;
DROP TABLE IF EXISTS ip_offenses;
18 changes: 18 additions & 0 deletions migrations/00000000000003_create_ip_offenses/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS ip_offenses (
id TEXT PRIMARY KEY,
ip_address TEXT NOT NULL,
source_type TEXT NOT NULL,
container_id TEXT,
offense_count INTEGER NOT NULL DEFAULT 1,
first_seen TEXT NOT NULL,
last_seen TEXT NOT NULL,
blocked_until TEXT,
status TEXT NOT NULL DEFAULT 'Active',
reason TEXT NOT NULL,
metadata TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_ip_offenses_ip ON ip_offenses(ip_address);
CREATE INDEX IF NOT EXISTS idx_ip_offenses_status ON ip_offenses(status);
CREATE INDEX IF NOT EXISTS idx_ip_offenses_last_seen ON ip_offenses(last_seen);
Loading
Loading