Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d0f9c07
Fix crash on element lookup failure
matthill Jan 28, 2026
adc4aec
Updated bpf libraries to newer
matthill Jan 28, 2026
e40dfe2
Defined missing types
matthill Jan 28, 2026
d4d07f3
Clang formatting
matthill Jan 28, 2026
d5b837c
Removed a few unused memcpy
matthill Jan 28, 2026
9d6ad12
Remove dead code
matthill Jan 28, 2026
55d5c4f
Simplify expression
matthill Jan 28, 2026
13b67a1
Simplify memcpy on rate limit
matthill Jan 28, 2026
6e01d86
Fix order of operations
matthill Jan 28, 2026
7e545a4
Fix order of operations
matthill Jan 28, 2026
fab5ca7
Dropped unused memcpy
matthill Jan 28, 2026
d4e5114
Removed unused comments
matthill Jan 28, 2026
b74c808
Add rate limit on events/second in addition to bytes/second
matthill Jan 28, 2026
7097ff3
Move filename lookup/copy to after process has qualified
matthill Jan 28, 2026
15b9628
Implemented hash map for SSHD process lookup
matthill Jan 28, 2026
a799ac2
Fixed race condition in short-lived processes while app is starting
matthill Jan 28, 2026
669659c
Use newer version of yyjson
matthill Jan 28, 2026
b759d08
Significant CPU reduction (~20x) on high volume of events
matthill Jan 28, 2026
889e721
Using newer version of plog lib
matthill Jan 28, 2026
448e7af
Add unit tests
matthill Jan 29, 2026
49e5e18
Fix file transfer logic to detect sftp-server case
matthill Jan 29, 2026
38f0f15
Update test
matthill Jan 29, 2026
bdc3367
Fix existing connection detection for newer openssh versions
matthill Jan 29, 2026
ff3071c
Add Dockerfile for deployment
matthill Jan 29, 2026
329b89e
Fix bug causing crash when /etc/passwd is not exposed
matthill Jan 29, 2026
141d5e3
Upgraded Python libs
matthill Jan 30, 2026
a9d1963
Fix CLI bugs expressed after lib upgrade
matthill Jan 30, 2026
b3fa923
Add diagnostic webserver
matthill Jan 30, 2026
813ce1c
Removed old comments
matthill Jan 30, 2026
3bfc647
Added env vars to CLI args to make docker deployment simpler
matthill Jan 30, 2026
1630606
Moved dockerfile build order to avoid recompiling on python change
matthill Jan 30, 2026
4702feb
Added session age to UI
matthill Jan 30, 2026
f576322
Fix deployment issue
matthill Jan 30, 2026
970ab1e
Add a visual indicator to show which session is being watched
matthill Jan 30, 2026
a457cd6
Added github action to build docker container
matthill Jan 30, 2026
0b990f5
Don't forget the submodules
matthill Jan 30, 2026
1664b69
Add argument to controll keystroke injection
matthill Jan 31, 2026
10f5c07
CMake tweaks
matthill Jan 31, 2026
b50eca6
Dockerfile updates
matthill Feb 1, 2026
14f0ce2
Dropped bpftool from repo. Instead using OS packaged tool
matthill Feb 1, 2026
1d004dd
Updated readme w/ docker-only deliverable
matthill Feb 1, 2026
769f3a7
Fixed unnecessary argument to libsshlog wrapper
matthill Feb 1, 2026
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
70 changes: 70 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Build and Publish Docker Image

on:
push:
branches: [ "master", "release/*" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]

env:
# Use ghcr.io for GitHub Container Registry
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

# Set up QEMU for multi-arch builds (arm64/amd64)
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

# Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# Login against the registry except on PR
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Extract metadata (tags, labels) for Docker
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=ref,event=pr
type=raw,value=latest,enable={{is_default_branch}}

# Build and push Docker image with Buildx
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "libbpf"]
path = libsshlog/libbpf
url = https://github.com/openkilt/libbpf.git
[submodule "libsshlog/bpftool"]
path = libsshlog/bpftool
url = https://github.com/libbpf/bpftool.git
102 changes: 89 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,98 @@
FROM sshlog/build:latest AS builder
# --- Stage 1: Builder ---
FROM debian:bookworm-slim AS builder

WORKDIR /build/
ENV DEBIAN_FRONTEND=noninteractive
ENV INSTALL_TARGET_DIR=/tmp/sshlog-install
SHELL ["/bin/bash", "-c"]

COPY . ./
# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
git \
clang-19 \
llvm-19 \
libelf-dev \
libbpf-dev \
pkg-config \
linux-libc-dev \
flex \
bison \
python3-docutils \
python3-virtualenv \
bpftool \
libcap-dev \
&& ln -s /usr/bin/clang-19 /usr/bin/clang \
&& ln -s /usr/bin/llvm-strip-19 /usr/bin/llvm-strip \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /source

# Copy the library source code

# Copy Source
COPY CMakeLists.txt .
COPY libsshlog/ ./libsshlog/
COPY cmake/ ./cmake/

WORKDIR /source/build

# Use RelWithDebInfo for optimized but debuggable binaries
RUN cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INSTALL_SYSCONFDIR=/etc .. \
&& make -j$(nproc)

# Install to a temporary directory
RUN make install DESTDIR=${INSTALL_TARGET_DIR}

RUN package/build_scripts/prep_repo.sh && \
cd drone_src && \
debuild -b -uc -us
# RUN mkdir -p ${INSTALL_TARGET_DIR}/usr/bin/ && \
# cp libsshlog/sshlog_cli ${INSTALL_TARGET_DIR}/usr/bin/

# Install Configs & Scripts
WORKDIR /source

# Deployable image
FROM ubuntu:20.04
# Copy the daemon source code and prep the python build env
COPY daemon/ ./daemon/
RUN rm -Rf /tmp/sshlog_venv 2>/dev/null && \
virtualenv /tmp/sshlog_venv && \
source /tmp/sshlog_venv/bin/activate && \
pip3 install -r daemon/requirements.txt

COPY --from=builder /build/*.deb ./

RUN daemon/build_binary.sh && \
mkdir -p ${INSTALL_TARGET_DIR}/usr/bin/ && cp dist/* ${INSTALL_TARGET_DIR}/usr/bin/ && \
mkdir -p ${INSTALL_TARGET_DIR}/var/log/sshlog && chmod 700 ${INSTALL_TARGET_DIR}/var/log/sshlog && \
mkdir -p ${INSTALL_TARGET_DIR}/etc/sshlog/conf.d && \
mkdir -p ${INSTALL_TARGET_DIR}/etc/sshlog/plugins && \
mkdir -p ${INSTALL_TARGET_DIR}/etc/sshlog/samples && \
cp daemon/config_samples/*.yaml ${INSTALL_TARGET_DIR}/etc/sshlog/samples/ && \
# Copy the session and event log config to the conf.d folder
cp ${INSTALL_TARGET_DIR}/etc/sshlog/samples/log_all_sessions.yaml ${INSTALL_TARGET_DIR}/etc/sshlog/conf.d && \
cp ${INSTALL_TARGET_DIR}/etc/sshlog/samples/log_events.yaml ${INSTALL_TARGET_DIR}/etc/sshlog/conf.d

# --- Stage 2: Production ---
FROM debian:bookworm-slim

ENV DEBIAN_FRONTEND=noninteractive

# Install Runtime Dependencies
# Added 'gdb' for debugging
RUN apt-get update && apt-get install -y \
libelf1 && \
dpkg -i ./*.deb && \
rm -rf /var/lib/apt/lists/*
libelf1 \
libbpf1 \
libcap2 \
ca-certificates \
gdb \
&& rm -rf /var/lib/apt/lists/*

# Copy the compiled artifacts
COPY --from=builder /tmp/sshlog-install /

# Ensure directories exist
RUN mkdir -p /var/log/sshlog /etc/sshlog

# Daemon must run as root to access the Kernel BPF subsystem
USER root

CMD ["sshlogd"]
CMD ["/usr/bin/sshlogd"]
Binary file added assets/sshlog_header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/webserver_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions daemon/build_binary.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ if [[ -f dist/sshlog && -f dist/sshlogd ]]; then
exit 0
fi

rm -Rf /tmp/sshlog_venv 2>/dev/null
#rm -Rf /tmp/sshlog_venv 2>/dev/null
virtualenv /tmp/sshlog_venv
source /tmp/sshlog_venv/bin/activate
pip3 install -r ${SCRIPT_DIR}/requirements.txt
Expand All @@ -38,4 +38,4 @@ client_imports=$(findimports --ignore-stdlib ${SCRIPT_DIR}/cli/ | grep -v 'cli\

pyinstaller --onefile $CLIENT_IMPORTS ${SCRIPT_DIR}/client.py -n sshlog

echo "Python binaries built in dist/ folder"
echo "Python binaries built in dist/ folder"
8 changes: 4 additions & 4 deletions daemon/comms/dtos.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass
import dataclasses
from dataclasses_json import dataclass_json
from typing import List
from typing import List, Dict, Any
import json
from uuid import uuid4

Expand Down Expand Up @@ -89,7 +89,7 @@ class KillSessionRequestDto:
@dataclass(frozen=True)
class EventWatchResponseDto:
event_type: str
payload_json: str
payload_json: Dict[str, Any]
payload_type: int = EVENT_WATCH_RESPONSE


Expand All @@ -107,8 +107,8 @@ class SessionDto:
pts_pid: int
shell_pid: int
tty_id: int
start_time: str
end_time: str
start_time: int
end_time: int
last_activity_time: int
last_command: str
user_id: int
Expand Down
17 changes: 10 additions & 7 deletions daemon/comms/mq_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class MQLocalServer(threading.Thread):
Acts as a server to receive requests from client process (sshlog)
responds with data that client can display to CLI
'''
def __init__(self, session_tracker: Tracker,
def __init__(self, session_tracker: Tracker, enable_injection=False,
group=None, target=None, name=None, args=(), kwargs=None):

super(MQLocalServer,self).__init__(group=group, target=target,
Expand All @@ -85,6 +85,7 @@ def __init__(self, session_tracker: Tracker,
self.response_queue = queue.Queue()
self.active_streams = ActiveStreams()
self._stay_alive = True
self.enable_injection = enable_injection



Expand Down Expand Up @@ -165,10 +166,14 @@ def _launch_task(self, request_message: RequestMessage):
logger.debug("Redrawing shell via SIGWINCH")
os.kill(session['shell_pid'], SIGWINCH)

# Write the text char-by-char to the TTY output using ioctl
with open(f'/dev/pts/{tty_id}', 'w') as tty_out:
for key in request_message.dto_payload.keys:
fcntl.ioctl(tty_out, termios.TIOCSTI, key.encode('utf-8'))
if request_message.dto_payload.keys:
if self.enable_injection:
# Write the text char-by-char to the TTY output using ioctl
with open(f'/dev/pts/{tty_id}', 'w') as tty_out:
for key in request_message.dto_payload.keys:
fcntl.ioctl(tty_out, termios.TIOCSTI, key.encode('utf-8'))
else:
logger.warning(f"Injection disabled. Ignoring request for PTM PID {ptm_pid}")



Expand All @@ -179,5 +184,3 @@ def shutdown(self):

def stay_alive(self):
return self._stay_alive


43 changes: 36 additions & 7 deletions daemon/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,43 @@
from comms.mq_base import PROC_LOCK_FILE
from comms.pidlockfile import PIDLockFile, LockTimeout, AlreadyLocked
import platform
from web_server import SSHLogWebServer

def run_main():

parser = argparse.ArgumentParser(description="SSHLog Daemon")

parser.add_argument("-l", "--logfile", default=None, help='Path to log file')

# parser.add_argument("-k", "--key", default=os.getenv('OPENREPO_APIKEY', ''), help='API key')
# parser.add_argument("-s", "--server", default=os.getenv('OPENREPO_SERVER', 'http://localhost:7376'),
# help="OpenRepo Server")
parser.add_argument("-l", "--logfile", default=os.environ.get('SSHLOG_LOGFILE', None), help='Path to log file')

parser.add_argument(
'--debug',
action='store_true',
default=os.environ.get('SSHLOG_DEBUG', '').lower() in ('true', '1', 'yes'),
help='Print debug info'
)
parser.add_argument(
'--enable-diagnostic-web',
action='store_true',
default=os.environ.get('SSHLOG_ENABLE_DIAGNOSTIC_WEB', '').lower() in ('true', '1', 'yes'),
help='Enable the diagnostic web interface'
)
parser.add_argument(
'--diagnostic-web-ip',
default=os.environ.get('SSHLOG_DIAGNOSTIC_WEB_IP', '127.0.0.1'),
help='Binding IP for the diagnostic web interface (default: 127.0.0.1)'
)
parser.add_argument(
'--diagnostic-web-port',
default=int(os.environ.get('SSHLOG_DIAGNOSTIC_WEB_PORT', 5000)),
type=int,
help='Port for the diagnostic web interface (default: 5000)'
)
parser.add_argument(
'--enable-session-injection',
action='store_true',
default=os.environ.get('SSHLOG_ENABLE_SESSION_INJECTION', '').lower() in ('true', '1', 'yes'),
help='Enable command injection into active sessions (default: False)'
)

args = parser.parse_args()

Expand Down Expand Up @@ -105,16 +126,25 @@ def run_main():


# Spin up local MQ server to start listening
server = MQLocalServer(session_tracker)
server = MQLocalServer(session_tracker, enable_injection=args.enable_session_injection)
server.start()

# Start the Web Server
web_server = None
if args.enable_diagnostic_web:
web_server = SSHLogWebServer(session_tracker, host=args.diagnostic_web_ip,
port=args.diagnostic_web_port, enable_session_injection=args.enable_session_injection)
web_server.start()

with SSHLog(loglevel=0) as sshb:

try:
while sshb.is_ok():
event_data = sshb.poll(timeout_ms=15)
if event_data is not None:
eventbus_sshtrace_push(event_data, session_tracker)
if web_server:
web_server.process_event(event_data)
except KeyboardInterrupt:
pass

Expand All @@ -135,4 +165,3 @@ def run_main():
print(f"Error: sshlog daemon is already running. To force process to run, delete {PROC_LOCK_FILE}")
except PermissionError:
print(f"Permission denied accessing file {PROC_LOCK_FILE}")

21 changes: 12 additions & 9 deletions daemon/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
blinker==1.5
dataclasses-json==0.5.7
datadog==0.45.0
orjson==3.8.4
prettytable==3.6.0
PyYAML==6.0
pyzmq==24.0.1
requests==2.28.2
blinker==1.7.0
dataclasses-json==0.6.4
datadog==0.49.1
orjson==3.9.15
prettytable==3.10.0
PyYAML==6.0.1
pyzmq==25.1.2
requests==2.31.0
syslog-py==0.2.5
timeago==1.0.16
pyinstaller==5.8.0 # For installer
pyinstaller==6.18.0 # For installer
findimports==2.3.0 # For installer
Flask==3.0.2
Flask-SocketIO==5.3.6
simple-websocket==1.0.0
Loading