ec2-byhand 0.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.
- ec2_byhand-0.1.0/LICENSE +21 -0
- ec2_byhand-0.1.0/PKG-INFO +417 -0
- ec2_byhand-0.1.0/README.md +385 -0
- ec2_byhand-0.1.0/pyproject.toml +137 -0
- ec2_byhand-0.1.0/pyproject.toml.orig +105 -0
- ec2_byhand-0.1.0/src/diskread/__init__.py +23 -0
- ec2_byhand-0.1.0/src/diskread/blockdev.py +187 -0
- ec2_byhand-0.1.0/src/diskread/crc32c.py +72 -0
- ec2_byhand-0.1.0/src/diskread/errors.py +50 -0
- ec2_byhand-0.1.0/src/diskread/fs/__init__.py +328 -0
- ec2_byhand-0.1.0/src/diskread/fs/detect.py +75 -0
- ec2_byhand-0.1.0/src/diskread/fs/ext4/__init__.py +11 -0
- ec2_byhand-0.1.0/src/diskread/fs/ext4/blockmap.py +137 -0
- ec2_byhand-0.1.0/src/diskread/fs/ext4/csum.py +147 -0
- ec2_byhand-0.1.0/src/diskread/fs/ext4/fs.py +376 -0
- ec2_byhand-0.1.0/src/diskread/fs/ext4/inode.py +143 -0
- ec2_byhand-0.1.0/src/diskread/fs/ext4/superblock.py +284 -0
- ec2_byhand-0.1.0/src/diskread/fs/ext4/xattr.py +127 -0
- ec2_byhand-0.1.0/src/diskread/fs/extents.py +116 -0
- ec2_byhand-0.1.0/src/diskread/fs/fat/__init__.py +10 -0
- ec2_byhand-0.1.0/src/diskread/fs/fat/bpb.py +176 -0
- ec2_byhand-0.1.0/src/diskread/fs/fat/dirent.py +223 -0
- ec2_byhand-0.1.0/src/diskread/fs/fat/fs.py +283 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/__init__.py +11 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/attr.py +246 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/bmap.py +119 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/csum.py +32 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/dir.py +112 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/fs.py +322 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/inode.py +157 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/log.py +264 -0
- ec2_byhand-0.1.0/src/diskread/fs/xfs/superblock.py +215 -0
- ec2_byhand-0.1.0/src/diskread/lvm/__init__.py +27 -0
- ec2_byhand-0.1.0/src/diskread/lvm/config.py +158 -0
- ec2_byhand-0.1.0/src/diskread/lvm/label.py +208 -0
- ec2_byhand-0.1.0/src/diskread/lvm/vg.py +400 -0
- ec2_byhand-0.1.0/src/diskread/partition/__init__.py +114 -0
- ec2_byhand-0.1.0/src/diskread/partition/gpt.py +179 -0
- ec2_byhand-0.1.0/src/diskread/partition/mbr.py +142 -0
- ec2_byhand-0.1.0/src/diskread/py.typed +0 -0
- ec2_byhand-0.1.0/src/diskread/source.py +305 -0
- ec2_byhand-0.1.0/src/diskread/system.py +559 -0
- ec2_byhand-0.1.0/src/ec2_byhand/__init__.py +13 -0
- ec2_byhand-0.1.0/src/ec2_byhand/__main__.py +12 -0
- ec2_byhand-0.1.0/src/ec2_byhand/analyze.py +1918 -0
- ec2_byhand-0.1.0/src/ec2_byhand/archive.py +545 -0
- ec2_byhand-0.1.0/src/ec2_byhand/aws/__init__.py +13 -0
- ec2_byhand-0.1.0/src/ec2_byhand/aws/devices.py +64 -0
- ec2_byhand-0.1.0/src/ec2_byhand/aws/ebs.py +320 -0
- ec2_byhand-0.1.0/src/ec2_byhand/aws/snapshots.py +198 -0
- ec2_byhand-0.1.0/src/ec2_byhand/aws/upload.py +155 -0
- ec2_byhand-0.1.0/src/ec2_byhand/bolt.py +152 -0
- ec2_byhand-0.1.0/src/ec2_byhand/cli.py +920 -0
- ec2_byhand-0.1.0/src/ec2_byhand/cloudinit.py +352 -0
- ec2_byhand-0.1.0/src/ec2_byhand/containers.py +498 -0
- ec2_byhand-0.1.0/src/ec2_byhand/langpkgs.py +703 -0
- ec2_byhand-0.1.0/src/ec2_byhand/manifest.py +48 -0
- ec2_byhand-0.1.0/src/ec2_byhand/messages.py +640 -0
- ec2_byhand-0.1.0/src/ec2_byhand/noise.py +508 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/__init__.py +145 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/diff.py +94 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/dpkg.py +178 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/history.py +327 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/model.py +87 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/rpmdb.py +203 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/rpmheader.py +178 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/vercmp.py +195 -0
- ec2_byhand-0.1.0/src/ec2_byhand/packages/verify.py +496 -0
- ec2_byhand-0.1.0/src/ec2_byhand/presets.py +119 -0
- ec2_byhand-0.1.0/src/ec2_byhand/py.typed +0 -0
- ec2_byhand-0.1.0/src/ec2_byhand/report.py +265 -0
- ec2_byhand-0.1.0/src/ec2_byhand/secrets.py +204 -0
- ec2_byhand-0.1.0/src/ec2_byhand/sensitive.py +144 -0
- ec2_byhand-0.1.0/src/ec2_byhand/servicedirs.py +150 -0
- ec2_byhand-0.1.0/src/ec2_byhand/snaps.py +232 -0
ec2_byhand-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gyuro
|
|
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,417 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ec2-byhand
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extract hand-placed and hand-modified files from EC2 EBS snapshots without logging in
|
|
5
|
+
Keywords: aws,ec2,ebs,snapshot,configuration-drift,migration
|
|
6
|
+
Author: gyuro
|
|
7
|
+
Author-email: gyuro <344015443+gyuro-dev@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Natural Language :: Japanese
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Classifier: Topic :: System :: Filesystems
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Dist: boto3[crt]>=1.43.118
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
|
+
Project-URL: Homepage, https://github.com/gyuro-dev/ec2-byhand
|
|
28
|
+
Project-URL: Repository, https://github.com/gyuro-dev/ec2-byhand
|
|
29
|
+
Project-URL: Issues, https://github.com/gyuro-dev/ec2-byhand/issues
|
|
30
|
+
Project-URL: Changelog, https://github.com/gyuro-dev/ec2-byhand/blob/main/CHANGELOG.md
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# ec2-byhand
|
|
34
|
+
|
|
35
|
+
[日本語版 README](https://github.com/gyuro-dev/ec2-byhand/blob/main/README.ja.md)
|
|
36
|
+
|
|
37
|
+
A CLI tool that compares an EC2 instance (or its EBS snapshots) with the AMI it was launched from and writes out **a list of what was added or changed by hand since launch**, together with **the files themselves (as they are now, as they were, and patches of the differences)**.
|
|
38
|
+
|
|
39
|
+
No login to the server and no agent. It reads EBS snapshots directly, so a running production server carries no load.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install ec2-byhand
|
|
45
|
+
ec2-byhand --instance-id i-0123456789abcdef0 -o out/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That one command takes the snapshots, compares them with the launch AMI and deletes the snapshots it made. The result is in `out/`.
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
out/
|
|
52
|
+
├── report.txt the list, for people: files and packages added or changed, one a line
|
|
53
|
+
├── manifest.json the list, for programs: the same as JSON
|
|
54
|
+
├── after.tar.gz the actual files (now): what was added or edited by hand, with modes and owners
|
|
55
|
+
├── before.tar.gz the actual files (AMI): as they were before being edited or deleted
|
|
56
|
+
└── patches/ patches: what was changed in configuration files (for patch / git apply)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It needs Python 3.12 or later. Credentials and region come from the same configuration the AWS CLI uses ([Permissions](#permissions)).
|
|
60
|
+
|
|
61
|
+
## What it finds and extracts
|
|
62
|
+
|
|
63
|
+
What changes by itself (logs, caches: `/var/log`, `/tmp`, `/var/cache` and the like) is left out.
|
|
64
|
+
|
|
65
|
+
| What | For example | In the list | Files you get |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| **Files put there by hand** | `/opt/myapp/`, `/etc/myapp.conf`, scripts, systemd units, cron jobs | path, type, mode, hash, when it last changed | the file as it is now in after.tar.gz; for an edited one also before.tar.gz and patches/ |
|
|
68
|
+
| **Package files edited by hand** | `/etc/ssh/sshd_config`, `/etc/nginx/nginx.conf`, a command whose mode was changed | which package it belongs to and what changed (content, mode, owner) | now in after.tar.gz, as in the AMI in before.tar.gz, the difference in patches/ |
|
|
69
|
+
| **Deleted files** | files and directories the AMI had and the server no longer has | the path, and how many entries lay below it | the files before they were deleted, in before.tar.gz |
|
|
70
|
+
| **OS packages installed later** | `dnf install nginx`, `apt install jq` | packages added, removed and upgraded, their versions, the command that was run | (the list only) |
|
|
71
|
+
| **pip / npm packages** | `pip install`, `npm install` (global, or inside an app) | name and version, and files edited after the install | the edited files in after.tar.gz |
|
|
72
|
+
| **Services switched on or off** | `systemctl enable myapp`, `systemctl disable ...` | services enabled, disabled or masked (a package's doing is told from hand work) | units and drop-ins you wrote, in after.tar.gz |
|
|
73
|
+
| **Containers** | Docker / Podman / containerd | images, containers and volumes that came or went | (the list only; settings under `/etc/docker` are files like any other) |
|
|
74
|
+
|
|
75
|
+
- **Works on**: Amazon Linux 2 / 2023, the RHEL family (rpm), Ubuntu / Debian (dpkg). Filesystems: ext4, XFS, LVM, vfat.
|
|
76
|
+
- **Secrets**: private keys (`id_rsa`, `*.pem`), `/etc/shadow`, `.env` and the like are listed as `[sensitive]`, and by default their contents stay out of the archives. Passwords and tokens inside text files are replaced with `***MASKED***` (`--include-sensitive` turns that off).
|
|
77
|
+
|
|
78
|
+
### An example of the output
|
|
79
|
+
|
|
80
|
+
`out/report.txt` lists the changes it found, one a line (an excerpt):
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
hand-placed (17)
|
|
84
|
+
added 2026-10-24 13:10 /etc/myapp.conf
|
|
85
|
+
added 2026-10-24 13:10 /etc/systemd/system/myapp.service
|
|
86
|
+
modified 2026-10-24 13:10 /opt/legacy/change.txt [changed: sha256]
|
|
87
|
+
added 2026-10-24 13:10 /opt/myapp/bin/run.sh
|
|
88
|
+
added 2026-10-24 13:10 /root/.ssh/id_rsa [sensitive]
|
|
89
|
+
|
|
90
|
+
modified package files (10)
|
|
91
|
+
2026-10-24 13:11 /etc/profile (setup: content, size) [since baseline] [config]
|
|
92
|
+
2026-10-24 13:11 /usr/bin/alias (bash: mode) [since baseline]
|
|
93
|
+
|
|
94
|
+
packages (3)
|
|
95
|
+
added tree.x86_64 1.8.0-6.amzn2023.0.2 (dnf: install -q -y tree)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`added` is new since the AMI, `modified` has changed since; then comes when it last changed (UTC).
|
|
99
|
+
|
|
100
|
+
On the screen, a summary of the numbers (an example):
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
manifest: out/manifest.json
|
|
104
|
+
report: out/report.txt
|
|
105
|
+
baseline: ami-0123456789abcdef0 /dev/xvda=snap-0aaaa1111bbbb2222c
|
|
106
|
+
changed between: 2026-10-24 13:10 and 2026-10-24 13:11 UTC (last change of each)
|
|
107
|
+
hand-placed: 17
|
|
108
|
+
modified package files: 10 (6 since baseline)
|
|
109
|
+
deleted: 1
|
|
110
|
+
packages: 110 installed, +3 -0 ^0 v0
|
|
111
|
+
language packages: 5
|
|
112
|
+
services: 1
|
|
113
|
+
noise: 628
|
|
114
|
+
after.tar.gz: 22 entries
|
|
115
|
+
before.tar.gz: 8 entries
|
|
116
|
+
patches: 2
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| Line | Meaning |
|
|
120
|
+
|---|---|
|
|
121
|
+
| hand-placed | paths no package owns, added or changed since the AMI |
|
|
122
|
+
| modified package files | files that differ from what their package installed (in brackets: changed since the AMI) |
|
|
123
|
+
| deleted | hand-placed paths the AMI had and the server no longer has |
|
|
124
|
+
| packages | how many are installed, and since the AMI: added (+), removed (-), upgraded (^), downgraded (v) |
|
|
125
|
+
| noise | paths left out of the lists; see [What is reported and what is not](#scope) |
|
|
126
|
+
|
|
127
|
+
<a id="scope"></a>
|
|
128
|
+
|
|
129
|
+
## What is reported and what is not
|
|
130
|
+
|
|
131
|
+
**Reported**: what the table above lists. And what cloud-init wrote at first boot, what package scripts made, alternatives and snaps, each under its own heading.
|
|
132
|
+
|
|
133
|
+
**Left out by default (noise)**: what changes on any running server without anyone's hand. The summary gives the number; `noise` in the manifest gives it by rule.
|
|
134
|
+
|
|
135
|
+
| Rule | Examples |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Logs | `/var/log/**` (a directory made there by hand is reported), the systemd journal |
|
|
138
|
+
| Temporary and runtime files | `/tmp`, `/var/tmp`, `/var/run`, `/run` |
|
|
139
|
+
| Package managers' state and caches | `/var/lib/rpm`, `/var/lib/dpkg`, `/var/cache` |
|
|
140
|
+
| What the system regenerates | `/etc/machine-id`, `/etc/ld.so.cache`, `/etc/ssh/ssh_host_*`, `/var/lib/cloud`, `*.pyc` |
|
|
141
|
+
| Shell and editor history | `.bash_history`, `.viminfo`, `~/.cache` |
|
|
142
|
+
| What containers hold | `/var/lib/docker`, `/var/lib/containers` (listed under `containers` instead) |
|
|
143
|
+
| Agents' state | the SSM Agent's `/var/lib/amazon` |
|
|
144
|
+
|
|
145
|
+
`--include GLOB` reports a path whatever the rules say; `--exclude GLOB` adds to them.
|
|
146
|
+
Nothing a person may have changed by hand is noise: `/etc/hosts`, crontabs, `/etc/alternatives` are reported.
|
|
147
|
+
|
|
148
|
+
**Listed, but their contents left out**:
|
|
149
|
+
|
|
150
|
+
- Paths likely to hold secrets, and secret-like strings (see "Secrets" above). `*.key` and `.aws/credentials` are among the paths.
|
|
151
|
+
- Files over 1 GiB (`--max-file-size` changes that), and swap files.
|
|
152
|
+
|
|
153
|
+
The outputs hold copies of a server's files: please read [SECURITY.md](https://github.com/gyuro-dev/ec2-byhand/blob/main/SECURITY.md) too.
|
|
154
|
+
|
|
155
|
+
**Not looked at**:
|
|
156
|
+
|
|
157
|
+
- Volumes that /etc/fstab does not mount (a warning says so), and instance store volumes.
|
|
158
|
+
- The layers of container images.
|
|
159
|
+
|
|
160
|
+
## Usage
|
|
161
|
+
|
|
162
|
+
### The server now, and what to compare it with
|
|
163
|
+
|
|
164
|
+
You always give two things: **the server now**, and **what to compare it with (the baseline)**.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# An EC2 instance: snapshot it and compare with the AMI it was launched from
|
|
168
|
+
ec2-byhand --instance-id i-0123456789abcdef0 -o out/
|
|
169
|
+
|
|
170
|
+
# Snapshots that already exist
|
|
171
|
+
ec2-byhand --current-snapshot snap-aaa snap-bbb --baseline-snapshot snap-ccc -o out/
|
|
172
|
+
|
|
173
|
+
# Local raw disk images, one per EBS volume
|
|
174
|
+
ec2-byhand --current-image server-root.img server-data.img --baseline-image ami-root.img -o out/
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| | The server now | The baseline |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| An instance | `--instance-id` | its launch AMI, by default |
|
|
180
|
+
| An AMI | | `--baseline-ami` |
|
|
181
|
+
| Snapshots | `--current-snapshot` | `--baseline-snapshot` |
|
|
182
|
+
| Disk images | `--current-image` | `--baseline-image` |
|
|
183
|
+
| No comparison | | `--no-baseline` |
|
|
184
|
+
|
|
185
|
+
With `--no-baseline` the results include the files the AMI itself provided. `ec2-byhand --help` groups the options the same way.
|
|
186
|
+
|
|
187
|
+
### Main options
|
|
188
|
+
|
|
189
|
+
| Option | Does |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `--exclude GLOB` | leave more paths out. `'DIR/**'` is not even read, which saves time and API charges |
|
|
192
|
+
| `--include GLOB` | report a path whatever the noise rules say |
|
|
193
|
+
| `--max-file-size SIZE` | do not read larger files (default `1G`; `none` reads all) |
|
|
194
|
+
| `--sensitive GLOB` | treat more paths as secrets |
|
|
195
|
+
| `--include-sensitive` | archive the contents of secrets too, and leave secret-like strings unmasked |
|
|
196
|
+
| `--no-archives` | write manifest.json and report.txt only |
|
|
197
|
+
| `--upload s3://bucket/prefix` | also upload the outputs to S3 |
|
|
198
|
+
| `--keep-snapshots` | keep the snapshots the run made |
|
|
199
|
+
| `--profile` / `--region` | override the AWS configuration |
|
|
200
|
+
| `--parallel N` | how many snapshot blocks to fetch at once (default 32) |
|
|
201
|
+
| `--verify-checksums` | check the ext4 / XFS v5 metadata checksums |
|
|
202
|
+
|
|
203
|
+
Patterns match the whole absolute path (`'/opt/app/**'`, `'**/*.bak'`).
|
|
204
|
+
|
|
205
|
+
<a id="permissions"></a>
|
|
206
|
+
|
|
207
|
+
### Permissions
|
|
208
|
+
|
|
209
|
+
[docs/iam-policy.json](https://github.com/gyuro-dev/ec2-byhand/blob/main/docs/iam-policy.json) is the least a run needs (checked on AWS). It may delete only the snapshots it tagged itself.
|
|
210
|
+
Snapshots encrypted with the default `aws/ebs` key need nothing more; with a customer managed key, its key policy must allow `kms:Decrypt`.
|
|
211
|
+
For `--upload`, add [docs/iam-policy-upload.json](https://github.com/gyuro-dev/ec2-byhand/blob/main/docs/iam-policy-upload.json) (BUCKET and PREFIX are yours to fill in).
|
|
212
|
+
|
|
213
|
+
### Language of the messages
|
|
214
|
+
|
|
215
|
+
The messages, the summary and `--help` come in English and Japanese. `EC2_BYHAND_LANG=ja` (or `=en`) chooses; without it the locale does (`LC_ALL`, `LC_MESSAGES`, `LANG`, then the OS setting), and anything but Japanese gives English.
|
|
216
|
+
manifest.json is the same in both: it is data for programs and stays English, as does the text of a warning in the summary, which comes from it.
|
|
217
|
+
|
|
218
|
+
## Details
|
|
219
|
+
|
|
220
|
+
### The snapshots a run makes
|
|
221
|
+
|
|
222
|
+
The wait for them has no limit (the first snapshot of a large volume can take hours) and says how far they are every few minutes.
|
|
223
|
+
A run that is stopped (Ctrl+C, SIGTERM, a closed terminal or SSH session) deletes its snapshots on the way out.
|
|
224
|
+
One that was killed outright leaves them, tagged `ec2-byhand=<instance id>`: `aws ec2 describe-snapshots --owner-ids self --filters Name=tag-key,Values=ec2-byhand` lists them.
|
|
225
|
+
|
|
226
|
+
### Choosing the baseline
|
|
227
|
+
|
|
228
|
+
**A backup as the baseline**: an older snapshot of the same server (taken by hand, by DLM, ...) works as the baseline too:
|
|
229
|
+
`--instance-id i-... --baseline-snapshot snap-<backup>` lists what was done since that backup,
|
|
230
|
+
and `--current-snapshot snap-<newer> --baseline-snapshot snap-<older>` what was done between two backups.
|
|
231
|
+
What cloud-init did at first boot is in the backup as well, so it no longer shows.
|
|
232
|
+
|
|
233
|
+
**Public AMIs**: the EBS direct APIs do not read the snapshots behind a public AMI (AWS answers `Public snapshots are not supported`).
|
|
234
|
+
Copy the AMI into your account (`aws ec2 copy-image`) and pass the copy with `--baseline-ami`,
|
|
235
|
+
or use `--no-baseline`.
|
|
236
|
+
Red Hat's RHEL AMIs cannot be copied (`You do not have permission to access the storage of this ami`): use `--no-baseline` for them.
|
|
237
|
+
|
|
238
|
+
**dpkg (Ubuntu / Debian)**: dpkg records no permissions, owners or link targets, so a package file is compared with the baseline's copy only when its package has the same version on both sides.
|
|
239
|
+
For the other packages (all of them without a baseline) `package_files_to_review` lists the setuid, setgid and world-writable files for a person to read (sudo and friends are among them);
|
|
240
|
+
without a baseline a `dpkg-metadata-unverified` warning also says so.
|
|
241
|
+
It also lists, as `changed-since-boot`, package files whose ctime is later than both their package's install and the instance's first boot (from cloud-init's log), outside any apt run: a chmod or chown by hand leaves that trace.
|
|
242
|
+
|
|
243
|
+
### Data volumes
|
|
244
|
+
|
|
245
|
+
`--instance-id` snapshots every EBS volume; analyzed is what /etc/fstab mounts.
|
|
246
|
+
A filesystem nothing mounts gives an `unmounted-volume` warning, and a volume that cannot be opened (damaged LVM metadata, an LV of a kind that is not read) an `unreadable-volume` one.
|
|
247
|
+
fstab entries by `UUID=`, `LABEL=` and LVM names always resolve.
|
|
248
|
+
`/dev/xvdf` and `/dev/sdf` resolve where the device names are known: with `--instance-id` and `--baseline-ami`, and for snapshots given by id when they are of the instance's own volumes
|
|
249
|
+
(`--instance-id i-... --baseline-snapshot snap-<backup of root> snap-<backup of data>`; DescribeSnapshots tells their volumes).
|
|
250
|
+
`/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol...` resolves for any snapshot of the volume. Disk images, and snapshots without `--instance-id`, carry no device names: such entries are `unresolved-mount`.
|
|
251
|
+
The number in `/dev/nvme1n1` names no volume: when exactly one unmounted volume fits (filesystem type, partition number) it is mounted with an `inferred-mount` warning, otherwise the entry is `unresolved-mount`.
|
|
252
|
+
To skip a large volume you do not need, snapshot the root yourself and pass it with `--current-snapshot`.
|
|
253
|
+
|
|
254
|
+
### The outputs, more closely
|
|
255
|
+
|
|
256
|
+
report.txt holds nothing manifest.json does not.
|
|
257
|
+
Each item carries its `ctime`, when it last changed (the kernel sets it; `touch` cannot), and the summary says between when and when the hand work was done.
|
|
258
|
+
ACLs (`setfacl`), capabilities (`setcap`) and other extended attributes count as part of a path: one changed alone is reported (`changed: xattrs`), and `xattrs` names what a path carries; the archives keep the values.
|
|
259
|
+
The SELinux label is compared as well: a `chcon` shows as `changed: selinux`, with the label the path has now. When only one side has labels at all (SELinux switched on or off since), that is not counted as a change of each path.
|
|
260
|
+
This holds for hand-placed paths; the labels and attributes of package files are not compared (a package's own scripts set them).
|
|
261
|
+
The outputs hold copies of files that may be root-only on the server: on Linux and macOS only you can read them; on Windows, protect the folder yourself.
|
|
262
|
+
|
|
263
|
+
### Large files, large servers
|
|
264
|
+
|
|
265
|
+
Files over `--max-file-size` get no sha256 and stay out of the archives; against a baseline, size and mtime decide.
|
|
266
|
+
Package and pip files over the limit are not checked against their recorded hashes (`content-not-verified` warning). The summary counts what the limit left unread.
|
|
267
|
+
Before reading any content, the run says on stderr how much it is about to read in files no package owns; from 1 GiB on it names the largest ones:
|
|
268
|
+
the moment to Ctrl+C and `--exclude` a data directory (with `--instance-id` this also deletes the snapshots just made; `--keep-snapshots` keeps them for a `--current-snapshot` run).
|
|
269
|
+
|
|
270
|
+
Memory grows with the number of files on both sides, about 1.5 to 1.8 KB a path a side (measured: 300,000 files on each side take 0.9 GB on ext4 and 1.0 GB on XFS; a million on each side would need about 3 GB).
|
|
271
|
+
With millions of files, leave the data directories out with `--exclude 'DIR/**'`: they are then not walked at all (any `--include` makes the run read everything).
|
|
272
|
+
|
|
273
|
+
### Uploading to S3
|
|
274
|
+
|
|
275
|
+
`--upload s3://bucket/prefix` uploads the files the run wrote (report.txt among them, nothing else that `-o` holds),
|
|
276
|
+
to `prefix/<label>/<time>/` (the label is the instance or snapshot id, or `--label`; the time is UTC), so no run overwrites another.
|
|
277
|
+
Every object is encrypted with SSE-KMS (the aws/s3 key, or `--kms-key-id KEY`) and checked on arrival with SHA-256; large files go up in parts. manifest.json goes last.
|
|
278
|
+
The bucket is checked before the analysis starts. With a customer managed key (`--kms-key-id`), the key must also allow `kms:GenerateDataKey`, and `kms:Decrypt` for the files that go up in parts.
|
|
279
|
+
If the upload fails, the outputs stay in `-o` and the exit code is 1.
|
|
280
|
+
|
|
281
|
+
### Secret-like strings
|
|
282
|
+
|
|
283
|
+
Text files of up to 1 MiB that are reported are scanned for secret-like strings: private keys, AWS access keys, GitHub and Slack tokens, passwords in URLs,
|
|
284
|
+
and values given to `password`, `secret`, `token`, `api_key` and the like, written as `key = value`, `key: value`, `'key' => 'value'`, `define('KEY', 'value')` or `<key>value</key>`
|
|
285
|
+
(not references such as `${DB_PASSWORD}`, a path, or a call like `getenv(...)`).
|
|
286
|
+
A value that only a space separates from its key (redis' `requirepass`) is not found: name such files with `--sensitive`. The manifest says which kind on which line, never the value;
|
|
287
|
+
what was masked is listed in `archives.masked`. Swap files are never read: they hold memory, not configuration.
|
|
288
|
+
|
|
289
|
+
### npm
|
|
290
|
+
|
|
291
|
+
An app's `node_modules` and global packages are grouped under `language_packages`.
|
|
292
|
+
Each package is compared with its own tarball in the server's npm cache (`~/.npm/_cacache`), found by the integrity its lockfile records, or for a global install by the cache's index: `modified` and `missing` are then exact (`verified_by: npm-cache`).
|
|
293
|
+
Nothing is downloaded. Without the tarball (a node_modules copied from elsewhere, a cleaned cache), npm records no per-file hashes, so a file changed more than 10 minutes after the install
|
|
294
|
+
is listed in `edited_after_install` as a suspected hand edit and goes into after.tar.gz. That one is a guess from times: `npm install` after an edit moves the reference time and hides the edit.
|
|
295
|
+
|
|
296
|
+
### Containers
|
|
297
|
+
|
|
298
|
+
Docker (`/var/lib/docker`), Podman (`/var/lib/containers`, and each user's rootless `~/.local/share/containers`) and containerd (`/var/lib/containerd`)
|
|
299
|
+
keep their own state there, not edited by hand (the configuration is under `/etc/docker`, `/etc/containers`, `/etc/containerd`, which shows as usual).
|
|
300
|
+
Those files are neither listed nor archived, and the image layers are not even read; `containers` in the manifest lists, for each engine,
|
|
301
|
+
the images (name and id), the containers (name, image, creation time: not their environment, which often holds passwords) and the volumes (files and bytes) instead,
|
|
302
|
+
with a baseline only those added, removed or changed. A rootless Podman's items carry its `user`, containerd's their `namespace`
|
|
303
|
+
(Docker's own namespace `moby` is left to Docker).
|
|
304
|
+
|
|
305
|
+
### Services
|
|
306
|
+
|
|
307
|
+
Each link under `/etc/systemd/system` in `services` says whether a package made it (`by_package`): deb-systemd-helper recorded it (Debian, Ubuntu),
|
|
308
|
+
or it was created inside a dnf / yum / apt run, which is then named in `transaction`. `apt install nginx` enabling nginx is the package's doing; `systemctl enable`, `disable` and `mask` afterwards are not.
|
|
309
|
+
A link that went away is the package's doing only when the unit it pointed to went with it.
|
|
310
|
+
A service its package enabled and someone then disabled (`apt install apache2`, then `systemctl disable apache2`) is listed as `disabled`: a new server would have it enabled.
|
|
311
|
+
Debian and Ubuntu record what a package enabled; for rpm it is worked out from the presets and the unit's `[Install]`, for packages installed since the baseline.
|
|
312
|
+
|
|
313
|
+
### Snaps
|
|
314
|
+
|
|
315
|
+
Ubuntu's snaps are listed under `snaps`, read from snapd's own state (`/var/lib/snapd/state.json`): name, type, revision, channel, whether the image came with it (`seeded`),
|
|
316
|
+
and for one installed by hand, the snapd change that did it (`via`). With a baseline only those added, removed or refreshed.
|
|
317
|
+
What comes with a snap (its `.snap` file, `/snap/<name>`, the units snapd writes, the bare `/var/snap/<name>` directories) is not listed as hand work; what was put in `/var/snap/<name>` is.
|
|
318
|
+
|
|
319
|
+
### cloud-init
|
|
320
|
+
|
|
321
|
+
An AMI's snapshot is taken before its first boot, so what cloud-init writes then (the default user's home, `sudoers.d`, `authorized_keys`, the hostname, the network configuration)
|
|
322
|
+
would show up as hand work on every instance. Its own log, `/var/log/cloud-init.log`, names each file it writes and each user it adds, with the time.
|
|
323
|
+
A file whose ctime falls inside such a write is listed under `cloud_init` in the manifest, by module, instead of `hand_placed`. It is still archived:
|
|
324
|
+
user-data's `write_files` is your own configuration. A package file cloud-init rewrote stays in `modified_package_files`, marked with `cloud_init`.
|
|
325
|
+
Anything changed afterwards, by hand or by a `runcmd` script, has a later ctime and stays hand work (a key appended to `authorized_keys`, a file put in the home).
|
|
326
|
+
If cloud-init ran but its log is gone, nothing is moved and the warning `cloud-init-log-missing` says so.
|
|
327
|
+
|
|
328
|
+
## How it works
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
hand-placed / hand-modified files
|
|
332
|
+
= files in the current snapshot
|
|
333
|
+
− files unchanged since the AMI
|
|
334
|
+
− files owned by a package (rpm/dpkg) whose hash still matches
|
|
335
|
+
− auto-generated noise (logs, caches, ...)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
- Disks are read block by block through the **EBS direct APIs**; nothing is mounted
|
|
339
|
+
- ext4, XFS and LVM are **parsed in user space**
|
|
340
|
+
- Package databases are read from the snapshot too; no rpm command runs on the server
|
|
341
|
+
- Even without the AMI, most files are told by "owned by no package, or hash mismatch"
|
|
342
|
+
|
|
343
|
+
## From Python
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from diskread import FileDevice
|
|
347
|
+
from ec2_byhand import manifest
|
|
348
|
+
from ec2_byhand.analyze import analyze
|
|
349
|
+
from ec2_byhand.cli import open_disks
|
|
350
|
+
|
|
351
|
+
with FileDevice("current.img") as cur, FileDevice("ami.img") as ami:
|
|
352
|
+
m = analyze(open_disks({"disk0": cur}, "current"), open_disks({"disk0": ami}, "ami"))
|
|
353
|
+
manifest.write(m, "manifest.json")
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`open_disks` resolves partitions, LVM and fstab. A server with several disks: list them all (`{"disk0": root, "disk1": data}`).
|
|
357
|
+
|
|
358
|
+
## Development
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
uv sync
|
|
362
|
+
uv run pytest
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Everything CI checks (ruff, mypy, and the tests under each supported Python), in one go:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
bash scripts/check.sh
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`bash scripts/check.sh 3.13` checks only the versions named. The results are kept in reports/check/ (summary.txt first).
|
|
372
|
+
|
|
373
|
+
The CLI tests are YAML cases ([tests/casebook_cli.yaml](https://github.com/gyuro-dev/ec2-byhand/blob/main/tests/casebook_cli.yaml) and
|
|
374
|
+
[tests/casebook_verify.yaml](https://github.com/gyuro-dev/ec2-byhand/blob/main/tests/casebook_verify.yaml), run with [pytest-casebook](https://pypi.org/project/pytest-casebook/)).
|
|
375
|
+
`uv run pytest --casebook-html=reports/casebook.html` renders the test specification (an HTML table).
|
|
376
|
+
After changing an expectation, `uv run pytest --casebook-update` rebuilds the goldens (tests/golden/casebook_*/); review the diff.
|
|
377
|
+
|
|
378
|
+
The EBS direct API path runs end to end against [moto](https://github.com/getmoto/moto), no account needed:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
uv run --group integration pytest tests/test_integration_moto.py
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
It uploads a golden image as a snapshot and compares the output with a run on the image file.
|
|
385
|
+
moto's blocks are 512 bytes (EBS: 512 KiB), so it takes about a minute; without the `integration` group it is skipped.
|
|
386
|
+
Expiring block tokens, paging, throttling and encrypted snapshots are not reproduced: those need an account.
|
|
387
|
+
|
|
388
|
+
The golden images are built in WSL as root (packages: `gdisk` `lvm2` `xfsprogs` `acl` `dosfstools`);
|
|
389
|
+
some XFS cases (AL2023 / Fedora mkfs defaults) need Docker. They land in `tests/golden/out/`, which is not committed.
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
wsl -u root -- bash tools/golden/build_partition.sh tests/golden/out
|
|
393
|
+
wsl -u root -- bash tools/golden/build_lvm.sh tests/golden/out
|
|
394
|
+
wsl -u root -- bash tools/golden/build_fs.sh tests/golden/out
|
|
395
|
+
wsl -u root -- bash tools/golden/build_dirty.sh tests/golden/out
|
|
396
|
+
wsl -u root -- bash tools/golden/build_system.sh tests/golden/out
|
|
397
|
+
wsl -u root -- bash tools/golden/build_vfat.sh tests/golden/out
|
|
398
|
+
# Package DB and version-comparison goldens (Docker)
|
|
399
|
+
wsl -u root -- bash tools/golden/build_pkgdb.sh tests/golden/out
|
|
400
|
+
wsl -u root -- bash tools/golden/build_vercmp.sh tests/golden/out
|
|
401
|
+
wsl -u root -- bash tools/golden/build_verify.sh tests/golden/out
|
|
402
|
+
# XFS v4 cannot be mounted by the WSL kernel, so it is built in an AlmaLinux 8 VM
|
|
403
|
+
# (requires qemu-system-x86 qemu-utils cloud-image-utils)
|
|
404
|
+
wsl -u root -- bash tools/golden/fetch_almalinux8.sh
|
|
405
|
+
wsl -u root -- bash tools/golden/vm_almalinux8.sh tests/golden/out xfs-v4 xfs-v4-noftype xfs-v4-1k
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Without the images, the golden tests are skipped.
|
|
409
|
+
|
|
410
|
+
## Documentation
|
|
411
|
+
|
|
412
|
+
- [SECURITY.md](https://github.com/gyuro-dev/ec2-byhand/blob/main/SECURITY.md) — how to report a vulnerability, and what to keep in mind about the outputs
|
|
413
|
+
- [CHANGELOG.md](https://github.com/gyuro-dev/ec2-byhand/blob/main/CHANGELOG.md) — what changed in each release
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
MIT. See [LICENSE](https://github.com/gyuro-dev/ec2-byhand/blob/main/LICENSE).
|