sshd-lint 1.4.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.
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ .pytest_cache/
6
+
7
+ # Python packaging
8
+ /dist/
9
+ /build/
10
+ *.egg-info/
11
+ __pycache__/
12
+ *.pyc
13
+ .venv/
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 capitan0n
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.
22
+
@@ -0,0 +1,366 @@
1
+ Metadata-Version: 2.4
2
+ Name: sshd-lint
3
+ Version: 1.4.0
4
+ Summary: Zero-dependency static analyzer for OpenSSH sshd_config files
5
+ Project-URL: Homepage, https://github.com/capitan0n/sshd-lint
6
+ Project-URL: Repository, https://github.com/capitan0n/sshd-lint
7
+ Project-URL: Issues, https://github.com/capitan0n/sshd-lint/issues
8
+ Author: capitan0n
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cis,devsecops,hardening,linter,security,ssh,sshd,static-analysis
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+
24
+ # sshd_lint
25
+
26
+ A zero-dependency static analyzer for OpenSSH `sshd_config` files. Audits configs
27
+ offline โ€” no root, no network, no live server needed.
28
+
29
+ `sshd_lint` evaluates your SSH server configuration against industry-standard security baselines, including the **CIS Benchmark for Linux**, **Mozilla OpenSSH Guidelines**, and **NIST SP 800-53**.
30
+
31
+ ---
32
+
33
+ ## ๐Ÿ’ก Motivation
34
+
35
+ This project was born out of a recurring practical need: quickly auditing an SSH server configuration whenever setting up a new test VM or reviewing a production host.
36
+
37
+ Existing tools either require root on the live system or an open network connection to the server. `sshd_lint` takes the opposite approach โ€” it reads the config file and nothing else, which means it works on a copy pulled from a machine you cannot log into, inside a container image, or in a CI pipeline where no SSH server is running at all.
38
+
39
+ ---
40
+
41
+ ## ๐Ÿค– AI-Assisted Project
42
+
43
+ This tool was conceptualized and developed with the assistance of Artificial Intelligence.
44
+ AI was used for code generation, logic refinement, and edge-case handling (such as cumulative directives and `Match` block scoping), under human direction and review.
45
+
46
+ ---
47
+
48
+ ## โœจ Features
49
+
50
+ - **Zero Dependencies** โ€” Built entirely on the Python standard library. No `pip install`.
51
+ - **Context-Aware Parsing** โ€” Understands OpenSSH semantics: `Match` blocks, `Match All` reset, directive shadowing, and `Include` glob expansion.
52
+ - **Scoped Match Block Findings** โ€” Distinguishes between global misconfigurations and risks that apply only to specific users, addresses, or groups.
53
+ - **Duplicate Directive Detection** โ€” Warns when a directive appears more than once globally, since sshd silently uses only the first occurrence.
54
+ - **Detailed, Actionable Reports** โ€” Explains what is wrong, why it matters, and references the relevant standard.
55
+ - **CI/CD Ready** โ€” Structured JSON output, and exit codes that separate a security verdict from an operational failure.
56
+ - **Version-Aware Rules** โ€” Adjusts expectations based on target OpenSSH version.
57
+
58
+ ---
59
+
60
+ ## ๐Ÿ“ฆ Requirements
61
+
62
+ - Python **3.9+**
63
+ - No external packages
64
+
65
+ ---
66
+
67
+ ## โš™๏ธ Installation
68
+
69
+ Since there are no external dependencies, you can run it directly:
70
+
71
+ ```bash
72
+ wget https://raw.githubusercontent.com/capitan0n/sshd-lint/main/sshd_lint.py -O sshd_lint
73
+ chmod +x sshd_lint
74
+ ./sshd_lint
75
+ ```
76
+
77
+ Or clone the repository:
78
+
79
+ ```bash
80
+ git clone https://github.com/capitan0n/sshd-lint.git
81
+ cd sshd-lint
82
+ python sshd_lint.py
83
+ ```
84
+
85
+ ---
86
+
87
+ ## ๐Ÿ“‹ Sample Output
88
+
89
+ ```
90
+ $ sshd_lint /etc/ssh/sshd_config --severity medium --compact
91
+
92
+ sshd_lint 1.4.0 โ€” /etc/ssh/sshd_config
93
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
94
+ Findings: 5 CRITICAL: 1 HIGH: 1 MEDIUM: 3
95
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
96
+
97
+ [CRITICAL] PermitRootLogin (line 2)
98
+ Current value : yes
99
+ Issue : Root login over SSH is permitted.
100
+
101
+ [HIGH] PasswordAuthentication (line 3)
102
+ Current value : yes
103
+ Issue : Password authentication is enabled.
104
+
105
+ [MEDIUM] MaxAuthTries (line 5)
106
+ Current value : 8
107
+ Issue : MaxAuthTries is 8 โ€” recommended โ‰ค 4.
108
+
109
+ [MEDIUM] AllowUsers / AllowGroups
110
+ Current value : <not set>
111
+ Issue : No user or group allowlist is defined.
112
+
113
+ [MEDIUM] X11Forwarding (line 4)
114
+ Current value : yes
115
+ Issue : X11 forwarding is enabled.
116
+ ```
117
+
118
+ Without `--compact`, each finding also carries a **Why it matters** explanation and the
119
+ standards it references. Colors are enabled automatically when writing to a terminal and
120
+ disabled when piping or redirecting.
121
+
122
+ ---
123
+
124
+ ## ๐Ÿš€ Usage
125
+
126
+ Analyze the default system SSH config:
127
+
128
+ ```bash
129
+ python sshd_lint.py
130
+ ```
131
+
132
+ Analyze a specific file:
133
+
134
+ ```bash
135
+ python sshd_lint.py /path/to/sshd_config
136
+ ```
137
+
138
+ Filter by severity (only HIGH and above):
139
+
140
+ ```bash
141
+ python sshd_lint.py --severity high
142
+ ```
143
+
144
+ Compact output (hides explanations โ€” useful for quick scans):
145
+
146
+ ```bash
147
+ python sshd_lint.py --compact
148
+ ```
149
+
150
+ JSON output for pipeline integration:
151
+
152
+ ```bash
153
+ python sshd_lint.py --format json
154
+ ```
155
+
156
+ The JSON output is self-contained โ€” `exit_code` and a per-severity `summary` are included at the top level so consumers don't need to capture `$?` separately:
157
+
158
+ ```json
159
+ {
160
+ "exit_code": 2,
161
+ "summary": {
162
+ "CRITICAL": 1,
163
+ "HIGH": 1,
164
+ "MEDIUM": 0,
165
+ "LOW": 0,
166
+ "INFO": 0
167
+ },
168
+ "findings": [
169
+ {
170
+ "severity": "HIGH",
171
+ "directive": "PasswordAuthentication",
172
+ "value": "yes",
173
+ "line": 3,
174
+ "scope": "global",
175
+ "message": "Password authentication is enabled.",
176
+ "detail": "Password authentication is vulnerable to brute-force and credential-stuffing attacks. Disable it and use public-key authentication exclusively: 'PasswordAuthentication no'.",
177
+ "references": ["CIS Benchmark for Linux", "Mozilla OpenSSH Guidelines"]
178
+ }
179
+ ]
180
+ }
181
+ ```
182
+
183
+ Note that `exit_code` in the JSON only ever carries the findings verdict (`0`, `1` or `2`).
184
+ Operational failures โ€” a bad flag or a missing config file โ€” produce no JSON at all, so a
185
+ document that parses is always a real report.
186
+
187
+ Filter with `jq`:
188
+
189
+ ```bash
190
+ # Only CRITICAL findings
191
+ python sshd_lint.py --format json | jq '.findings[] | select(.severity == "CRITICAL")'
192
+
193
+ # Summary only
194
+ python sshd_lint.py --format json | jq '.summary'
195
+
196
+ # Read exit code from JSON instead of $?
197
+ python sshd_lint.py --format json | jq '.exit_code'
198
+ ```
199
+
200
+ Audit a config copied from a remote server, resolving Includes from the live system:
201
+
202
+ ```bash
203
+ scp user@server:/etc/ssh/sshd_config /tmp/audit/sshd_config
204
+ python sshd_lint.py /tmp/audit/sshd_config --base-dir /etc/ssh
205
+ ```
206
+
207
+ ---
208
+
209
+ ## ๐Ÿงฐ CLI Flags
210
+
211
+ | Flag | Short | Description |
212
+ |-----------------------|-------|-------------|
213
+ | `config` | โ€” | Path to sshd_config (default: `/etc/ssh/sshd_config`) |
214
+ | `--severity` | `-s` | Minimum severity: `critical`, `high`, `medium`, `low`, `info` (default: `info`) |
215
+ | `--format` | `-f` | Output format: `text` or `json` (default: `text`) |
216
+ | `--compact` | `-c` | Hide explanations and references for cleaner output |
217
+ | `--no-color` | โ€” | Disable ANSI colors |
218
+ | `--openssh-version` | โ€” | Target OpenSSH version (e.g. `8.9`) for version-aware rule adjustments |
219
+ | `--base-dir` | โ€” | Base directory for `Include` resolution. Default: same directory as the config file |
220
+ | `--help` | `-h` | Show usage and exit |
221
+ | `--version` | `-V` | Show version and exit |
222
+
223
+ > `-V` is used for `--version` so that `-v` stays free for a future verbosity flag,
224
+ > following the common convention where `-v` means verbose.
225
+
226
+ ---
227
+
228
+ ## ๐Ÿšฆ Exit Codes
229
+
230
+ Exit codes fall into two groups. `0`โ€“`2` are the **security verdict**; `64` and `66`
231
+ signal that the tool could not run at all. Keeping them separate means a typo in a flag
232
+ can never be mistaken by a pipeline for a critical finding.
233
+
234
+ | Code | Meaning |
235
+ |------|---------|
236
+ | `0` | No findings at or above the requested severity threshold |
237
+ | `1` | Findings exist, but none are HIGH or CRITICAL |
238
+ | `2` | At least one HIGH or CRITICAL finding โ€” pipeline should fail |
239
+ | `64` | Usage error โ€” unrecognized flag or invalid argument (`EX_USAGE`) |
240
+ | `66` | Config file not found or unreadable (`EX_NOINPUT`) |
241
+
242
+ `64` and `66` follow the conventional values from BSD `sysexits.h`.
243
+
244
+ When using `--format json`, the verdict is also embedded in the JSON output as
245
+ `exit_code`, so the report is fully self-contained and readable by downstream tools
246
+ without capturing `$?`.
247
+
248
+ ### GitHub Actions
249
+
250
+ CI systems treat **any** non-zero exit code as failure, which would collapse the
251
+ distinction between `1` and `2`. Translate the verdict explicitly:
252
+
253
+ ```yaml
254
+ - name: Lint SSH config
255
+ run: |
256
+ code=0
257
+ python sshd_lint.py /etc/ssh/sshd_config --format json > report.json || code=$?
258
+ cat report.json
259
+ # Fail only on HIGH/CRITICAL. Exit 1 = minor findings, informational.
260
+ if [ "$code" -ge 2 ]; then
261
+ echo "::error::HIGH or CRITICAL findings in sshd_config"
262
+ exit 1
263
+ fi
264
+ ```
265
+
266
+ `|| code=$?` does two jobs: it captures the exit code, and it stops `bash -e` (the default
267
+ shell for `run:` steps) from aborting the script the moment the linter returns non-zero.
268
+ `cat` must come *after* the capture, since `$?` only holds the status of the most recent
269
+ command.
270
+
271
+ ---
272
+
273
+ ## ๐Ÿงช Rules Evaluated
274
+
275
+ ### Parse & File Handling
276
+ - Rule 00: Include resolution problems โ€” unreadable files, an Include glob matching
277
+ more than 500 files (refused outright), an Include glob matching 0 files, and lines
278
+ that couldn't be parsed as a directive
279
+
280
+ ### Authentication
281
+ - Rule 01: `PermitRootLogin`
282
+ - Rule 02: `PasswordAuthentication`
283
+ - Rule 03: `PermitEmptyPasswords`
284
+ - Rule 04: `ChallengeResponseAuthentication`
285
+ - Rule 05: `PubkeyAuthentication`
286
+ - Rule 06: `HostbasedAuthentication` / `IgnoreRhosts`
287
+
288
+ ### Access Control
289
+ - Rule 10: `LoginGraceTime`
290
+ - Rule 11: `MaxAuthTries`
291
+ - Rule 12: `MaxSessions`
292
+ - Rule 13: `MaxStartups`
293
+ - Rule 14: `AllowUsers` / `AllowGroups`
294
+
295
+ ### Forwarding & Tunneling
296
+ - Rule 20: `X11Forwarding`
297
+ - Rule 21: `AllowTcpForwarding`
298
+ - Rule 22: `AllowAgentForwarding`
299
+ - Rule 23: `GatewayPorts`
300
+ - Rule 24: `PermitTunnel`
301
+
302
+ ### Logging & Auditing
303
+ - Rule 30: `LogLevel`
304
+ - Rule 31: `PrintLastLog`
305
+
306
+ ### Cryptography
307
+ - Rule 40: Weak or deprecated Ciphers
308
+ - Rule 41: Weak or deprecated MACs
309
+ - Rule 42: Weak KexAlgorithms (key exchange)
310
+ - Rule 43: Deprecated HostKeyAlgorithms
311
+ - Rule 44: Deprecated `PubkeyAcceptedAlgorithms`
312
+
313
+ Rules 40-44 understand OpenSSH's `+`/`-`/`^` default-set syntax (e.g. `Ciphers +arcfour`
314
+ appends to the compiled-in default rather than replacing it) โ€” a weak algorithm is flagged
315
+ whether it fully replaces the list or is merely appended to it.
316
+
317
+ ### Miscellaneous
318
+ - Rule 50: `Banner`
319
+ - Rule 51: `StrictModes`
320
+ - Rule 52: `Port` (default port 22)
321
+ - Rule 53: `ClientAliveInterval` / idle session timeout
322
+ - Rule 54: `UseDNS`
323
+ - Rule 55: Insecure directives inside `Match` blocks (scoped risk)
324
+ - Rule 56: Duplicate global directives (shadowed by sshd)
325
+ - Rule 57: `PermitUserEnvironment`
326
+
327
+ ---
328
+
329
+ ## ๐Ÿ” How sshd_lint differs from similar tools
330
+
331
+ | Tool | How it works | Requires root / live system |
332
+ |------|--------------|-----------------------------|
333
+ | **Lynis** | Runs live on the system, audits many aspects | Yes |
334
+ | **ssh-audit** | Connects to a live SSH server, tests its responses | Yes (network access) |
335
+ | **sshd_lint** | Reads the config file statically, offline | No |
336
+
337
+ `sshd_lint` is designed for offline auditing, CI/CD pipelines, and reviewing configs from remote systems without needing access to the live server.
338
+
339
+ ---
340
+
341
+ ## โš ๏ธ Limitations
342
+
343
+ - **Static analysis only** โ€” does not connect to a live server or test actual behaviour.
344
+ - **Match block conditions are not evaluated** โ€” the condition string (e.g. `User anoncvs`, `Address 10.0.0.0/8`) is recorded and reported, but sshd_lint cannot determine whether it applies to a given connection.
345
+ - **Include resolution requires filesystem access** โ€” unreadable files and Include
346
+ globs matching over 500 files are reported as CRITICAL; a glob matching 0 files is
347
+ reported as INFO (often benign โ€” e.g. an empty `sshd_config.d/` โ€” but worth a glance).
348
+ - **Relative `Include` paths resolve against `--base-dir` / the config file's own
349
+ directory, not real sshd's hardcoded `/etc/ssh`.** For the common case of auditing
350
+ the live `/etc/ssh/sshd_config` these are identical. When auditing a copy of only the
351
+ main file (without its snippets alongside it), pass `--base-dir /etc/ssh` to resolve
352
+ includes the way sshd itself would.
353
+ - **Version-aware rules are currently minimal** โ€” the `--openssh-version` flag adjusts a small number of known defaults; more version-specific rules may be added in future releases.
354
+ - **Compiled-in defaults are for OpenSSH 8.x** โ€” behaviour on significantly older or newer versions may differ.
355
+
356
+ ---
357
+
358
+ ## Author
359
+
360
+ * **capitan0n** - [capitan0n](https://github.com/capitan0n)
361
+
362
+ ---
363
+
364
+ ## ๐Ÿ“œ License
365
+
366
+ MIT License