Document packaging and release artifacts
This commit is contained in:
parent
aee984d862
commit
e0e5cd62d9
14 changed files with 470 additions and 13 deletions
|
|
@ -85,6 +85,7 @@ Start with:
|
||||||
- `docs/ROADMAP.md` for sequenced work.
|
- `docs/ROADMAP.md` for sequenced work.
|
||||||
- `docs/COMMAND_REFERENCE.md` for CLI contracts.
|
- `docs/COMMAND_REFERENCE.md` for CLI contracts.
|
||||||
- `docs/OPERATIONS.md` and `docs/RUNBOOKS.md` for operator workflows.
|
- `docs/OPERATIONS.md` and `docs/RUNBOOKS.md` for operator workflows.
|
||||||
|
- `docs/PACKAGING.md` for package layout and distro install paths.
|
||||||
- `docs/THREAT_MODEL.md` for security boundaries and non-goals.
|
- `docs/THREAT_MODEL.md` for security boundaries and non-goals.
|
||||||
|
|
||||||
## Engineering Rules
|
## Engineering Rules
|
||||||
|
|
@ -180,5 +181,8 @@ When changing behavior, update the relevant set:
|
||||||
- `docs/THREAT_MODEL.md` for trust-boundary/security-model changes.
|
- `docs/THREAT_MODEL.md` for trust-boundary/security-model changes.
|
||||||
- `docs/ROADMAP.md` for planned or completed roadmap movement.
|
- `docs/ROADMAP.md` for planned or completed roadmap movement.
|
||||||
- `docs/SCHEMAS.md` for stable JSON contracts.
|
- `docs/SCHEMAS.md` for stable JSON contracts.
|
||||||
|
- `docs/PACKAGING.md` for package/install behavior.
|
||||||
|
- `packaging/release-artifacts.sh` for release tarballs, checksums, and optional
|
||||||
|
GPG signatures.
|
||||||
- `docs/VERSION.json` and `docs/INDEX.md` for documentation versioning.
|
- `docs/VERSION.json` and `docs/INDEX.md` for documentation versioning.
|
||||||
- `config/enodia-sentinel.toml` for config knobs.
|
- `config/enodia-sentinel.toml` for config knobs.
|
||||||
|
|
|
||||||
27
Makefile
27
Makefile
|
|
@ -4,8 +4,10 @@ LIBDIR := $(PREFIX)/lib/enodia-sentinel
|
||||||
SYSTEMDDIR := /etc/systemd/system
|
SYSTEMDDIR := /etc/systemd/system
|
||||||
CONFDIR := /etc
|
CONFDIR := /etc
|
||||||
LOGDIR := /var/log/enodia-sentinel
|
LOGDIR := /var/log/enodia-sentinel
|
||||||
|
DOCDIR := $(PREFIX)/share/doc/enodia-sentinel
|
||||||
|
TMPFILESDIR := /etc/tmpfiles.d
|
||||||
|
|
||||||
.PHONY: install uninstall enable disable status logs check baseline drill test clean
|
.PHONY: install uninstall enable disable status logs check baseline drill test release-artifacts clean
|
||||||
|
|
||||||
# Installs the stdlib-only Python package as a plain directory + launcher
|
# Installs the stdlib-only Python package as a plain directory + launcher
|
||||||
# wrapper (no pip, no virtualenv, no site-packages). Zero runtime deps.
|
# wrapper (no pip, no virtualenv, no site-packages). Zero runtime deps.
|
||||||
|
|
@ -17,13 +19,27 @@ install:
|
||||||
install -Dm644 systemd/enodia-sentinel.service $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel.service
|
install -Dm644 systemd/enodia-sentinel.service $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel.service
|
||||||
install -Dm644 systemd/enodia-sentinel-web.service $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel-web.service
|
install -Dm644 systemd/enodia-sentinel-web.service $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel-web.service
|
||||||
install -Dm644 packaging/enodia-sentinel-fim.hook $(DESTDIR)/etc/pacman.d/hooks/enodia-sentinel-fim.hook
|
install -Dm644 packaging/enodia-sentinel-fim.hook $(DESTDIR)/etc/pacman.d/hooks/enodia-sentinel-fim.hook
|
||||||
|
install -Dm644 packaging/enodia-sentinel.tmpfiles $(DESTDIR)$(TMPFILESDIR)/enodia-sentinel.conf
|
||||||
@if [ ! -e "$(DESTDIR)$(CONFDIR)/enodia-sentinel.toml" ]; then \
|
@if [ ! -e "$(DESTDIR)$(CONFDIR)/enodia-sentinel.toml" ]; then \
|
||||||
install -Dm644 config/enodia-sentinel.toml $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml; \
|
install -Dm644 config/enodia-sentinel.toml $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml; \
|
||||||
echo "Installed default config at $(CONFDIR)/enodia-sentinel.toml"; \
|
echo "Installed default config at $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml"; \
|
||||||
else \
|
else \
|
||||||
install -Dm644 config/enodia-sentinel.toml $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new; \
|
install -Dm644 config/enodia-sentinel.toml $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new; \
|
||||||
echo "Existing config preserved; new template at $(CONFDIR)/enodia-sentinel.toml.new"; \
|
echo "Existing config preserved; new template at $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new"; \
|
||||||
fi
|
fi
|
||||||
|
install -Dm644 README.md $(DESTDIR)$(DOCDIR)/README.md
|
||||||
|
install -Dm644 docs/INDEX.md $(DESTDIR)$(DOCDIR)/INDEX.md
|
||||||
|
install -Dm644 docs/COMMAND_REFERENCE.md $(DESTDIR)$(DOCDIR)/COMMAND_REFERENCE.md
|
||||||
|
install -Dm644 docs/SCHEMAS.md $(DESTDIR)$(DOCDIR)/SCHEMAS.md
|
||||||
|
install -Dm644 docs/PACKAGING.md $(DESTDIR)$(DOCDIR)/PACKAGING.md
|
||||||
|
install -Dm644 docs/OPERATIONS.md $(DESTDIR)$(DOCDIR)/OPERATIONS.md
|
||||||
|
install -Dm644 docs/RUNBOOKS.md $(DESTDIR)$(DOCDIR)/RUNBOOKS.md
|
||||||
|
install -Dm644 docs/RULES.md $(DESTDIR)$(DOCDIR)/RULES.md
|
||||||
|
install -Dm644 docs/SPECIFICATION.md $(DESTDIR)$(DOCDIR)/SPECIFICATION.md
|
||||||
|
install -Dm644 docs/ROADMAP.md $(DESTDIR)$(DOCDIR)/ROADMAP.md
|
||||||
|
install -Dm644 docs/THREAT_MODEL.md $(DESTDIR)$(DOCDIR)/THREAT_MODEL.md
|
||||||
|
install -Dm644 docs/VERSION.json $(DESTDIR)$(DOCDIR)/VERSION.json
|
||||||
|
install -Dm644 systemd/enodia-sentinel-ebpf.conf $(DESTDIR)$(DOCDIR)/examples/enodia-sentinel-ebpf.conf
|
||||||
install -dm750 $(DESTDIR)$(LOGDIR)
|
install -dm750 $(DESTDIR)$(LOGDIR)
|
||||||
@echo "Installed. Run 'sudo make enable' to start the service."
|
@echo "Installed. Run 'sudo make enable' to start the service."
|
||||||
|
|
||||||
|
|
@ -34,7 +50,9 @@ uninstall:
|
||||||
rm -f $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel.service
|
rm -f $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel.service
|
||||||
rm -f $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel-web.service
|
rm -f $(DESTDIR)$(SYSTEMDDIR)/enodia-sentinel-web.service
|
||||||
rm -f $(DESTDIR)/etc/pacman.d/hooks/enodia-sentinel-fim.hook
|
rm -f $(DESTDIR)/etc/pacman.d/hooks/enodia-sentinel-fim.hook
|
||||||
|
rm -f $(DESTDIR)$(TMPFILESDIR)/enodia-sentinel.conf
|
||||||
rm -f $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new
|
rm -f $(DESTDIR)$(CONFDIR)/enodia-sentinel.toml.new
|
||||||
|
rm -rf $(DESTDIR)$(DOCDIR)
|
||||||
@echo "Uninstalled. Config and logs preserved."
|
@echo "Uninstalled. Config and logs preserved."
|
||||||
|
|
||||||
enable:
|
enable:
|
||||||
|
|
@ -66,5 +84,8 @@ web:
|
||||||
drill:
|
drill:
|
||||||
./src/sentinel-redteam
|
./src/sentinel-redteam
|
||||||
|
|
||||||
|
release-artifacts:
|
||||||
|
packaging/release-artifacts.sh
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ Project docs:
|
||||||
response, fleet, and assurance layers.
|
response, fleet, and assurance layers.
|
||||||
- [Operations guide](docs/OPERATIONS.md) — install checks, health checks, alert
|
- [Operations guide](docs/OPERATIONS.md) — install checks, health checks, alert
|
||||||
workflow, baseline hygiene, and evidence export.
|
workflow, baseline hygiene, and evidence export.
|
||||||
|
- [Packaging guide](docs/PACKAGING.md) — Arch package layout plus Debian/RPM
|
||||||
|
source-install paths.
|
||||||
- [JSON schemas](docs/SCHEMAS.md) — stable v1 contracts for alerts, incidents,
|
- [JSON schemas](docs/SCHEMAS.md) — stable v1 contracts for alerts, incidents,
|
||||||
status, response plans, and response audit records.
|
status, response plans, and response audit records.
|
||||||
- [IR runbooks](docs/RUNBOOKS.md) — confirm/preserve/contain/recover playbooks
|
- [IR runbooks](docs/RUNBOOKS.md) — confirm/preserve/contain/recover playbooks
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ same patch as any command, schema, or behavior documentation changes.
|
||||||
| [COMMAND_REFERENCE.md](COMMAND_REFERENCE.md) | Manpage-style CLI and service command contract. |
|
| [COMMAND_REFERENCE.md](COMMAND_REFERENCE.md) | Manpage-style CLI and service command contract. |
|
||||||
| [SCHEMAS.md](SCHEMAS.md) | Stable JSON schema IDs and required v1 fields. |
|
| [SCHEMAS.md](SCHEMAS.md) | Stable JSON schema IDs and required v1 fields. |
|
||||||
| [OPERATIONS.md](OPERATIONS.md) | Install checks, health checks, alert workflow, baseline hygiene, and evidence export. |
|
| [OPERATIONS.md](OPERATIONS.md) | Install checks, health checks, alert workflow, baseline hygiene, and evidence export. |
|
||||||
|
| [PACKAGING.md](PACKAGING.md) | Arch package layout and Debian/RPM source-install paths. |
|
||||||
| [RUNBOOKS.md](RUNBOOKS.md) | Incident response playbooks for common Sentinel findings. |
|
| [RUNBOOKS.md](RUNBOOKS.md) | Incident response playbooks for common Sentinel findings. |
|
||||||
| [RULES.md](RULES.md) | Generated built-in event-rule reference by SID. |
|
| [RULES.md](RULES.md) | Generated built-in event-rule reference by SID. |
|
||||||
|
|
||||||
|
|
|
||||||
153
docs/PACKAGING.md
Normal file
153
docs/PACKAGING.md
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
@ -93,8 +93,8 @@ Purpose: define a stable single-host product.
|
||||||
audits.
|
audits.
|
||||||
- ✅ Add compatibility tests for schema evolution.
|
- ✅ Add compatibility tests for schema evolution.
|
||||||
- ✅ Add documentation versioning and manpage-style command reference.
|
- ✅ Add documentation versioning and manpage-style command reference.
|
||||||
- Harden packaging for Arch first, then document Debian/RPM install paths.
|
- ✅ Harden packaging for Arch first, then document Debian/RPM install paths.
|
||||||
- Add signed release artifacts and checksums.
|
- ✅ Add signed release artifacts and checksums.
|
||||||
- Improve dashboard from alert browser to local console:
|
- Improve dashboard from alert browser to local console:
|
||||||
incidents, posture, integrity state, response plans, and watchdog status.
|
incidents, posture, integrity state, response plans, and watchdog status.
|
||||||
- ✅ Add a local dashboard Settings menu with persistent themes for dark
|
- ✅ Add a local dashboard Settings menu with persistent themes for dark
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,7 @@
|
||||||
"docs_version": "0.7",
|
"docs_version": "0.7",
|
||||||
"schema_generation": "v1",
|
"schema_generation": "v1",
|
||||||
"command_reference": "docs/COMMAND_REFERENCE.md",
|
"command_reference": "docs/COMMAND_REFERENCE.md",
|
||||||
"schema_reference": "docs/SCHEMAS.md"
|
"schema_reference": "docs/SCHEMAS.md",
|
||||||
|
"packaging_reference": "docs/PACKAGING.md",
|
||||||
|
"release_artifact_target": "make release-artifacts"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Maintainer: Enodia
|
# Maintainer: Enodia
|
||||||
pkgname=enodia-sentinel
|
pkgname=enodia-sentinel
|
||||||
pkgver=0.6.0
|
pkgver=0.7.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Linux IDS/IPS/EDR agent with integrity, anti-rootkit, forensic evidence, and explicit prevention workflows"
|
pkgdesc="Linux IDS/IPS/EDR agent with integrity, anti-rootkit, forensic evidence, and explicit prevention workflows"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|
@ -9,7 +9,8 @@ license=('GPL-3.0-or-later')
|
||||||
depends=('python>=3.11' 'iproute2' 'procps-ng')
|
depends=('python>=3.11' 'iproute2' 'procps-ng')
|
||||||
optdepends=('python-bpfcc: eBPF event-driven execve/syscall monitors (catches short-lived processes and memory telemetry)'
|
optdepends=('python-bpfcc: eBPF event-driven execve/syscall monitors (catches short-lived processes and memory telemetry)'
|
||||||
'bpftrace: execve tracing of short-lived processes in snapshots'
|
'bpftrace: execve tracing of short-lived processes in snapshots'
|
||||||
'libnotify: desktop notifications on alert')
|
'libnotify: desktop notifications on alert'
|
||||||
|
'libarchive: signed-package verification against cached package .MTREE manifests')
|
||||||
backup=('etc/enodia-sentinel.toml')
|
backup=('etc/enodia-sentinel.toml')
|
||||||
source=()
|
source=()
|
||||||
sha256sums=()
|
sha256sums=()
|
||||||
|
|
@ -24,7 +25,21 @@ package() {
|
||||||
install -Dm644 systemd/enodia-sentinel.service "$pkgdir/usr/lib/systemd/system/enodia-sentinel.service"
|
install -Dm644 systemd/enodia-sentinel.service "$pkgdir/usr/lib/systemd/system/enodia-sentinel.service"
|
||||||
install -Dm644 systemd/enodia-sentinel-web.service "$pkgdir/usr/lib/systemd/system/enodia-sentinel-web.service"
|
install -Dm644 systemd/enodia-sentinel-web.service "$pkgdir/usr/lib/systemd/system/enodia-sentinel-web.service"
|
||||||
install -Dm644 packaging/enodia-sentinel-fim.hook "$pkgdir/usr/share/libalpm/hooks/enodia-sentinel-fim.hook"
|
install -Dm644 packaging/enodia-sentinel-fim.hook "$pkgdir/usr/share/libalpm/hooks/enodia-sentinel-fim.hook"
|
||||||
|
install -Dm644 packaging/enodia-sentinel.tmpfiles "$pkgdir/usr/lib/tmpfiles.d/enodia-sentinel.conf"
|
||||||
install -Dm644 config/enodia-sentinel.toml "$pkgdir/etc/enodia-sentinel.toml"
|
install -Dm644 config/enodia-sentinel.toml "$pkgdir/etc/enodia-sentinel.toml"
|
||||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||||
|
install -Dm644 docs/INDEX.md "$pkgdir/usr/share/doc/$pkgname/INDEX.md"
|
||||||
|
install -Dm644 docs/COMMAND_REFERENCE.md "$pkgdir/usr/share/doc/$pkgname/COMMAND_REFERENCE.md"
|
||||||
|
install -Dm644 docs/SCHEMAS.md "$pkgdir/usr/share/doc/$pkgname/SCHEMAS.md"
|
||||||
|
install -Dm644 docs/PACKAGING.md "$pkgdir/usr/share/doc/$pkgname/PACKAGING.md"
|
||||||
|
install -Dm644 docs/OPERATIONS.md "$pkgdir/usr/share/doc/$pkgname/OPERATIONS.md"
|
||||||
|
install -Dm644 docs/RUNBOOKS.md "$pkgdir/usr/share/doc/$pkgname/RUNBOOKS.md"
|
||||||
|
install -Dm644 docs/RULES.md "$pkgdir/usr/share/doc/$pkgname/RULES.md"
|
||||||
|
install -Dm644 docs/SPECIFICATION.md "$pkgdir/usr/share/doc/$pkgname/SPECIFICATION.md"
|
||||||
|
install -Dm644 docs/ROADMAP.md "$pkgdir/usr/share/doc/$pkgname/ROADMAP.md"
|
||||||
|
install -Dm644 docs/THREAT_MODEL.md "$pkgdir/usr/share/doc/$pkgname/THREAT_MODEL.md"
|
||||||
|
install -Dm644 docs/VERSION.json "$pkgdir/usr/share/doc/$pkgname/VERSION.json"
|
||||||
|
install -Dm644 systemd/enodia-sentinel-ebpf.conf "$pkgdir/usr/share/doc/$pkgname/examples/enodia-sentinel-ebpf.conf"
|
||||||
install -dm750 "$pkgdir/var/log/enodia-sentinel"
|
install -dm750 "$pkgdir/var/log/enodia-sentinel"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# Build the Arch package from the repo root.
|
# Build the Arch package from the repo root.
|
||||||
set -e
|
set -eu
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
command -v makepkg >/dev/null 2>&1 || {
|
||||||
|
echo "error: makepkg not found; install pacman/base-devel tooling first" >&2
|
||||||
|
exit 127
|
||||||
|
}
|
||||||
echo "Building enodia-sentinel package..."
|
echo "Building enodia-sentinel package..."
|
||||||
makepkg -f --noconfirm
|
umask 022
|
||||||
|
makepkg -f --cleanbuild --noconfirm
|
||||||
mv -f *.pkg.tar.zst ../ 2>/dev/null || true
|
mv -f *.pkg.tar.zst ../ 2>/dev/null || true
|
||||||
echo "Done. Install with: sudo pacman -U enodia-sentinel-*.pkg.tar.zst"
|
echo "Done. Install with: sudo pacman -U enodia-sentinel-*.pkg.tar.zst"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Refresh the Enodia Sentinel file-integrity baseline after every package
|
# Refresh the Enodia Sentinel file-integrity baseline after every package
|
||||||
# transaction, so legitimate package changes never trigger a FIM alert.
|
# transaction, so legitimate package changes never trigger a FIM alert.
|
||||||
# Install as: /etc/pacman.d/hooks/enodia-sentinel-fim.hook
|
# Packaged as: /usr/share/libalpm/hooks/enodia-sentinel-fim.hook
|
||||||
[Trigger]
|
[Trigger]
|
||||||
Operation = Install
|
Operation = Install
|
||||||
Operation = Upgrade
|
Operation = Upgrade
|
||||||
|
|
@ -14,5 +14,5 @@ Target = opt/*
|
||||||
[Action]
|
[Action]
|
||||||
Description = Refreshing Enodia Sentinel file-integrity baseline...
|
Description = Refreshing Enodia Sentinel file-integrity baseline...
|
||||||
When = PostTransaction
|
When = PostTransaction
|
||||||
Exec = /usr/bin/env enodia-sentinel fim-update
|
Exec = /usr/bin/enodia-sentinel fim-update
|
||||||
Depends = python
|
Depends = python
|
||||||
|
|
|
||||||
3
packaging/enodia-sentinel.tmpfiles
Normal file
3
packaging/enodia-sentinel.tmpfiles
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# Runtime log/state directory for Enodia Sentinel.
|
||||||
|
d /var/log/enodia-sentinel 0750 root root -
|
||||||
103
packaging/release-artifacts.sh
Executable file
103
packaging/release-artifacts.sh
Executable file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# Build release artifacts: source tarball, checksum file, manifest, optional
|
||||||
|
# detached signature over SHA256SUMS.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
version="$(python3 - <<'PY'
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
print(tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"])
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
project="enodia-sentinel"
|
||||||
|
prefix="${project}-${version}"
|
||||||
|
distdir="${DISTDIR:-dist}"
|
||||||
|
archive="${prefix}.tar.gz"
|
||||||
|
manifest="RELEASE_MANIFEST.json"
|
||||||
|
checksums="SHA256SUMS"
|
||||||
|
signature="SHA256SUMS.asc"
|
||||||
|
allow_dirty="${RELEASE_ALLOW_DIRTY:-0}"
|
||||||
|
sign="${RELEASE_SIGN:-0}"
|
||||||
|
signing_key="${RELEASE_SIGNING_KEY:-}"
|
||||||
|
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
dirty=0
|
||||||
|
git diff --quiet || dirty=1
|
||||||
|
git diff --cached --quiet || dirty=1
|
||||||
|
if [ -n "$(git ls-files --others --exclude-standard)" ]; then
|
||||||
|
dirty=1
|
||||||
|
fi
|
||||||
|
git_commit="$(git rev-parse --verify HEAD)"
|
||||||
|
if [ "$dirty" -ne 0 ] && [ "$allow_dirty" != "1" ]; then
|
||||||
|
echo "error: worktree is dirty; commit changes or set RELEASE_ALLOW_DIRTY=1" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
dirty=1
|
||||||
|
git_commit=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$distdir"
|
||||||
|
tmp="$(mktemp -d "${TMPDIR:-/tmp}/enodia-release.XXXXXX")"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
mkdir -p "$tmp/$prefix"
|
||||||
|
tar \
|
||||||
|
--exclude='./.git' \
|
||||||
|
--exclude='./dist' \
|
||||||
|
--exclude='./__pycache__' \
|
||||||
|
--exclude='*/__pycache__' \
|
||||||
|
--exclude='./.pytest_cache' \
|
||||||
|
--exclude='./.mypy_cache' \
|
||||||
|
--exclude='./.ruff_cache' \
|
||||||
|
--exclude='*.pyc' \
|
||||||
|
--exclude='*.pkg.tar.zst' \
|
||||||
|
-cf - . | tar -xf - -C "$tmp/$prefix"
|
||||||
|
|
||||||
|
tar --sort=name --mtime='@0' --owner=0 --group=0 --numeric-owner \
|
||||||
|
-czf "$distdir/$archive" -C "$tmp" "$prefix"
|
||||||
|
|
||||||
|
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
cat > "$distdir/$manifest" <<JSON
|
||||||
|
{
|
||||||
|
"project": "$project",
|
||||||
|
"version": "$version",
|
||||||
|
"generated_at": "$generated_at",
|
||||||
|
"git_commit": "$git_commit",
|
||||||
|
"dirty": $dirty,
|
||||||
|
"artifacts": [
|
||||||
|
"$archive",
|
||||||
|
"$checksums"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$distdir"
|
||||||
|
sha256sum "$archive" "$manifest" > "$checksums"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$sign" = "1" ]; then
|
||||||
|
command -v gpg >/dev/null 2>&1 || {
|
||||||
|
echo "error: RELEASE_SIGN=1 requires gpg" >&2
|
||||||
|
exit 127
|
||||||
|
}
|
||||||
|
gpg_args=(--batch --yes --armor --detach-sign)
|
||||||
|
if [ -n "$signing_key" ]; then
|
||||||
|
gpg_args+=(--local-user "$signing_key")
|
||||||
|
fi
|
||||||
|
gpg "${gpg_args[@]}" -o "$distdir/$signature" "$distdir/$checksums"
|
||||||
|
echo "Signed $distdir/$signature"
|
||||||
|
else
|
||||||
|
echo "Signing skipped; set RELEASE_SIGN=1 to create $signature"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Wrote $distdir/$archive"
|
||||||
|
echo "Wrote $distdir/$checksums"
|
||||||
|
echo "Wrote $distdir/$manifest"
|
||||||
|
|
@ -26,10 +26,12 @@ class TestDocsVersioning(unittest.TestCase):
|
||||||
self.assertEqual(docs_version["schema_generation"], "v1")
|
self.assertEqual(docs_version["schema_generation"], "v1")
|
||||||
self.assertEqual(docs_version["command_reference"], "docs/COMMAND_REFERENCE.md")
|
self.assertEqual(docs_version["command_reference"], "docs/COMMAND_REFERENCE.md")
|
||||||
self.assertEqual(docs_version["schema_reference"], "docs/SCHEMAS.md")
|
self.assertEqual(docs_version["schema_reference"], "docs/SCHEMAS.md")
|
||||||
|
self.assertEqual(docs_version["packaging_reference"], "docs/PACKAGING.md")
|
||||||
|
self.assertEqual(docs_version["release_artifact_target"], "make release-artifacts")
|
||||||
|
|
||||||
def test_versioned_docs_headers_are_present(self):
|
def test_versioned_docs_headers_are_present(self):
|
||||||
version = json.loads((DOCS / "VERSION.json").read_text())
|
version = json.loads((DOCS / "VERSION.json").read_text())
|
||||||
for name in ("INDEX.md", "COMMAND_REFERENCE.md", "SCHEMAS.md"):
|
for name in ("INDEX.md", "COMMAND_REFERENCE.md", "SCHEMAS.md", "PACKAGING.md"):
|
||||||
with self.subTest(name=name):
|
with self.subTest(name=name):
|
||||||
text = (DOCS / name).read_text()
|
text = (DOCS / name).read_text()
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
|
|
|
||||||
146
tests/test_packaging.py
Normal file
146
tests/test_packaging.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
"""Static checks for package metadata and install hardening."""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import tomllib
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from enodia_sentinel import __version__
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _read(path: str) -> str:
|
||||||
|
return (ROOT / path).read_text()
|
||||||
|
|
||||||
|
|
||||||
|
class TestArchPackaging(unittest.TestCase):
|
||||||
|
def test_pkgbuild_version_matches_project(self):
|
||||||
|
pkgbuild = _read("packaging/PKGBUILD")
|
||||||
|
pyproject = tomllib.loads(_read("pyproject.toml"))
|
||||||
|
docs_version = json.loads(_read("docs/VERSION.json"))
|
||||||
|
|
||||||
|
pkgver = re.search(r"^pkgver=([^\n]+)$", pkgbuild, re.M)
|
||||||
|
self.assertIsNotNone(pkgver)
|
||||||
|
self.assertEqual(pkgver.group(1), pyproject["project"]["version"])
|
||||||
|
self.assertEqual(pkgver.group(1), __version__)
|
||||||
|
self.assertEqual(docs_version["package_version"], pkgver.group(1))
|
||||||
|
|
||||||
|
def test_pkgbuild_installs_hardened_runtime_files(self):
|
||||||
|
pkgbuild = _read("packaging/PKGBUILD")
|
||||||
|
for expected in (
|
||||||
|
"/usr/bin/enodia-sentinel",
|
||||||
|
"/usr/lib/systemd/system/enodia-sentinel.service",
|
||||||
|
"/usr/lib/systemd/system/enodia-sentinel-web.service",
|
||||||
|
"/usr/share/libalpm/hooks/enodia-sentinel-fim.hook",
|
||||||
|
"/usr/lib/tmpfiles.d/enodia-sentinel.conf",
|
||||||
|
"/usr/share/doc/$pkgname/PACKAGING.md",
|
||||||
|
"/usr/share/doc/$pkgname/VERSION.json",
|
||||||
|
"/usr/share/doc/$pkgname/examples/enodia-sentinel-ebpf.conf",
|
||||||
|
"/var/log/enodia-sentinel",
|
||||||
|
):
|
||||||
|
self.assertIn(expected, pkgbuild)
|
||||||
|
self.assertIn("backup=('etc/enodia-sentinel.toml')", pkgbuild)
|
||||||
|
self.assertIn("install -dm750", pkgbuild)
|
||||||
|
|
||||||
|
def test_alpm_hook_uses_absolute_sentinel_path(self):
|
||||||
|
hook = _read("packaging/enodia-sentinel-fim.hook")
|
||||||
|
self.assertIn("Exec = /usr/bin/enodia-sentinel fim-update", hook)
|
||||||
|
self.assertNotIn("/usr/bin/env enodia-sentinel", hook)
|
||||||
|
self.assertIn("When = PostTransaction", hook)
|
||||||
|
|
||||||
|
def test_tmpfiles_declares_private_log_dir(self):
|
||||||
|
tmpfiles = _read("packaging/enodia-sentinel.tmpfiles")
|
||||||
|
self.assertIn("d /var/log/enodia-sentinel 0750 root root -", tmpfiles)
|
||||||
|
|
||||||
|
def test_build_script_uses_clean_package_build(self):
|
||||||
|
script = _read("packaging/build-package.sh")
|
||||||
|
self.assertIn("set -eu", script)
|
||||||
|
self.assertIn("command -v makepkg", script)
|
||||||
|
self.assertIn("--cleanbuild", script)
|
||||||
|
|
||||||
|
|
||||||
|
class TestReleaseArtifacts(unittest.TestCase):
|
||||||
|
def test_release_script_generates_checksummed_artifacts(self):
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update({
|
||||||
|
"DISTDIR": d,
|
||||||
|
"RELEASE_ALLOW_DIRTY": "1",
|
||||||
|
"RELEASE_SIGN": "0",
|
||||||
|
})
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(ROOT / "packaging" / "release-artifacts.sh")],
|
||||||
|
cwd=ROOT,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
self.assertIn("Signing skipped", result.stdout)
|
||||||
|
|
||||||
|
version = tomllib.loads(_read("pyproject.toml"))["project"]["version"]
|
||||||
|
archive = Path(d) / f"enodia-sentinel-{version}.tar.gz"
|
||||||
|
manifest = Path(d) / "RELEASE_MANIFEST.json"
|
||||||
|
checksums = Path(d) / "SHA256SUMS"
|
||||||
|
|
||||||
|
self.assertTrue(archive.is_file())
|
||||||
|
self.assertTrue(manifest.is_file())
|
||||||
|
self.assertTrue(checksums.is_file())
|
||||||
|
self.assertFalse((Path(d) / "SHA256SUMS.asc").exists())
|
||||||
|
|
||||||
|
data = json.loads(manifest.read_text())
|
||||||
|
self.assertEqual(data["project"], "enodia-sentinel")
|
||||||
|
self.assertEqual(data["version"], version)
|
||||||
|
self.assertEqual(data["dirty"], 1)
|
||||||
|
self.assertIn(archive.name, data["artifacts"])
|
||||||
|
self.assertIn("SHA256SUMS", data["artifacts"])
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
archive.name: hashlib.sha256(archive.read_bytes()).hexdigest(),
|
||||||
|
manifest.name: hashlib.sha256(manifest.read_bytes()).hexdigest(),
|
||||||
|
}
|
||||||
|
actual = {}
|
||||||
|
for line in checksums.read_text().splitlines():
|
||||||
|
digest, name = line.split(maxsplit=1)
|
||||||
|
actual[name] = digest
|
||||||
|
self.assertEqual(actual, expected)
|
||||||
|
|
||||||
|
def test_release_script_supports_gpg_signing(self):
|
||||||
|
script = _read("packaging/release-artifacts.sh")
|
||||||
|
self.assertIn("RELEASE_SIGN", script)
|
||||||
|
self.assertIn("RELEASE_SIGNING_KEY", script)
|
||||||
|
self.assertIn("--armor --detach-sign", script)
|
||||||
|
self.assertIn("SHA256SUMS.asc", script)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSourceInstallPackaging(unittest.TestCase):
|
||||||
|
def test_makefile_installs_docs_and_tmpfiles(self):
|
||||||
|
makefile = _read("Makefile")
|
||||||
|
self.assertIn("TMPFILESDIR := /etc/tmpfiles.d", makefile)
|
||||||
|
self.assertIn("packaging/enodia-sentinel.tmpfiles", makefile)
|
||||||
|
self.assertIn("docs/PACKAGING.md", makefile)
|
||||||
|
self.assertIn("docs/VERSION.json", makefile)
|
||||||
|
self.assertIn("systemd/enodia-sentinel-ebpf.conf", makefile)
|
||||||
|
|
||||||
|
def test_packaging_guide_documents_distro_paths(self):
|
||||||
|
guide = _read("docs/PACKAGING.md")
|
||||||
|
for phrase in (
|
||||||
|
"Arch is the primary packaged target",
|
||||||
|
"sudo pacman -U enodia-sentinel-*.pkg.tar.zst",
|
||||||
|
"There is no native `.deb` package yet",
|
||||||
|
"There is no native RPM package yet",
|
||||||
|
"Package-manager hooks use absolute paths",
|
||||||
|
):
|
||||||
|
self.assertIn(phrase, guide)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue