enodia-sentinal/docs/PACKAGING.md
Luna 1d1dee7f6e
feat(packaging): add desktop launcher entries for both GUI dashboards
The tray applet shipped with a .desktop file but the GUI dashboards had
none, so desktop users had no menu entry for them.

Unlike the tray entry, these carry no autostart key: a dashboard window
should open when the operator asks for it, not at login. Tests assert the
Exec names match the pyproject console scripts and that neither entry can
regain an autostart key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JX86xeoBJVBb16qHkDf53K
2026-07-22 04:35:37 -07:00

283 lines
11 KiB
Markdown

# Enodia Sentinel Packaging
Applies to package version: `0.7.0`
Docs version: `0.7`
Schema generation: `v1`
Sentinel is a stdlib-only Python package installed as a plain source directory
plus launcher wrapper. The service and package are designed around Linux hosts
with systemd.
## Arch Linux
Arch is the primary packaged target.
Build from the repository root:
```bash
packaging/build-package.sh
```
Install the generated package:
```bash
sudo pacman -U enodia-sentinel-*.pkg.tar.zst
sudo systemctl daemon-reload
sudo systemctl enable --now enodia-sentinel.service
```
The Arch package installs:
| Path | Purpose |
|---|---|
| `/usr/bin/enodia-sentinel` | Launcher wrapper for the Python package. |
| `/usr/bin/sentinel-redteam` | Safe local red-team harness. |
| `/usr/lib/enodia-sentinel/enodia_sentinel/` | Stdlib-only Python package. |
| `/etc/enodia-sentinel.toml` | Operator configuration, preserved as a pacman backup file. |
| `/usr/lib/systemd/system/enodia-sentinel.service` | Hardened daemon unit. |
| `/usr/lib/systemd/system/enodia-sentinel-web.service` | Read-only dashboard unit. |
| `/usr/share/libalpm/hooks/enodia-sentinel-fim.hook` | Post-transaction FIM baseline refresh hook. |
| `/usr/lib/tmpfiles.d/enodia-sentinel.conf` | Runtime log directory definition. |
| `/usr/share/doc/enodia-sentinel/` | Versioned operator docs and examples. |
| `/var/log/enodia-sentinel/` | Alert snapshots, events, incidents, response plans, and audit logs. |
The pacman hook calls `/usr/bin/enodia-sentinel fim-update` after package
transactions so legitimate package changes refresh the FIM baseline. It does
not silence unexplained non-package drift.
Optional Arch packages:
- `python-bpfcc` for eBPF exec/syscall telemetry.
- `bpftrace` for short-lived exec capture inside forensic snapshots.
- `libnotify` for local desktop notifications.
- `libarchive` for signed-cache-package `.MTREE` verification helpers.
The default service is hardened for polling mode. To enable eBPF, copy the
packaged drop-in example and reload systemd:
```bash
sudo install -Dm644 /usr/share/doc/enodia-sentinel/examples/enodia-sentinel-ebpf.conf \
/etc/systemd/system/enodia-sentinel.service.d/ebpf.conf
sudo systemctl daemon-reload
sudo systemctl restart enodia-sentinel.service
```
## Go Migration Sidecar (opt-in)
The Go agent has a separate validation service and is never installed by the
default `make install` target or the Arch package. Build and install it
explicitly from a checkout with Go 1.25 or newer:
```bash
make build-go
sudo make install-go PREFIX=/usr/local
sudo make enable-go
```
The static `enodia-sentinel-go` binary emits `enodia.event.v1` JSONL to the
system journal. Its service owns `/var/lib/enodia-sentinel-go` for baseline
state and a bounded retained event stream. `events.jsonl` rotates to one
`events.jsonl.1` segment before crossing 64 MiB; both are private to root, and
non-status records are synchronized before emission succeeds. The service
requests the embedded native exec and syscall probes and fails open to polling
when the kernel refuses a probe. It does not write the production Python
daemon's `/var/log/enodia-sentinel` state or replace
`enodia-sentinel.service`. Systemd readiness is emitted only after baseline
initialization, event-log and snapshot-store preflight, and both requested probe
load attempts complete.
Alert events are additionally retained as private
`alert-YYYYMMDD-HHMMSS.{json,log}` pairs in `/var/lib/enodia-sentinel-go`.
Same-second events are merged into one snapshot, required
`enodia.alert.snapshot.v1` fields are preserved, and the shared
`max_snapshots` / `max_snapshot_age_days` settings bound retention. The current
sidecar also writes a private atomic `incidents.json` with the required
`enodia.incident.v1` fields, process-lineage/time-window grouping, and additive
correlation evidence. Hot-path Go enrichment is bounded and no-I/O: process and
parent context, lineage, remote classification, and candidate paths come from
already-captured records. A bounded asynchronous worker subsequently hashes
regular executables no larger than 8 MiB and records file metadata; a full queue
skips those optional fields without delaying an alert. Python remains
authoritative for package ownership, richer integrity metadata, notifications,
and management consumers.
Each snapshot text/JSON revision is SHA-256 hashed into a synchronized private
`hash-chain.jsonl` using the stable `enodia.hash_chain.v1` record schema.
Each successful sweep atomically refreshes
`/var/lib/enodia-sentinel-go/heartbeat` as a Unix timestamp with mode `0600`;
the retained value becomes stale when the sidecar stops or wedges.
`enodia-sentinel-go --health` reports that state as JSON and exits nonzero when
it is unavailable or older than the configured `heartbeat_max_age`.
Inspect the retained event stream without depending on journald:
```bash
sudo tail -n 50 /var/lib/enodia-sentinel-go/events.jsonl
sudo enodia-sentinel-go --events-tail 50
```
The native reader includes `events.jsonl.1`, preserves chronological order, and
fails nonzero if either retained segment contains malformed JSON.
Inspect the sidecar's isolated incident records and timeline without starting
the service or changing Python state:
```bash
sudo enodia-sentinel-go --incidents-list
sudo enodia-sentinel-go --incident-show <incident-id>
```
Inspect or remove the validation service with:
```bash
sudo make status-go
sudo make disable-go
sudo make uninstall-go
```
Uninstalling preserves the shared configuration and sidecar state for forensic
review or a later reinstall.
## Debian and Ubuntu
There is no native `.deb` package yet. Use the source install path:
```bash
sudo apt install python3 iproute2 procps
sudo make install PREFIX=/usr/local
sudo systemctl daemon-reload
sudo systemctl enable --now enodia-sentinel.service
```
Notes:
- The installed launcher does not require pip or a virtualenv.
- `fim-check --packages` uses the available package-manager backend; Debian
package verification support should be validated for the target host before
relying on it operationally.
- The source install path includes the pacman hook file for parity with Arch
source installs. On non-Arch systems it is inert unless pacman/libalpm is
installed; Debian installs should manage FIM baseline refresh through
operator procedure or a future dpkg trigger package.
## RPM-family Linux
There is no native RPM package yet. Use the same source install path:
```bash
sudo dnf install python3 iproute procps-ng
sudo make install PREFIX=/usr/local
sudo systemctl daemon-reload
sudo systemctl enable --now enodia-sentinel.service
```
Notes:
- Native RPM packaging should eventually install systemd units under the distro
unit path, preserve `/etc/enodia-sentinel.toml`, and create a package-manager
trigger equivalent to the Arch FIM refresh hook.
- Validate package verification behavior on the target distro before enabling
signed-package mismatch alerting.
## Desktop Tray Applet (optional)
The `[tray]` optional extra (`pip install 'enodia-sentinel[tray]'`) pulls in
`pystray` and `Pillow` and provides the `enodia-sentinel-tray` console script.
The extra is never required by the daemon; core runtime dependencies stay
empty.
`packaging/enodia-sentinel-tray.desktop` is an opt-in autostart entry. It is
not installed to a system autostart path by default — desktop users copy it to
`~/.config/autostart/` themselves.
## Desktop GUI Frontends (optional)
Two windowed dashboards ship alongside the tray applet. Both are optional
frontends over the same local state the TUI and web console read; neither is
required by the daemon, and core runtime dependencies stay empty.
| Frontend | Command | Console script | Requirement |
| --- | --- | --- | --- |
| tkinter | `enodia-sentinel gui` | `enodia-sentinel-gui` | stdlib `tkinter` (distro `tk` / `python-tk` / `python-tkinter`) |
| Qt6 | `enodia-sentinel gui-qt` | `enodia-sentinel-gui-qt` | `[qt]` extra (`PySide6`) |
Notes:
- `tkinter` is stdlib but is packaged separately on most distros, so it is an
`optdepends` (Arch `tk`), never a hard dependency.
- The `[qt]` extra (`pip install 'enodia-sentinel[qt]'`) pulls in `PySide6`
only for the Qt frontend; the Arch package lists `pyside6` as an optdepend.
- Both GUIs are read-only over detection state and display response plans for
review only — packaging must not grant them privileges beyond what the
operator's own account has. Daemon start/stop/restart goes through
`systemctl` and prompts for polkit authorization as usual.
- Only `gui/app.py` (tkinter) and `gui/qt_app.py` (Qt) import GUI libraries;
`gui/model.py` stays import-clean so headless builders can run the test
suite without a display server or `PySide6` installed.
`packaging/enodia-sentinel-gui.desktop` and
`packaging/enodia-sentinel-gui-qt.desktop` are application-menu launchers.
Unlike `enodia-sentinel-tray.desktop`, they carry no autostart key — a
dashboard window should open when the operator asks for it, not at login.
Desktop packagers may install them to `/usr/share/applications/`; source
installs can copy them to `~/.local/share/applications/`.
## Terminal TUI and Completion
The terminal dashboard is stdlib-only and available through
`enodia-sentinel tui` in source/wrapper installs. Python packaging metadata also
declares `enodia-sentinel-tui = enodia_sentinel.tui:main` for pip-style script
generation.
Shell completion has no runtime dependency: operators generate scripts with
`enodia-sentinel completion bash` or `enodia-sentinel completion zsh` and install
them in their normal shell completion path.
## Package Hardening Checklist
- Package version matches `pyproject.toml` and `enodia_sentinel.__version__`.
- Config is preserved across upgrades.
- Log directory is created with mode `0750`.
- Service units remain hardened and read-mostly by default.
- eBPF privilege relaxation is shipped only as an opt-in drop-in example.
- Package-manager hooks use absolute paths and do not depend on ambient `PATH`.
- Versioned operator docs are installed with the package.
## Release Artifacts
Release artifacts are built with:
```bash
make release-artifacts
```
The release target writes to `dist/` by default:
| Artifact | Purpose |
|---|---|
| `enodia-sentinel-<version>.tar.gz` | Versioned source release. |
| `SHA256SUMS` | SHA-256 checksums for the source tarball and release manifest. |
| `RELEASE_MANIFEST.json` | Project/version/git metadata for the generated release. |
| `SHA256SUMS.asc` | Optional detached GPG signature over `SHA256SUMS`. |
The script refuses a dirty git worktree by default so a release maps to a known
commit. For local packaging smoke tests only, set `RELEASE_ALLOW_DIRTY=1`.
To sign checksum artifacts:
```bash
RELEASE_SIGN=1 make release-artifacts
```
To select a specific GPG key:
```bash
RELEASE_SIGN=1 RELEASE_SIGNING_KEY=<key-id-or-fingerprint> make release-artifacts
```
Verification flow for operators:
```bash
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum -c SHA256SUMS
```