Rootless Linux host inventory and hardening audit CLI.
Project description
Linwarden
Linwarden is a rootless Linux hardening scanner for CI and fleet triage. It reads ordinary system files such as /etc/os-release, /etc/ssh/sshd_config, and selected /proc/sys values, then produces Markdown, JSON, or SARIF artifacts without installing an agent, daemon, privileged helper, database, or network service.
The project goal is practical: give maintainers a small, auditable tool that explains risky Linux defaults without needing an agent, daemon, privileged service, external database, or network access.
Why Linwarden
Use Linwarden when you need a fast security posture signal, not a heavyweight compliance platform.
| Need | Linwarden approach |
|---|---|
| CI-friendly output | JSON, Markdown, and SARIF for GitHub code scanning. |
| Low operational risk | Read-only collection from ordinary Linux files. |
| Offline analysis | Scan mounted roots, image extracts, containers, and fixtures. |
| Explainable findings | Every rule includes evidence, impact, remediation, and references. |
| Small supply chain | Zero runtime dependencies beyond Python 3.9+. |
Linwarden is not a CIS or STIG replacement. It is the lightweight first pass that tells operators what deserves attention before they reach for heavier scanners.
Features
- Rootless collection from
/etc,/proc, and procfs sysctl paths. - Deterministic JSON output for CI pipelines and scheduled host scans.
- Markdown output suitable for GitHub job summaries and issue attachments.
- SARIF output suitable for GitHub-native security ingestion.
- JSON config support for profiles, disabled rules, and justified suppressions.
- Optional effective OpenSSH config collection through
sshd -T, including Match context. - Package update and host firewall posture signals where rootless files expose them.
- Package metadata freshness checks for common package manager cache markers.
- Bridge interface and bridge firewall hook posture checks for container hosts.
- Static container runtime posture checks for exposed Docker or Podman APIs and Docker group membership.
- Optional package vulnerability findings from a local JSON feed.
- Release checksum manifests with optional detached GPG signatures.
- Severity scoring with
critical,high,medium, andlowbuckets. - CI-friendly exit thresholds through
--fail-on. - Composite GitHub Action wrapper through
uses: kingkyylian/linwarden@v0.15.0. - Fixture-root scanning for tests, containers, forensic copies, and offline analysis.
- Zero runtime dependencies beyond Python 3.9+.
Quick Start
Install from PyPI:
python3 -m pip install linwarden
linwarden --version
linwarden scan --format markdown
For an isolated CLI install:
pipx install linwarden
From a development checkout:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e .
linwarden scan --format markdown
Run against the included fixture:
PYTHONPATH=src python3 -m linwarden scan \
--root tests/fixtures/linux-root \
--format json \
--fail-on high
Exit code 2 means at least one finding matched the selected threshold.
See the synthetic terminal demo for a Markdown scan transcript generated from the fixture root.
Common Workflows
| Workflow | Command or doc |
|---|---|
| Local triage | linwarden scan --format markdown |
| CI failure threshold | linwarden scan --format json --fail-on high |
| GitHub code scanning | uses: kingkyylian/linwarden@v0.15.0 |
| Mounted image scan | linwarden scan --root /mnt/server-image --format json |
| Effective SSH scan | linwarden scan --sshd-mode effective --sshd-match user=deploy |
| Tool positioning | docs/comparison.md |
CLI
linwarden profiles [--format markdown|json]
linwarden scan [--root PATH] [--proc-root PATH] [--etc-root PATH] [--sys-root PATH]
[--config PATH] [--format markdown|json|sarif]
[--vulnerability-feed PATH] [--vulnerability-feed-format linwarden|trivy|grype|osv]
[--sshd-mode static|effective|auto] [--sshd-binary PATH]
[--sshd-match KEY=VALUE]
[--output PATH]
[--fail-on off|low|medium|high|critical]
Common examples:
linwarden profiles
linwarden scan --format markdown --output linwarden-report.md
linwarden scan --format json --fail-on high
linwarden scan --config linwarden.json --format sarif --output linwarden.sarif
linwarden scan --sshd-mode effective --format json
linwarden scan --sshd-mode effective --sshd-match user=deploy --sshd-match addr=203.0.113.10
linwarden scan --root /mnt/server-image --format json
linwarden scan --proc-root /host/proc --etc-root /host/etc --sys-root /host/sys --format markdown
linwarden scan --vulnerability-feed ./linwarden-vulnerabilities.json --format sarif
linwarden scan --vulnerability-feed ./trivy-report.json --vulnerability-feed-format trivy --format sarif
linwarden scan --vulnerability-feed ./grype-report.json --vulnerability-feed-format grype --format sarif
linwarden scan --vulnerability-feed ./osv-scanner-report.json --vulnerability-feed-format osv --format sarif
Configuration
Linwarden accepts a zero-dependency JSON config file:
{
"profile": "router",
"disabled_rules": ["LNX-NET-002"],
"suppressions": [
{
"rule_id": "LNX-SSH-002",
"reason": "Temporary migration host; password auth removed after cutover."
}
]
}
Profiles:
| Profile | Behavior |
|---|---|
server |
Default for general Linux servers. No profile suppressions. |
workstation |
Interactive desktop or laptop posture. No profile suppressions; SSH, firewall, package, and kernel findings stay visible. |
router |
For hosts that intentionally route traffic. Suppresses IPv4, IPv6, and bridge forwarding findings. |
container |
For container or image-root scans where host-kernel sysctl values may be inherited. Suppresses kernel and filesystem sysctl findings. |
Suppressed findings remain visible in JSON and Markdown reports. SARIF output includes active findings only.
Exit Codes
| Code | Meaning |
|---|---|
0 |
Scan completed and no selected threshold was met. |
1 |
CLI usage error from argument parsing. |
2 |
Scan completed and --fail-on threshold was met. |
Current Rules
| Rule | Severity | Area | Summary |
|---|---|---|---|
LNX-SSH-001 |
high | SSH | PermitRootLogin yes is enabled. |
LNX-SSH-002 |
medium | SSH | PasswordAuthentication yes is enabled. |
LNX-SSH-003 |
high | SSH | PermitEmptyPasswords yes is enabled. |
LNX-SSH-004 |
medium | SSH | MaxAuthTries is above 4. |
LNX-SSH-005 |
medium | SSH | AllowTcpForwarding yes or all is enabled. |
LNX-KRN-001 |
high | Kernel | kernel.randomize_va_space=0 disables ASLR. |
LNX-KRN-002 |
high | Kernel | vm.mmap_min_addr is below 65536. |
LNX-KRN-003 |
medium | Kernel | kernel.kptr_restrict=0 exposes kernel pointers. |
LNX-FS-001 |
high | Filesystem | fs.protected_hardlinks=0 disables hardlink protection. |
LNX-FS-002 |
high | Filesystem | fs.protected_symlinks=0 disables symlink protection. |
LNX-NET-001 |
medium | Network | net.ipv4.ip_forward=1 is enabled. |
LNX-NET-002 |
low | Network | net.ipv4.conf.all.accept_redirects=1 is enabled. |
LNX-NET-003 |
medium | Network | net.ipv6.conf.all.forwarding=1 is enabled. |
LNX-NET-004 |
low | Network | net.ipv6.conf.all.accept_redirects=1 is enabled. |
LNX-NET-005 |
medium | Network | Bridge IPv4 firewall hooks are disabled while bridge interfaces exist. |
LNX-NET-006 |
medium | Network | Bridge IPv6 firewall hooks are disabled while bridge interfaces exist. |
LNX-NET-007 |
medium | Network | A bridge interface has forwarding enabled. |
LNX-PKG-001 |
medium | Packages | Package updates are available. |
LNX-PKG-002 |
high | Packages | Security package updates are available. |
LNX-PKG-003 |
medium | Packages | Package metadata is stale. |
LNX-PKG-004 |
feed severity | Packages | A local vulnerability feed reports an affected package. |
LNX-FW-001 |
medium | Firewall | A known host firewall is disabled. |
LNX-SVC-001 |
medium | Services | An enabled systemd unit appears externally bound. |
LNX-CTR-001 |
high | Containers | A container runtime API is bound to non-loopback TCP. |
LNX-CTR-002 |
high | Containers | The Docker group grants daemon-level access to non-root users. |
Rule details live in docs/rules.md.
Report Score
Linwarden starts each report at 100 and subtracts a fixed penalty per finding:
| Severity | Penalty |
|---|---|
| critical | 35 |
| high | 20 |
| medium | 10 |
| low | 3 |
The score is intentionally simple. It is a triage signal, not a compliance rating.
Project Layout
src/linwarden/
cli.py command line entry point
config.py profiles, disabled rules, and suppressions
collectors.py host snapshot collection
parsers.py small parsers for Linux files
rules.py built-in hardening checks
reporters.py JSON, Markdown, and SARIF rendering
models.py report data structures
tests/
fixtures/ deterministic Linux fixture root
docs/
architecture.md implementation overview
configuration.md profile and suppression config
comparison.md positioning against adjacent Linux security tools
contributor-ideas.md scoped contribution backlog
github-actions.md CI and SARIF workflow examples
launch.md copy and checklist for public announcements
positioning.md maintainer messaging guide
release.md release artifact and publishing workflow
rules.md rule catalog
report-schema.md JSON report contract
schemas/
report.schema.json machine-readable JSON report schema
Development
make test
make compile
make lint
make typecheck
make smoke
make smoke-sarif
make check
Use make check PYTHON=.venv/bin/python when running through a project virtualenv.
No network services or privileged permissions are required for the test suite.
Security Model
Linwarden is read-only by default. It does not modify host state, load kernel modules, call package managers, or send telemetry. Reports can contain host configuration details, so treat generated artifacts as operationally sensitive.
Known Limits
- Static SSH mode reads
sshd_configplus simpleIncludedirectives;Matchbehavior may differ from effective OpenSSH config. - Effective SSH mode executes
sshd -T; use it only when scanning the live host intentionally. - Package metadata age relies on local cache marker mtimes and does not call package manager commands.
- Package vulnerability findings require an explicit local JSON feed and never fetch remote CVE data.
- Bridge posture checks rely on procfs and sysfs files inside the scanned root; missing bridge data is treated as unknown.
- Firewalld and nftables service state is inferred from systemd enablement markers when present; config-only detection leaves enabled state unknown.
- Enabled systemd service exposure detection is static and only flags common wildcard bind options in service unit
ExecStartlines. - Container runtime posture checks only report explicit static evidence from config, group, or enabled unit files; missing runtime files are unknown, not safe.
- Missing files are treated as absent data so scans can run in containers and fixture roots.
- Linwarden is a hardening triage tool, not a full CIS or DISA STIG compliance scanner.
Please report vulnerabilities using SECURITY.md.
Roadmap
- Fixture-backed distro coverage for package metadata and firewall posture.
- Static container runtime signals only when evidence is reliable and rootless.
- Release checks that keep GitHub artifacts, attestations, and PyPI installs verifiable.
Contributor-ready ideas live in docs/contributor-ideas.md.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file linwarden-0.15.0.tar.gz.
File metadata
- Download URL: linwarden-0.15.0.tar.gz
- Upload date:
- Size: 90.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef961852906b01263d3be4e67c49fc7cd2b32f023feed7452a52e4f5e39e178d
|
|
| MD5 |
fab9daede96e1ce521dfabda2f77727a
|
|
| BLAKE2b-256 |
4a471d49c21c043d05b7b751e5475f5436c41c4d0e07351c4676259876ef7429
|
Provenance
The following attestation bundles were made for linwarden-0.15.0.tar.gz:
Publisher:
release.yml on kingkyylian/linwarden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
linwarden-0.15.0.tar.gz -
Subject digest:
ef961852906b01263d3be4e67c49fc7cd2b32f023feed7452a52e4f5e39e178d - Sigstore transparency entry: 1615001966
- Sigstore integration time:
-
Permalink:
kingkyylian/linwarden@ec02edfa84ca6528b9747bbb42454d46f5df8d09 -
Branch / Tag:
refs/tags/v0.15.0 - Owner: https://github.com/kingkyylian
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ec02edfa84ca6528b9747bbb42454d46f5df8d09 -
Trigger Event:
push
-
Statement type:
File details
Details for the file linwarden-0.15.0-py3-none-any.whl.
File metadata
- Download URL: linwarden-0.15.0-py3-none-any.whl
- Upload date:
- Size: 30.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea3bb3ebce67485243f8a9fe678378bf2d9699a3ec149802ba45cf8eed5fffaf
|
|
| MD5 |
8bfc2f31f529d19e1a74d158bc09c96a
|
|
| BLAKE2b-256 |
32dfa095aa5087f69bf3524ffdfaa07bb84a56c5daedfe274de0291988b765bb
|
Provenance
The following attestation bundles were made for linwarden-0.15.0-py3-none-any.whl:
Publisher:
release.yml on kingkyylian/linwarden
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
linwarden-0.15.0-py3-none-any.whl -
Subject digest:
ea3bb3ebce67485243f8a9fe678378bf2d9699a3ec149802ba45cf8eed5fffaf - Sigstore transparency entry: 1615001996
- Sigstore integration time:
-
Permalink:
kingkyylian/linwarden@ec02edfa84ca6528b9747bbb42454d46f5df8d09 -
Branch / Tag:
refs/tags/v0.15.0 - Owner: https://github.com/kingkyylian
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ec02edfa84ca6528b9747bbb42454d46f5df8d09 -
Trigger Event:
push
-
Statement type: