infradrift 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infradrift-1.1.0/LICENSE +21 -0
- infradrift-1.1.0/PKG-INFO +195 -0
- infradrift-1.1.0/README.md +168 -0
- infradrift-1.1.0/infradrift/__init__.py +5 -0
- infradrift-1.1.0/infradrift/__main__.py +3 -0
- infradrift-1.1.0/infradrift/cli.py +178 -0
- infradrift-1.1.0/infradrift/collectors.py +288 -0
- infradrift-1.1.0/infradrift/diff.py +193 -0
- infradrift-1.1.0/infradrift/renderer.py +174 -0
- infradrift-1.1.0/infradrift/snapshot.py +77 -0
- infradrift-1.1.0/infradrift/versions.py +49 -0
- infradrift-1.1.0/infradrift.egg-info/PKG-INFO +195 -0
- infradrift-1.1.0/infradrift.egg-info/SOURCES.txt +20 -0
- infradrift-1.1.0/infradrift.egg-info/dependency_links.txt +1 -0
- infradrift-1.1.0/infradrift.egg-info/entry_points.txt +2 -0
- infradrift-1.1.0/infradrift.egg-info/requires.txt +5 -0
- infradrift-1.1.0/infradrift.egg-info/top_level.txt +1 -0
- infradrift-1.1.0/pyproject.toml +51 -0
- infradrift-1.1.0/setup.cfg +4 -0
- infradrift-1.1.0/tests/test_cli.py +72 -0
- infradrift-1.1.0/tests/test_collectors.py +69 -0
- infradrift-1.1.0/tests/test_diff.py +113 -0
infradrift-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serber1990
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infradrift
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Detect drift between a known-good baseline and the current state of a Linux server โ packages, ports, users, groups, crons and services.
|
|
5
|
+
Author-email: Serber1990 <serber1990@pm.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/serber1990/infradrift
|
|
8
|
+
Project-URL: Repository, https://github.com/serber1990/infradrift
|
|
9
|
+
Project-URL: Issues, https://github.com/serber1990/infradrift/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/serber1990/infradrift/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: infrastructure,drift,sysadmin,devops,security,audit,snapshot,linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Topic :: System :: Systems Administration
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: shellcolorize>=1.1.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# infradrift
|
|
29
|
+
|
|
30
|
+
[](https://github.com/serber1990/infradrift/actions/workflows/ci.yml)
|
|
31
|
+
[](https://badge.fury.io/py/infradrift)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
Detect **infrastructure drift** between a known-good baseline and the current state of any Linux server โ
|
|
35
|
+
packages, listening ports, user accounts, privileged groups, cron jobs and systemd services.
|
|
36
|
+
|
|
37
|
+
Take a snapshot when the server is in a good state. Run `check` any time โ after a deploy, from cron,
|
|
38
|
+
or during an incident โ to see exactly what changed. No agent, no daemon, no dependencies on the server
|
|
39
|
+
beyond Python.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ๐ Example
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
47
|
+
โ infradrift ยท drift report โ
|
|
48
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
49
|
+
|
|
50
|
+
Baseline 2026-05-10T09:00:00+02:00 (prod-01) as root
|
|
51
|
+
Current 2026-05-12T14:22:18+02:00 (prod-01) as root
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
โโ Ports โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
55
|
+
โ + 4444/tcp on all interfaces (nc) โ new listening port
|
|
56
|
+
โ ~ 6379/tcp on all interfaces (redis-server) โ now listening on a new address
|
|
57
|
+
|
|
58
|
+
โโ Users โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
59
|
+
โ ~ backup โ account changed shell /usr/sbin/nologin โ /bin/bash
|
|
60
|
+
โ + deploy โ new user uid=1005 shell=/bin/bash
|
|
61
|
+
|
|
62
|
+
โโ Privileged Groups โโโโโโโโโโโโโโโโโโโโโ
|
|
63
|
+
โ + deploy added to privileged group sudo
|
|
64
|
+
|
|
65
|
+
โโ Cron Jobs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
66
|
+
โ + New cron job: @reboot /tmp/.cache/update.sh crontab:www-data
|
|
67
|
+
|
|
68
|
+
โโ Services โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
69
|
+
โ ~ nginx active โ failed
|
|
70
|
+
|
|
71
|
+
โโ Packages โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
72
|
+
ยท ~ [apt] openssl upgraded 3.0.13-0ubuntu3 โ 3.0.13-0ubuntu3.1
|
|
73
|
+
|
|
74
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
75
|
+
8 changes detected (5 critical, 2 warnings, 1 info)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## โจ What it detects
|
|
81
|
+
|
|
82
|
+
| Category | What's tracked |
|
|
83
|
+
|----------|----------------|
|
|
84
|
+
| **Packages** | dpkg (Debian/Ubuntu), rpm (RHEL/Fedora/SUSE), pacman (Arch), pip โ installed, removed, upgraded, downgraded |
|
|
85
|
+
| **Ports** | Listening TCP and bound UDP sockets **with their bind address** โ a service moving from `127.0.0.1` to `0.0.0.0` is flagged |
|
|
86
|
+
| **Users** | `/etc/passwd` accounts โ added, removed, uid/gid/shell/home changes |
|
|
87
|
+
| **Privileged groups** | Membership of `sudo`, `wheel`, `admin`, `docker`, `lxd`, `libvirt`, `adm`, `disk`, `shadow`, `root` |
|
|
88
|
+
| **Cron jobs** | User crontabs, `/etc/crontab`, `/etc/cron.d/`, `/etc/cron.{hourly,daily,weekly,monthly}/`, including `@reboot` entries |
|
|
89
|
+
| **Services** | systemd unit state (active / inactive / failed) and boot enablement (enabled / disabled / masked) |
|
|
90
|
+
|
|
91
|
+
### Severity levels
|
|
92
|
+
|
|
93
|
+
| Severity | Examples |
|
|
94
|
+
|----------|----------|
|
|
95
|
+
| โ **CRITICAL** | New listening port, port exposed on a new address, new user, uid/shell change, user added to a privileged group |
|
|
96
|
+
| โ ๏ธ **WARNING** | Service state or enablement change, new enabled service, cron job added/removed, package removed or downgraded |
|
|
97
|
+
| โน๏ธ **INFO** | Package installed or upgraded, new high UDP port (ephemeral range โ usually a client socket) |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## ๐ฅ Installation
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install infradrift
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run it as **root** for complete results: other users' crontabs and the process names of every socket are
|
|
108
|
+
only visible to root. infradrift warns you when it runs unprivileged.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## ๐ Usage
|
|
113
|
+
|
|
114
|
+
### 1 โ Take a baseline snapshot
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
sudo infradrift snapshot
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Saved to `/var/lib/infradrift/baseline.json` when run as root, or `~/.config/infradrift/baseline.json` otherwise.
|
|
121
|
+
The file is created with `0600` permissions: it lists users, ports and cron commands.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
sudo infradrift snapshot --output /opt/baselines/prod-$(date +%F).json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2 โ Check for drift
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
sudo infradrift check # terminal report
|
|
131
|
+
sudo infradrift check --format json | jq .summary # JSON on stdout, progress on stderr
|
|
132
|
+
sudo infradrift check -f markdown -o drift-report.md # Markdown file
|
|
133
|
+
sudo infradrift check --baseline /opt/baselines/prod-2026-05-12.json --fail-on critical
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3 โ Diff any two snapshots
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
infradrift diff before-deploy.json after-deploy.json
|
|
140
|
+
infradrift diff web-01.json web-02.json --format markdown # compare two servers
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## ๐ Use in cron / CI
|
|
146
|
+
|
|
147
|
+
| Exit code | Meaning |
|
|
148
|
+
|-----------|---------|
|
|
149
|
+
| `0` | No drift at or above `--fail-on` |
|
|
150
|
+
| `1` | Drift detected |
|
|
151
|
+
| `2` | Error (missing or unreadable baseline) |
|
|
152
|
+
|
|
153
|
+
```cron
|
|
154
|
+
# Every hour: keep a Markdown report only when something critical changed
|
|
155
|
+
0 * * * * root infradrift check --fail-on critical -o /var/log/infradrift/last.md || cp /var/log/infradrift/last.md /var/log/infradrift/alert-$(date +\%F-\%H).md
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## ๐ Options
|
|
161
|
+
|
|
162
|
+
### `infradrift snapshot`
|
|
163
|
+
| Option | Description |
|
|
164
|
+
|--------|-------------|
|
|
165
|
+
| `-o`, `--output FILE` | Where to save the baseline |
|
|
166
|
+
|
|
167
|
+
### `infradrift check` / `infradrift diff BASELINE CURRENT`
|
|
168
|
+
| Option | Description |
|
|
169
|
+
|--------|-------------|
|
|
170
|
+
| `-b`, `--baseline FILE` | *(check)* Baseline to compare against |
|
|
171
|
+
| `-f`, `--format` | `terminal` (default), `json` or `markdown` |
|
|
172
|
+
| `-o`, `--output FILE` | Write the report to a file (JSON or Markdown) |
|
|
173
|
+
| `--fail-on LEVEL` | `info` (default: any change), `warning`, `critical` or `never` |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## ๐งช Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install -e ".[dev]"
|
|
181
|
+
ruff check .
|
|
182
|
+
pytest
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## ๐ License
|
|
190
|
+
|
|
191
|
+
MIT โ see [LICENSE](LICENSE).
|
|
192
|
+
|
|
193
|
+
## ๐ Connect
|
|
194
|
+
|
|
195
|
+
[](https://github.com/serber1990)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# infradrift
|
|
2
|
+
|
|
3
|
+
[](https://github.com/serber1990/infradrift/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/py/infradrift)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Detect **infrastructure drift** between a known-good baseline and the current state of any Linux server โ
|
|
8
|
+
packages, listening ports, user accounts, privileged groups, cron jobs and systemd services.
|
|
9
|
+
|
|
10
|
+
Take a snapshot when the server is in a good state. Run `check` any time โ after a deploy, from cron,
|
|
11
|
+
or during an incident โ to see exactly what changed. No agent, no daemon, no dependencies on the server
|
|
12
|
+
beyond Python.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ๐ Example
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
20
|
+
โ infradrift ยท drift report โ
|
|
21
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
22
|
+
|
|
23
|
+
Baseline 2026-05-10T09:00:00+02:00 (prod-01) as root
|
|
24
|
+
Current 2026-05-12T14:22:18+02:00 (prod-01) as root
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
โโ Ports โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
28
|
+
โ + 4444/tcp on all interfaces (nc) โ new listening port
|
|
29
|
+
โ ~ 6379/tcp on all interfaces (redis-server) โ now listening on a new address
|
|
30
|
+
|
|
31
|
+
โโ Users โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
32
|
+
โ ~ backup โ account changed shell /usr/sbin/nologin โ /bin/bash
|
|
33
|
+
โ + deploy โ new user uid=1005 shell=/bin/bash
|
|
34
|
+
|
|
35
|
+
โโ Privileged Groups โโโโโโโโโโโโโโโโโโโโโ
|
|
36
|
+
โ + deploy added to privileged group sudo
|
|
37
|
+
|
|
38
|
+
โโ Cron Jobs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
39
|
+
โ + New cron job: @reboot /tmp/.cache/update.sh crontab:www-data
|
|
40
|
+
|
|
41
|
+
โโ Services โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
42
|
+
โ ~ nginx active โ failed
|
|
43
|
+
|
|
44
|
+
โโ Packages โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
45
|
+
ยท ~ [apt] openssl upgraded 3.0.13-0ubuntu3 โ 3.0.13-0ubuntu3.1
|
|
46
|
+
|
|
47
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
48
|
+
8 changes detected (5 critical, 2 warnings, 1 info)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## โจ What it detects
|
|
54
|
+
|
|
55
|
+
| Category | What's tracked |
|
|
56
|
+
|----------|----------------|
|
|
57
|
+
| **Packages** | dpkg (Debian/Ubuntu), rpm (RHEL/Fedora/SUSE), pacman (Arch), pip โ installed, removed, upgraded, downgraded |
|
|
58
|
+
| **Ports** | Listening TCP and bound UDP sockets **with their bind address** โ a service moving from `127.0.0.1` to `0.0.0.0` is flagged |
|
|
59
|
+
| **Users** | `/etc/passwd` accounts โ added, removed, uid/gid/shell/home changes |
|
|
60
|
+
| **Privileged groups** | Membership of `sudo`, `wheel`, `admin`, `docker`, `lxd`, `libvirt`, `adm`, `disk`, `shadow`, `root` |
|
|
61
|
+
| **Cron jobs** | User crontabs, `/etc/crontab`, `/etc/cron.d/`, `/etc/cron.{hourly,daily,weekly,monthly}/`, including `@reboot` entries |
|
|
62
|
+
| **Services** | systemd unit state (active / inactive / failed) and boot enablement (enabled / disabled / masked) |
|
|
63
|
+
|
|
64
|
+
### Severity levels
|
|
65
|
+
|
|
66
|
+
| Severity | Examples |
|
|
67
|
+
|----------|----------|
|
|
68
|
+
| โ **CRITICAL** | New listening port, port exposed on a new address, new user, uid/shell change, user added to a privileged group |
|
|
69
|
+
| โ ๏ธ **WARNING** | Service state or enablement change, new enabled service, cron job added/removed, package removed or downgraded |
|
|
70
|
+
| โน๏ธ **INFO** | Package installed or upgraded, new high UDP port (ephemeral range โ usually a client socket) |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## ๐ฅ Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install infradrift
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Run it as **root** for complete results: other users' crontabs and the process names of every socket are
|
|
81
|
+
only visible to root. infradrift warns you when it runs unprivileged.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## ๐ Usage
|
|
86
|
+
|
|
87
|
+
### 1 โ Take a baseline snapshot
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
sudo infradrift snapshot
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Saved to `/var/lib/infradrift/baseline.json` when run as root, or `~/.config/infradrift/baseline.json` otherwise.
|
|
94
|
+
The file is created with `0600` permissions: it lists users, ports and cron commands.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
sudo infradrift snapshot --output /opt/baselines/prod-$(date +%F).json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2 โ Check for drift
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
sudo infradrift check # terminal report
|
|
104
|
+
sudo infradrift check --format json | jq .summary # JSON on stdout, progress on stderr
|
|
105
|
+
sudo infradrift check -f markdown -o drift-report.md # Markdown file
|
|
106
|
+
sudo infradrift check --baseline /opt/baselines/prod-2026-05-12.json --fail-on critical
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3 โ Diff any two snapshots
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
infradrift diff before-deploy.json after-deploy.json
|
|
113
|
+
infradrift diff web-01.json web-02.json --format markdown # compare two servers
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## ๐ Use in cron / CI
|
|
119
|
+
|
|
120
|
+
| Exit code | Meaning |
|
|
121
|
+
|-----------|---------|
|
|
122
|
+
| `0` | No drift at or above `--fail-on` |
|
|
123
|
+
| `1` | Drift detected |
|
|
124
|
+
| `2` | Error (missing or unreadable baseline) |
|
|
125
|
+
|
|
126
|
+
```cron
|
|
127
|
+
# Every hour: keep a Markdown report only when something critical changed
|
|
128
|
+
0 * * * * root infradrift check --fail-on critical -o /var/log/infradrift/last.md || cp /var/log/infradrift/last.md /var/log/infradrift/alert-$(date +\%F-\%H).md
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## ๐ Options
|
|
134
|
+
|
|
135
|
+
### `infradrift snapshot`
|
|
136
|
+
| Option | Description |
|
|
137
|
+
|--------|-------------|
|
|
138
|
+
| `-o`, `--output FILE` | Where to save the baseline |
|
|
139
|
+
|
|
140
|
+
### `infradrift check` / `infradrift diff BASELINE CURRENT`
|
|
141
|
+
| Option | Description |
|
|
142
|
+
|--------|-------------|
|
|
143
|
+
| `-b`, `--baseline FILE` | *(check)* Baseline to compare against |
|
|
144
|
+
| `-f`, `--format` | `terminal` (default), `json` or `markdown` |
|
|
145
|
+
| `-o`, `--output FILE` | Write the report to a file (JSON or Markdown) |
|
|
146
|
+
| `--fail-on LEVEL` | `info` (default: any change), `warning`, `critical` or `never` |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## ๐งช Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install -e ".[dev]"
|
|
154
|
+
ruff check .
|
|
155
|
+
pytest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## ๐ License
|
|
163
|
+
|
|
164
|
+
MIT โ see [LICENSE](LICENSE).
|
|
165
|
+
|
|
166
|
+
## ๐ Connect
|
|
167
|
+
|
|
168
|
+
[](https://github.com/serber1990)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
infradrift โ snapshot a Linux server and detect drift: packages, ports, users,
|
|
4
|
+
privileged groups, cron jobs and systemd services.
|
|
5
|
+
|
|
6
|
+
Exit codes: 0 no drift (at or above --fail-on) 1 drift detected 2 usage or I/O error
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from shellcolorize import Color
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .diff import diff_snapshots
|
|
17
|
+
from .renderer import render_json, render_markdown, render_terminal
|
|
18
|
+
from .snapshot import StateSnapshot, default_path, take_snapshot
|
|
19
|
+
|
|
20
|
+
EXIT_OK, EXIT_DRIFT, EXIT_ERROR = 0, 1, 2
|
|
21
|
+
FAIL_LEVELS = {'info': 'INFO', 'warning': 'WARNING', 'critical': 'CRITICAL'}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Progress, warnings and errors go to stderr so stdout only carries the report.
|
|
25
|
+
def _ok(msg: str) -> None: print(f" {Color.GREEN}โ{Color.RESET} {msg}", file=sys.stderr)
|
|
26
|
+
def _step(msg: str) -> None: print(f" {Color.CYAN}โถ{Color.RESET} {msg}", file=sys.stderr)
|
|
27
|
+
def _err(msg: str) -> None: print(f" {Color.RED}โ{Color.RESET} {msg}", file=sys.stderr)
|
|
28
|
+
def _warn(msg: str) -> None: print(f" {Color.YELLOW}โ {Color.RESET} {msg}", file=sys.stderr)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _root_hint() -> None:
|
|
32
|
+
if os.geteuid() != 0:
|
|
33
|
+
_warn('Not running as root: other users\' crontabs and some process names are not visible.')
|
|
34
|
+
|
|
35
|
+
# โโ snapshot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
36
|
+
|
|
37
|
+
def cmd_snapshot(args: argparse.Namespace) -> int:
|
|
38
|
+
output = Path(args.output) if args.output else default_path()
|
|
39
|
+
print(f"\n {Color.CYAN}{Color.BOLD}infradrift ยท snapshot{Color.RESET}\n", file=sys.stderr)
|
|
40
|
+
_root_hint()
|
|
41
|
+
snap = take_snapshot(progress=lambda what: _step(f'Collecting {what}...'))
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
snap.save(output)
|
|
45
|
+
except OSError as e:
|
|
46
|
+
_err(f'Cannot write {output}: {e.strerror}')
|
|
47
|
+
if os.geteuid() != 0:
|
|
48
|
+
_err('Use --output to choose another location, or run with sudo.')
|
|
49
|
+
return EXIT_ERROR
|
|
50
|
+
|
|
51
|
+
print()
|
|
52
|
+
_ok(f'Baseline saved: {output}')
|
|
53
|
+
managers = ', '.join(f'{m} {len(p)}' for m, p in snap.packages.items()) or 'none'
|
|
54
|
+
print(f' Packages : {sum(len(p) for p in snap.packages.values())} ({managers})')
|
|
55
|
+
print(f' Ports : {len(snap.ports)} listening')
|
|
56
|
+
print(f' Users : {len(snap.users)} accounts, {len(snap.groups)} privileged groups')
|
|
57
|
+
print(f' Crons : {len(snap.crons)} jobs')
|
|
58
|
+
print(f' Services : {len(snap.services)} units')
|
|
59
|
+
print()
|
|
60
|
+
return EXIT_OK
|
|
61
|
+
|
|
62
|
+
# โโ check / diff โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
63
|
+
|
|
64
|
+
def _load(path: Path):
|
|
65
|
+
try:
|
|
66
|
+
return StateSnapshot.load(path)
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
_err(f'Snapshot not found: {path}')
|
|
69
|
+
except PermissionError:
|
|
70
|
+
_err(f'Permission denied: {path} (baselines are only readable by their owner)')
|
|
71
|
+
except (OSError, ValueError, TypeError) as e:
|
|
72
|
+
_err(f'Cannot read snapshot {path}: {e}')
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _report(report, args: argparse.Namespace) -> int:
|
|
77
|
+
b_user, c_user = report.baseline_meta.get('user'), report.current_meta.get('user')
|
|
78
|
+
if b_user and c_user and b_user != c_user:
|
|
79
|
+
_warn(f'Baseline taken as "{b_user}", current state as "{c_user}": '
|
|
80
|
+
'some differences may only be visibility differences.')
|
|
81
|
+
|
|
82
|
+
fmt = args.format
|
|
83
|
+
if fmt == 'terminal' and args.output:
|
|
84
|
+
fmt = 'markdown' if str(args.output).endswith('.md') else 'json'
|
|
85
|
+
|
|
86
|
+
if fmt == 'terminal':
|
|
87
|
+
render_terminal(report)
|
|
88
|
+
else:
|
|
89
|
+
text = render_json(report) if fmt == 'json' else render_markdown(report)
|
|
90
|
+
if args.output:
|
|
91
|
+
try:
|
|
92
|
+
Path(args.output).write_text(text + '\n', encoding='utf-8')
|
|
93
|
+
except OSError as e:
|
|
94
|
+
_err(f'Cannot write {args.output}: {e.strerror}')
|
|
95
|
+
return EXIT_ERROR
|
|
96
|
+
_ok(f'Report saved to {args.output}')
|
|
97
|
+
else:
|
|
98
|
+
print(text)
|
|
99
|
+
|
|
100
|
+
if args.fail_on == 'never':
|
|
101
|
+
return EXIT_OK
|
|
102
|
+
return EXIT_DRIFT if report.has_at_least(FAIL_LEVELS[args.fail_on]) else EXIT_OK
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cmd_check(args: argparse.Namespace) -> int:
|
|
106
|
+
baseline_path = Path(args.baseline) if args.baseline else default_path()
|
|
107
|
+
if not baseline_path.exists():
|
|
108
|
+
_err(f'Baseline not found: {baseline_path}')
|
|
109
|
+
_err("Run 'infradrift snapshot' first to create one.")
|
|
110
|
+
return EXIT_ERROR
|
|
111
|
+
baseline = _load(baseline_path)
|
|
112
|
+
if baseline is None:
|
|
113
|
+
return EXIT_ERROR
|
|
114
|
+
_step('Collecting current state...')
|
|
115
|
+
return _report(diff_snapshots(baseline, take_snapshot()), args)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_diff(args: argparse.Namespace) -> int:
|
|
119
|
+
snap_a, snap_b = _load(Path(args.snapshot_a)), _load(Path(args.snapshot_b))
|
|
120
|
+
if snap_a is None or snap_b is None:
|
|
121
|
+
return EXIT_ERROR
|
|
122
|
+
return _report(diff_snapshots(snap_a, snap_b), args)
|
|
123
|
+
|
|
124
|
+
# โโ main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
125
|
+
|
|
126
|
+
def _add_report_options(p: argparse.ArgumentParser) -> None:
|
|
127
|
+
p.add_argument('--format', '-f', choices=['terminal', 'json', 'markdown'], default='terminal',
|
|
128
|
+
help='Report format (default: terminal)')
|
|
129
|
+
p.add_argument('--output', '-o', metavar='FILE',
|
|
130
|
+
help='Write the report to FILE (json or markdown; inferred from .md/.json if needed)')
|
|
131
|
+
p.add_argument('--fail-on', choices=['info', 'warning', 'critical', 'never'], default='info',
|
|
132
|
+
help='Lowest severity that makes the exit code 1 (default: info = any change)')
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
136
|
+
default = default_path()
|
|
137
|
+
parser = argparse.ArgumentParser(
|
|
138
|
+
prog='infradrift',
|
|
139
|
+
description='Detect drift between a known-good baseline and the current state of a Linux server.',
|
|
140
|
+
epilog='exit codes: 0 no drift ยท 1 drift detected ยท 2 error',
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument('-v', '--version', action='version', version=f'infradrift {__version__}')
|
|
143
|
+
sub = parser.add_subparsers(dest='command', metavar='COMMAND')
|
|
144
|
+
|
|
145
|
+
p_snap = sub.add_parser('snapshot', help='Capture the current state as baseline')
|
|
146
|
+
p_snap.add_argument('--output', '-o', metavar='FILE', help=f'Baseline output path (default: {default})')
|
|
147
|
+
|
|
148
|
+
p_check = sub.add_parser('check', help='Compare the current state against the baseline')
|
|
149
|
+
p_check.add_argument('--baseline', '-b', metavar='FILE',
|
|
150
|
+
help=f'Baseline to compare against (default: {default})')
|
|
151
|
+
_add_report_options(p_check)
|
|
152
|
+
|
|
153
|
+
p_diff = sub.add_parser('diff', help='Compare two snapshot files')
|
|
154
|
+
p_diff.add_argument('snapshot_a', metavar='BASELINE')
|
|
155
|
+
p_diff.add_argument('snapshot_b', metavar='CURRENT')
|
|
156
|
+
_add_report_options(p_diff)
|
|
157
|
+
return parser
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main(argv=None) -> None:
|
|
161
|
+
parser = build_parser()
|
|
162
|
+
args = parser.parse_args(argv)
|
|
163
|
+
Color.auto()
|
|
164
|
+
|
|
165
|
+
if args.command is None:
|
|
166
|
+
parser.print_help()
|
|
167
|
+
sys.exit(EXIT_OK)
|
|
168
|
+
|
|
169
|
+
dispatch = {'snapshot': cmd_snapshot, 'check': cmd_check, 'diff': cmd_diff}
|
|
170
|
+
try:
|
|
171
|
+
sys.exit(dispatch[args.command](args))
|
|
172
|
+
except KeyboardInterrupt:
|
|
173
|
+
_err('Interrupted')
|
|
174
|
+
sys.exit(130)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == '__main__':
|
|
178
|
+
main()
|