narvy-cli 1.0.0__py3-none-any.whl

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.
Files changed (93) hide show
  1. narvy/__init__.py +3 -0
  2. narvy/android/__init__.py +0 -0
  3. narvy/android/source_analyzer.py +268 -0
  4. narvy/android/split_bundle.py +475 -0
  5. narvy/android_rule_context.py +196 -0
  6. narvy/apk_memory_preflight.py +248 -0
  7. narvy/auth.py +40 -0
  8. narvy/ci_templates/bitbucket.yml +25 -0
  9. narvy/ci_templates/github.yml +60 -0
  10. narvy/ci_templates/gitlab.yml +32 -0
  11. narvy/cloud/__init__.py +0 -0
  12. narvy/cloud/aws_scan.py +1188 -0
  13. narvy/comment_filter.py +134 -0
  14. narvy/crypto_taint_lite.py +82 -0
  15. narvy/decompiler.py +337 -0
  16. narvy/doctor.py +244 -0
  17. narvy/host/__init__.py +0 -0
  18. narvy/host/audit.py +157 -0
  19. narvy/host/host_knowledge.py +982 -0
  20. narvy/host/lynis_bootstrap.py +400 -0
  21. narvy/host/lynis_parser.py +358 -0
  22. narvy/host/narvy_checks.py +789 -0
  23. narvy/host/report.py +69 -0
  24. narvy/host/ssh_exec.py +219 -0
  25. narvy/ios/__init__.py +0 -0
  26. narvy/ios/binary_analyzer.py +1201 -0
  27. narvy/ios/plist_checks.py +249 -0
  28. narvy/ios/source_analyzer.py +301 -0
  29. narvy/ios/third_party_filter.py +242 -0
  30. narvy/ios/trust_all_context.py +66 -0
  31. narvy/main.py +1983 -0
  32. narvy/native_hardening.py +503 -0
  33. narvy/reporter.py +151 -0
  34. narvy/rule_engine.py +57 -0
  35. narvy/rules/android/config.yml +77 -0
  36. narvy/rules/android/crypto.yml +34 -0
  37. narvy/rules/android/secrets.yml +161 -0
  38. narvy/rules/android/storage.yml +24 -0
  39. narvy/rules/android/webview.yml +23 -0
  40. narvy/rules/android.yml +219 -0
  41. narvy/rules/ios/objc/crypto.yml +56 -0
  42. narvy/rules/ios/objc/network.yml +67 -0
  43. narvy/rules/ios/objc/secrets.yml +79 -0
  44. narvy/rules/ios/objc/storage.yml +45 -0
  45. narvy/rules/ios/objc/webview.yml +45 -0
  46. narvy/rules/ios_swift.yml +327 -0
  47. narvy/rules/web/go.yml +2837 -0
  48. narvy/rules/web/java.yml +1576 -0
  49. narvy/rules/web/javascript.yml +3683 -0
  50. narvy/rules/web/kotlin.yml +413 -0
  51. narvy/rules/web/local/csharp_narvy/config.yml +61 -0
  52. narvy/rules/web/local/csharp_narvy/crypto.yml +64 -0
  53. narvy/rules/web/local/csharp_narvy/deserialization.yml +59 -0
  54. narvy/rules/web/local/csharp_narvy/injection.yml +122 -0
  55. narvy/rules/web/local/csharp_narvy/xxe.yml +48 -0
  56. narvy/rules/web/local/java_narvy/auth_jwt.yml +134 -0
  57. narvy/rules/web/local/java_narvy/deserialization.yml +108 -0
  58. narvy/rules/web/local/java_narvy/mybatis.yml +39 -0
  59. narvy/rules/web/local/java_narvy/snakeyaml.yml +34 -0
  60. narvy/rules/web/local/java_narvy/spring_authz.yml +32 -0
  61. narvy/rules/web/local/java_narvy/spring_config.yml +67 -0
  62. narvy/rules/web/local/java_narvy/spring_hardening.yml +291 -0
  63. narvy/rules/web/local/java_narvy/sqli.yml +235 -0
  64. narvy/rules/web/local/java_narvy/xxe.yml +212 -0
  65. narvy/rules/web/php.yml +1644 -0
  66. narvy/rules/web/python.yml +3967 -0
  67. narvy/rules/web/ruby.yml +703 -0
  68. narvy/rules/web/rust.yml +258 -0
  69. narvy/rules/web/secrets.yml +1420 -0
  70. narvy/rules/web/secrets_supplement.yml +383 -0
  71. narvy/sca/__init__.py +1 -0
  72. narvy/sca/android_deps.py +349 -0
  73. narvy/sca/ios_deps.py +578 -0
  74. narvy/sca/osv_client.py +617 -0
  75. narvy/sca/web_deps.py +955 -0
  76. narvy/scope_config.py +195 -0
  77. narvy/semgrep_engine.py +219 -0
  78. narvy/stack_protector_evidence.py +96 -0
  79. narvy/third_party_filter.py +211 -0
  80. narvy/uploader.py +92 -0
  81. narvy/weak_prng_context.py +270 -0
  82. narvy/web/__init__.py +0 -0
  83. narvy/web/nuclei_binary.py +105 -0
  84. narvy/web/scan_blocklist.py +96 -0
  85. narvy/web/scanner.py +842 -0
  86. narvy/web/source_analyzer.py +714 -0
  87. narvy/web/ssrf_guard.py +374 -0
  88. narvy_cli-1.0.0.dist-info/METADATA +165 -0
  89. narvy_cli-1.0.0.dist-info/RECORD +93 -0
  90. narvy_cli-1.0.0.dist-info/WHEEL +5 -0
  91. narvy_cli-1.0.0.dist-info/entry_points.txt +2 -0
  92. narvy_cli-1.0.0.dist-info/licenses/LICENSE +202 -0
  93. narvy_cli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,982 @@
1
+ """Knowledge base for host and infrastructure audit findings.
2
+
3
+ Maps each hardening-engine test id to a control with a remediation and severity.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Dict, Optional
10
+
11
+ __all__ = ["HostControl", "HOST_CONTROLS", "lookup"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class HostControl:
16
+ control_id: str
17
+ title: str
18
+ description: str
19
+ remediation: str
20
+ cis: str
21
+ cwe: str
22
+ location: str
23
+ severity: str
24
+
25
+
26
+ HOST_CONTROLS: Dict[str, HostControl] = {
27
+ "GEN-0010": HostControl(
28
+ control_id="NARVY-HOST-OS-001",
29
+ title="Operating system release is end-of-life and no longer receives security updates",
30
+ description=(
31
+ "The installed distribution release has passed its support end date. "
32
+ "The vendor no longer publishes security patches for the kernel, "
33
+ "OpenSSL, systemd or any packaged service on this host. Every "
34
+ "vulnerability disclosed from the end-of-life date onward stays open "
35
+ "permanently, including remote pre-auth flaws, and no amount of "
36
+ "configuration hardening compensates for an unpatchable base system."
37
+ ),
38
+ remediation=(
39
+ "Upgrade to a currently supported release. Check the release and its "
40
+ "support window with `lsb_release -a` and `hwe-support-status "
41
+ "--verbose`. Plan an in-place upgrade to the next supported release "
42
+ "(`sudo apt update && sudo apt full-upgrade`, then `sudo do-release-upgrade`) "
43
+ "or, preferably for a production server, rebuild on a supported LTS "
44
+ "release and migrate. Prefer an LTS release so the next forced upgrade "
45
+ "is years away, not months. Until the upgrade lands, treat this host "
46
+ "as exposed: restrict inbound access to it at the network edge."
47
+ ),
48
+ cis="CIS 1.9 Patch Management",
49
+ cwe="CWE-1104",
50
+ location="/etc/os-release",
51
+ severity="high",
52
+ ),
53
+
54
+ "DBS-1820": HostControl(
55
+ control_id="NARVY-HOST-DB-001",
56
+ title="MongoDB accepts connections without authentication",
57
+ description=(
58
+ "The MongoDB instance runs with access control disabled: any client "
59
+ "that can open a TCP connection to the database port gets full "
60
+ "administrative access, with no credentials. It can read every "
61
+ "collection, modify or drop them, and on many builds read files or "
62
+ "run server-side JavaScript. If the port is reachable from outside "
63
+ "the host this is total data loss and total data disclosure. "
64
+ "Unauthenticated MongoDB instances are found and wiped by automated "
65
+ "ransom scanners within hours of being exposed."
66
+ ),
67
+ remediation=(
68
+ "1. Bind the listener to localhost or a private interface: set "
69
+ "`net.bindIp: 127.0.0.1` in /etc/mongod.conf. Never leave it at 0.0.0.0.\n"
70
+ "2. Create an admin user before enabling auth: connect with `mongosh`, "
71
+ "`use admin`, then `db.createUser({user:\"admin\", pwd:<strong>, "
72
+ "roles:[{role:\"userAdminAnyDatabase\", db:\"admin\"}]})`.\n"
73
+ "3. Enable access control: add `security.authorization: enabled` to "
74
+ "/etc/mongod.conf, then `sudo systemctl restart mongod`.\n"
75
+ "4. Create a least-privilege user per application database "
76
+ "(`readWrite` on that database only), and do not let apps use the admin user.\n"
77
+ "5. Verify: an unauthenticated `mongosh --eval 'db.adminCommand({listDatabases:1})'` "
78
+ "must now fail with `Unauthorized`.\n"
79
+ "6. Block the port at the firewall for everything except the "
80
+ "application hosts, and assume current data is compromised if the "
81
+ "port was ever publicly reachable: rotate secrets stored in it and "
82
+ "review the collections for tampering."
83
+ ),
84
+ cis="CIS 5.x Database Authentication",
85
+ cwe="CWE-306",
86
+ location="/etc/mongod.conf",
87
+ severity="high",
88
+ ),
89
+
90
+ "TIME-3185": HostControl(
91
+ control_id="NARVY-HOST-TIME-001",
92
+ title="System clock is not synchronized with a time source",
93
+ description=(
94
+ "The time synchronization service is running but reports that it has "
95
+ "not reached a synchronized state, so the clock is free-running. "
96
+ "Drift breaks certificate validity checks, time-based one-time "
97
+ "passwords, token and session expiry, and scheduled jobs. It also "
98
+ "makes logs from this host impossible to correlate with other hosts "
99
+ "during an incident, which is when correlation matters most. Not "
100
+ "directly exploitable, so this is a reliability and forensics issue "
101
+ "rather than an open door."
102
+ ),
103
+ remediation=(
104
+ "Check state with `timedatectl status` (expect `System clock "
105
+ "synchronized: yes` and `NTP service: active`). If it is not "
106
+ "synchronized: `sudo timedatectl set-ntp true`, confirm reachable "
107
+ "servers in /etc/systemd/timesyncd.conf (`NTP=` / `FallbackNTP=`), "
108
+ "then `sudo systemctl restart systemd-timesyncd` and re-check with "
109
+ "`timedatectl show-timesync --all`. Ensure outbound UDP/123 to the "
110
+ "configured time servers is allowed by the firewall, since a blocked "
111
+ "port is the usual cause. On hosts that need stricter accuracy, use "
112
+ "a full NTP daemon pointed at internal time servers."
113
+ ),
114
+ cis="CIS 2.1 Time Synchronization",
115
+ cwe="",
116
+ location="/etc/systemd/timesyncd.conf",
117
+ severity="low",
118
+ ),
119
+
120
+ "BOOT-5122": HostControl(
121
+ control_id="NARVY-HOST-BOOT-001",
122
+ title="Bootloader is not password protected",
123
+ description=(
124
+ "The bootloader has no password set, so anyone at the console or "
125
+ "with virtual-console access can edit the boot entry, append "
126
+ "`init=/bin/bash` and get a root shell without any credential, or "
127
+ "boot into single-user mode. This bypasses every OS-level access "
128
+ "control. It requires physical or console access, so on a hosted "
129
+ "virtual machine the practical risk is limited to whoever already "
130
+ "controls the hypervisor console."
131
+ ),
132
+ remediation=(
133
+ "Generate a hash with `grub-mkpasswd-pbkdf2`, then add to "
134
+ "/etc/grub.d/40_custom:\n"
135
+ " set superusers=\"root\"\n"
136
+ " password_pbkdf2 root grub.pbkdf2.sha512....\n"
137
+ "Apply with `sudo update-grub`. To keep unattended reboots working, "
138
+ "add `--unrestricted` to the default menu entry so booting needs no "
139
+ "password but editing entries does. Store the password in your secret "
140
+ "manager: a lost bootloader password turns a recovery boot into a "
141
+ "rescue-media job. Also restrict console/KVM access at the provider."
142
+ ),
143
+ cis="CIS 1.4.1 Bootloader Password",
144
+ cwe="CWE-284",
145
+ location="/etc/grub.d/40_custom",
146
+ severity="low",
147
+ ),
148
+ "BOOT-5180": HostControl(
149
+ control_id="NARVY-HOST-BOOT-002",
150
+ title="Services enabled at boot have not been reviewed",
151
+ description=(
152
+ "The set of services started automatically at boot has not been "
153
+ "reduced to what this host actually needs. Every enabled unit is code "
154
+ "that runs as root at startup, often opens a socket, and must be "
155
+ "patched forever. Unused daemons left enabled by a default install "
156
+ "are a recurring source of exposure. They get forgotten, so they "
157
+ "never get reviewed when a vulnerability is published."
158
+ ),
159
+ remediation=(
160
+ "List what starts at boot: `systemctl list-unit-files --type=service "
161
+ "--state=enabled`. For each unit, decide whether this host needs it. "
162
+ "Disable what it does not: `sudo systemctl disable --now <unit>`. If "
163
+ "the package itself is not needed, remove it (`sudo apt purge <pkg>`) "
164
+ "rather than only disabling the unit. Pay attention to anything "
165
+ "listening on a socket, and cross-check with `sudo ss -tulpn`. Re-run "
166
+ "this review whenever the host's role changes."
167
+ ),
168
+ cis="CIS 2.x Special Purpose Services",
169
+ cwe="CWE-1188",
170
+ location="systemd enabled units",
171
+ severity="low",
172
+ ),
173
+ "BOOT-5264": HostControl(
174
+ control_id="NARVY-HOST-BOOT-003",
175
+ title="Service units run without sandboxing options",
176
+ description=(
177
+ "Services on this host run with the default unit configuration, which "
178
+ "grants them far more of the system than they need: full filesystem "
179
+ "write access, all capabilities, and the ability to gain new "
180
+ "privileges. systemd can confine each service so that a compromise of "
181
+ "one daemon does not hand over the whole host. Without it, a single "
182
+ "remote-code-execution bug in any service escalates directly to root-level "
183
+ "reach across the filesystem."
184
+ ),
185
+ remediation=(
186
+ "For each exposed service, add a drop-in with `sudo systemctl edit "
187
+ "<unit>` and set the options the service can tolerate:\n"
188
+ " [Service]\n"
189
+ " NoNewPrivileges=yes\n"
190
+ " PrivateTmp=yes\n"
191
+ " ProtectSystem=strict\n"
192
+ " ProtectHome=yes\n"
193
+ " ProtectKernelTunables=yes\n"
194
+ " ProtectKernelModules=yes\n"
195
+ " ProtectControlGroups=yes\n"
196
+ " RestrictSUIDSGID=yes\n"
197
+ " ReadWritePaths=/var/lib/<service>\n"
198
+ "Then `sudo systemctl daemon-reload && sudo systemctl restart <unit>`. "
199
+ "Use `systemd-analyze security <unit>` to see the current exposure "
200
+ "score before and after, and test each service after tightening, since "
201
+ "`ProtectSystem=strict` breaks daemons that write outside their "
202
+ "declared paths."
203
+ ),
204
+ cis="CIS 2.x Special Purpose Services",
205
+ cwe="CWE-250",
206
+ location="/etc/systemd/system/<unit>.d/override.conf",
207
+ severity="low",
208
+ ),
209
+
210
+ "AUTH-9230": HostControl(
211
+ control_id="NARVY-HOST-AUTH-001",
212
+ title="Password hashing cost is left at the default",
213
+ description=(
214
+ "The number of hashing rounds used when a local password is set is "
215
+ "not configured, so the system default applies. The cost factor is "
216
+ "what makes an offline attack on a stolen /etc/shadow slow. A low "
217
+ "cost means a leaked shadow file can be brute-forced far faster. This "
218
+ "only matters once an attacker already has the hash file, which is "
219
+ "why it is defence-in-depth rather than an open door."
220
+ ),
221
+ remediation=(
222
+ "In /etc/login.defs set an explicit cost, for example:\n"
223
+ " SHA_CRYPT_MIN_ROUNDS 640000\n"
224
+ " SHA_CRYPT_MAX_ROUNDS 640000\n"
225
+ "Better, switch to yescrypt (the modern default on current Ubuntu) "
226
+ "and confirm `password ... pam_unix.so ... yescrypt` in "
227
+ "/etc/pam.d/common-password. Existing hashes are NOT re-hashed by "
228
+ "this change: they are upgraded only when each password is next "
229
+ "changed, so force a rotation for local accounts that still have a "
230
+ "password (`sudo passwd --expire <user>`). Verify the resulting "
231
+ "prefix in /etc/shadow (`$y$` for yescrypt, `$6$rounds=` for SHA-512)."
232
+ ),
233
+ cis="CIS 5.4.1 Password Hashing Algorithm",
234
+ cwe="CWE-916",
235
+ location="/etc/login.defs",
236
+ severity="low",
237
+ ),
238
+ "AUTH-9262": HostControl(
239
+ control_id="NARVY-HOST-AUTH-002",
240
+ title="No password strength enforcement on local accounts",
241
+ description=(
242
+ "No password quality module is active, so any local password is "
243
+ "accepted, including a single character, the username, or a known-breached "
244
+ "password. Nothing stops an administrator from setting a guessable "
245
+ "password on a root-capable account. This matters as soon as any "
246
+ "password-accepting path exists on the host (console, remote access "
247
+ "with password auth, or privilege escalation via sudo)."
248
+ ),
249
+ remediation=(
250
+ "Install the quality module: `sudo apt install libpam-pwquality`. In "
251
+ "/etc/security/pwquality.conf set:\n"
252
+ " minlen = 14\n"
253
+ " minclass = 3\n"
254
+ " dcredit = 0 / ucredit = 0 / lcredit = 0 / ocredit = 0\n"
255
+ " maxrepeat = 3\n"
256
+ " dictcheck = 1\n"
257
+ " enforce_for_root\n"
258
+ "Confirm /etc/pam.d/common-password contains a `pam_pwquality.so "
259
+ "retry=3` line before `pam_unix.so`. Prefer length over character-class "
260
+ "gymnastics, since long passphrases beat short complex ones. Test by "
261
+ "trying to set a weak password with `passwd` on a throwaway account; "
262
+ "it must be rejected."
263
+ ),
264
+ cis="CIS 5.4.1 Password Creation Requirements",
265
+ cwe="CWE-521",
266
+ location="/etc/security/pwquality.conf",
267
+ severity="medium",
268
+ ),
269
+ "AUTH-9282": HostControl(
270
+ control_id="NARVY-HOST-AUTH-003",
271
+ title="Password-protected accounts have no expiration date",
272
+ description=(
273
+ "One or more local accounts with a password have no account "
274
+ "expiration set. Accounts for contractors, temporary staff or "
275
+ "one-off migrations then stay valid forever, long after the person "
276
+ "or purpose is gone. Orphaned but still-valid credentials are a "
277
+ "classic quiet backdoor: nobody watches them, so nobody notices them "
278
+ "being used."
279
+ ),
280
+ remediation=(
281
+ "Review accounts and their dates: `sudo chage -l <user>`, and list "
282
+ "accounts with passwords via `sudo awk -F: '($2 !~ /^[*!]/) {print $1}' "
283
+ "/etc/shadow`. Set an expiry on temporary accounts: `sudo chage -E "
284
+ "$(date -d '+90 days' +%F) <user>`. Delete accounts that should no "
285
+ "longer exist (`sudo userdel -r <user>`), and lock service accounts "
286
+ "that must never log in (`sudo usermod -L -s /usr/sbin/nologin "
287
+ "<user>`). Set an inactivity lock as a safety net: `sudo useradd -D "
288
+ "-f 30` so a password stays usable for at most 30 days past expiry."
289
+ ),
290
+ cis="CIS 5.5.1 Account Expiration",
291
+ cwe="CWE-1104",
292
+ location="/etc/shadow",
293
+ severity="low",
294
+ ),
295
+ "AUTH-9286": HostControl(
296
+ control_id="NARVY-HOST-AUTH-004",
297
+ title="Password minimum and maximum age are not configured",
298
+ description=(
299
+ "Neither the minimum nor the maximum password age is set, so a local "
300
+ "password is valid indefinitely and can also be cycled instantly to "
301
+ "defeat password-history checks. A credential leaked years ago stays "
302
+ "valid; a user forced to change a password can loop back to the old "
303
+ "one in seconds. These two settings are the floor and the ceiling of "
304
+ "a password's lifetime and are normally configured together."
305
+ ),
306
+ remediation=(
307
+ "In /etc/login.defs set:\n"
308
+ " PASS_MIN_DAYS 1 # cannot re-cycle a password the same day\n"
309
+ " PASS_MAX_DAYS 365 # upper bound on credential lifetime\n"
310
+ " PASS_WARN_AGE 7\n"
311
+ "These apply to accounts created afterwards, so also fix existing "
312
+ "ones: `sudo chage -m 1 -M 365 -W 7 <user>` for each local account "
313
+ "with a password (audit with `sudo chage -l <user>`). Do not set an "
314
+ "aggressive rotation window: forced frequent rotation pushes users to "
315
+ "predictable variants and is no longer recommended practice. The "
316
+ "stronger control is removing interactive passwords entirely in "
317
+ "favour of key-based access."
318
+ ),
319
+ cis="CIS 5.5.1 Password Aging",
320
+ cwe="CWE-262",
321
+ location="/etc/login.defs",
322
+ severity="low",
323
+ ),
324
+ "AUTH-9328": HostControl(
325
+ control_id="NARVY-HOST-AUTH-005",
326
+ title="Default file-creation mask is too permissive",
327
+ description=(
328
+ "The default umask is not restrictive, so newly created files and "
329
+ "directories are readable by other local users by default. Anything "
330
+ "an application or administrator writes without setting explicit "
331
+ "permissions (config files, exports, logs, backups, temporary "
332
+ "dumps) is world-readable the moment it is created. This turns any "
333
+ "low-privilege local account or compromised service into a reader of "
334
+ "secrets it was never meant to see."
335
+ ),
336
+ remediation=(
337
+ "Set `UMASK 027` in /etc/login.defs (owner full, group read, others "
338
+ "nothing), and confirm `session optional pam_umask.so` is present in "
339
+ "/etc/pam.d/common-session so it is applied to login sessions. Also "
340
+ "set `umask 027` in /etc/profile and /etc/bash.bashrc to cover "
341
+ "non-PAM shells. Use `umask 077` on hosts where users must not read "
342
+ "each other's files at all. Verify with `umask` in a fresh login "
343
+ "session, where it must print 0027. Existing files keep their current "
344
+ "permissions: fix sensitive ones explicitly with `chmod`."
345
+ ),
346
+ cis="CIS 5.4.4 Default User Umask",
347
+ cwe="CWE-732",
348
+ location="/etc/login.defs",
349
+ severity="low",
350
+ ),
351
+
352
+ "FILE-6310": HostControl(
353
+ control_id="NARVY-HOST-FS-001",
354
+ title="/home and /var are not on separate partitions",
355
+ description=(
356
+ "/home and /var share the root filesystem. Two consequences: a "
357
+ "runaway log, upload or spool file under /var can fill the root "
358
+ "filesystem and stop the whole system, which is a trivially "
359
+ "triggerable denial of service on any host that accepts user data; "
360
+ "and user-writable areas cannot be given their own mount options "
361
+ "(nodev, nosuid, noexec), so an attacker who can write a file there "
362
+ "can also execute it or drop a setuid binary."
363
+ ),
364
+ remediation=(
365
+ "Check the current layout with `findmnt` / `lsblk`. On an existing "
366
+ "host, repartitioning means downtime, so weigh it against the value: "
367
+ "if a rebuild is planned, define separate /home, /var, /var/log, "
368
+ "/var/tmp and /tmp filesystems in the installer and mount user-writable "
369
+ "ones with `nodev,nosuid` (add `noexec` on /var/tmp, /tmp and /home "
370
+ "where applications tolerate it). Where repartitioning is not "
371
+ "practical, get most of the benefit now: enforce disk quotas or log "
372
+ "rotation limits on /var, and mount /tmp as tmpfs with "
373
+ "`nodev,nosuid,noexec` via `sudo systemctl enable --now tmp.mount`. "
374
+ "Monitor root filesystem usage with an alert well below 100%."
375
+ ),
376
+ cis="CIS 1.1.x Filesystem Partitions",
377
+ cwe="CWE-400",
378
+ location="/etc/fstab",
379
+ severity="low",
380
+ ),
381
+ "FILE-7524": HostControl(
382
+ control_id="NARVY-HOST-FS-002",
383
+ title="Permissions on sensitive files are wider than required",
384
+ description=(
385
+ "One or more system files carry permissions broader than they need. "
386
+ "On configuration and credential files this means a local account or "
387
+ "a compromised low-privilege service can read material it should not "
388
+ "(password hashes, keys, service credentials), or, if a file is "
389
+ "writable, alter behaviour that runs as root. File permissions are "
390
+ "the last barrier once an attacker has any foothold on the host."
391
+ ),
392
+ remediation=(
393
+ "Restore the expected ownership and modes on the core files:\n"
394
+ " sudo chown root:root /etc/passwd /etc/group && sudo chmod 644 /etc/passwd /etc/group\n"
395
+ " sudo chown root:shadow /etc/shadow /etc/gshadow && sudo chmod 640 /etc/shadow /etc/gshadow\n"
396
+ " sudo chmod 600 /etc/ssh/sshd_config && sudo chmod 600 /etc/ssh/ssh_host_*_key\n"
397
+ " sudo chmod 700 /root\n"
398
+ "Then hunt for the general cases: world-writable files (`sudo find / "
399
+ "-xdev -type f -perm -0002 -not -path '/proc/*'`), files with no "
400
+ "owner (`sudo find / -xdev -nouser -o -nogroup`), and unexpected "
401
+ "setuid/setgid binaries (`sudo find / -xdev -type f -perm /6000 -ls`) "
402
+ "-- review each and strip the bit where it is not needed. Check "
403
+ "application config holding secrets is 600 and owned by its service "
404
+ "user."
405
+ ),
406
+ cis="CIS 6.1.x File Permissions",
407
+ cwe="CWE-732",
408
+ location="filesystem permissions",
409
+ severity="low",
410
+ ),
411
+
412
+ "USB-1000": HostControl(
413
+ control_id="NARVY-HOST-USB-001",
414
+ title="USB storage support is enabled",
415
+ description=(
416
+ "The kernel will load USB mass-storage drivers, so plugging in a "
417
+ "device at the console mounts removable media. That is a path for "
418
+ "data exfiltration and for introducing files onto the host outside "
419
+ "any monitored channel. It requires physical access, so on a hosted "
420
+ "virtual machine the practical risk is close to zero; it matters on "
421
+ "bare metal, on-premise or kiosk-style servers."
422
+ ),
423
+ remediation=(
424
+ "Blacklist the driver: create /etc/modprobe.d/blacklist-usb-storage.conf "
425
+ "with:\n"
426
+ " install usb-storage /bin/true\n"
427
+ " blacklist usb-storage\n"
428
+ "Then `sudo update-initramfs -u` and reboot. Verify with `lsmod | "
429
+ "grep usb_storage` (no output) and by plugging a device in: it must "
430
+ "not appear. If some peripheral legitimately needs mass storage, "
431
+ "leave this as accepted risk and control it physically instead, and "
432
+ "record the decision rather than silently ignoring the finding."
433
+ ),
434
+ cis="CIS 1.1.x Removable Media",
435
+ cwe="CWE-1299",
436
+ location="/etc/modprobe.d/",
437
+ severity="low",
438
+ ),
439
+
440
+ "PKGS-7346": HostControl(
441
+ control_id="NARVY-HOST-PKG-001",
442
+ title="Removed packages still have configuration and files on disk",
443
+ description=(
444
+ "Packages that were removed but not purged left configuration files "
445
+ "behind, and older versions remain on disk. Leftover config can be "
446
+ "picked up if the package is ever reinstalled, and stale files "
447
+ "silently misrepresent what is installed: an inventory or "
448
+ "vulnerability scan reads them as present, or misses them. It is "
449
+ "housekeeping, not an exposure, but it is the housekeeping that keeps "
450
+ "patch inventories honest."
451
+ ),
452
+ remediation=(
453
+ "List leftovers: `dpkg -l | awk '/^rc/ {print $2}'`. Purge them: "
454
+ "`sudo apt purge $(dpkg -l | awk '/^rc/ {print $2}')`, but read the "
455
+ "list before confirming. Then `sudo apt autoremove --purge` to drop "
456
+ "orphaned dependencies and `sudo apt clean` to clear the package "
457
+ "cache. Check for old kernels left behind (`dpkg -l | grep "
458
+ "linux-image`) and remove all but the running one and one fallback. "
459
+ "Make purge the default habit: use `apt purge`, not `apt remove`."
460
+ ),
461
+ cis="CIS 1.9 Patch Management",
462
+ cwe="",
463
+ location="dpkg package state",
464
+ severity="low",
465
+ ),
466
+ "PKGS-7370": HostControl(
467
+ control_id="NARVY-HOST-PKG-002",
468
+ title="No verification of installed package file integrity",
469
+ description=(
470
+ "There is no way on this host to check installed files against the "
471
+ "checksums their packages shipped with. If a system binary is "
472
+ "replaced (by an attacker, or by a botched manual install), "
473
+ "nothing detects the change. Package checksum verification is the "
474
+ "cheapest available answer to 'is /usr/bin still what the "
475
+ "distribution shipped?'"
476
+ ),
477
+ remediation=(
478
+ "Install the package-checksum verification tool: `sudo apt install "
479
+ "debsums`. Verify the system with `sudo debsums -c` (lists files "
480
+ "whose checksum no longer matches; expect a few legitimately edited "
481
+ "config files) and `sudo debsums -a` for a full pass. Run it on a "
482
+ "schedule and alert on changes to binaries: `sudo debsums -c` in a "
483
+ "weekly cron or systemd timer, output piped to your alerting. "
484
+ "Investigate every unexpected binary mismatch as a possible "
485
+ "compromise. Pair with file integrity monitoring for coverage of "
486
+ "files that are not package-owned."
487
+ ),
488
+ cis="CIS 1.3.x File Integrity",
489
+ cwe="CWE-354",
490
+ location="package integrity tooling",
491
+ severity="low",
492
+ ),
493
+ "PKGS-7394": HostControl(
494
+ control_id="NARVY-HOST-PKG-003",
495
+ title="No tooling to report available package security updates",
496
+ description=(
497
+ "This host has nothing installed that reports which packages have "
498
+ "newer versions available and which of those come from the security "
499
+ "repository. Without it, 'are we patched?' can only be answered by "
500
+ "hand, so in practice it stops being answered. Missing security "
501
+ "updates is the single most common way a well-configured server gets "
502
+ "compromised."
503
+ ),
504
+ remediation=(
505
+ "Install the reporting tool: `sudo apt install apt-show-versions`, "
506
+ "then `sudo apt update && apt-show-versions -u` to list upgradable "
507
+ "packages. `apt list --upgradable` gives a quick equivalent, and "
508
+ "`/usr/lib/update-notifier/apt-check --human-readable` separates the "
509
+ "security updates from the rest. Then close the loop rather than just "
510
+ "reporting: enable automatic security patching with `sudo apt install "
511
+ "unattended-upgrades && sudo dpkg-reconfigure -plow unattended-upgrades`, "
512
+ "confirm the security origin is enabled in "
513
+ "/etc/apt/apt.conf.d/50unattended-upgrades, and monitor "
514
+ "/var/log/unattended-upgrades/ so silent failures do not go unnoticed."
515
+ ),
516
+ cis="CIS 1.9 Patch Management",
517
+ cwe="CWE-1104",
518
+ location="/etc/apt/apt.conf.d/50unattended-upgrades",
519
+ severity="low",
520
+ ),
521
+
522
+ "NETW-3200": HostControl(
523
+ control_id="NARVY-HOST-NET-001",
524
+ title="Uncommon network protocols are available to the kernel",
525
+ description=(
526
+ "Rarely used transport protocols (DCCP, SCTP, RDS, TIPC) can be "
527
+ "loaded by the kernel on this host. Almost no server uses them, so "
528
+ "their kernel code is comparatively lightly reviewed and has a track "
529
+ "record of memory-corruption bugs, several of which have been local "
530
+ "privilege-escalation vectors. A local process can trigger the module "
531
+ "load simply by creating a socket of that family, so leaving them "
532
+ "available is attack surface for no benefit."
533
+ ),
534
+ remediation=(
535
+ "Create /etc/modprobe.d/blacklist-rare-net.conf with:\n"
536
+ " install dccp /bin/true\n"
537
+ " install sctp /bin/true\n"
538
+ " install rds /bin/true\n"
539
+ " install tipc /bin/true\n"
540
+ "Then `sudo update-initramfs -u`. Unload any already loaded: `sudo "
541
+ "modprobe -r dccp sctp rds tipc`. Verify with `lsmod | grep -E "
542
+ "'dccp|sctp|rds|tipc'` (no output) and `modprobe -n -v sctp` (must "
543
+ "show /bin/true). Only exception: if an application genuinely uses "
544
+ "SCTP (some telecom stacks do), keep it and blacklist the rest."
545
+ ),
546
+ cis="CIS 3.4.x Uncommon Network Protocols",
547
+ cwe="CWE-1327",
548
+ location="/etc/modprobe.d/",
549
+ severity="low",
550
+ ),
551
+ "FIRE-4513": HostControl(
552
+ control_id="NARVY-HOST-FIRE-001",
553
+ title="Host firewall has no effective rules",
554
+ description=(
555
+ "The packet filter is present but its rule set is empty or unused, so "
556
+ "the host does not filter traffic itself: every port a service binds "
557
+ "to is reachable from anywhere the network allows. The host then "
558
+ "depends entirely on an upstream control (a cloud security group or "
559
+ "an edge firewall) for its perimeter. If that control is "
560
+ "misconfigured, changed, or simply absent, a service that was never "
561
+ "meant to be public becomes public with nothing to stop it. Default-deny "
562
+ "on the host is the layer that survives someone else's mistake."
563
+ ),
564
+ remediation=(
565
+ "Establish a default-deny inbound policy with an explicit allow list. "
566
+ "Simplest on Ubuntu:\n"
567
+ " sudo ufw default deny incoming\n"
568
+ " sudo ufw default allow outgoing\n"
569
+ " sudo ufw allow 22/tcp # do this BEFORE enabling, or you lock yourself out\n"
570
+ " sudo ufw allow 443/tcp\n"
571
+ " sudo ufw enable\n"
572
+ "Restrict management ports to known sources rather than the world: "
573
+ "`sudo ufw allow from <admin-net> to any port 22 proto tcp`. Never "
574
+ "expose database ports: they should be allowed only from application "
575
+ "hosts, or bound to localhost. Verify with `sudo ufw status numbered` "
576
+ "and confirm from outside the host with a port scan that only the "
577
+ "intended ports answer. Keep an out-of-band console session open the "
578
+ "first time you enable it."
579
+ ),
580
+ cis="CIS 3.5.x Firewall Configuration",
581
+ cwe="CWE-1327",
582
+ location="host firewall rules",
583
+ severity="medium",
584
+ ),
585
+
586
+ "HTTP-6710": HostControl(
587
+ control_id="NARVY-HOST-WEB-001",
588
+ title="Web server TLS configuration has not been hardened",
589
+ description=(
590
+ "The web server's TLS settings are at defaults and have not been "
591
+ "reviewed. Defaults commonly still permit legacy protocol versions "
592
+ "and weak cipher suites, omit HSTS so a first request can be "
593
+ "downgraded to plaintext, and leak the exact server version in "
594
+ "response headers, which tells an attacker which exploits to try. "
595
+ "None of this is exploitable on its own; together it makes "
596
+ "interception and targeting materially easier."
597
+ ),
598
+ remediation=(
599
+ "For nginx, in the server block (or a shared snippet):\n"
600
+ " ssl_protocols TLSv1.2 TLSv1.3;\n"
601
+ " ssl_prefer_server_ciphers off;\n"
602
+ " ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;\n"
603
+ " ssl_session_tickets off;\n"
604
+ " add_header Strict-Transport-Security \"max-age=63072000; includeSubDomains\" always;\n"
605
+ " server_tokens off;\n"
606
+ "and redirect port 80 to 443. For Apache: `SSLProtocol -all +TLSv1.2 "
607
+ "+TLSv1.3`, a matching `SSLCipherSuite`, `ServerTokens Prod`, "
608
+ "`ServerSignature Off`, and the same HSTS header. Test the config "
609
+ "(`sudo nginx -t` / `sudo apachectl configtest`), reload, then verify "
610
+ "the negotiated protocols from outside with `openssl s_client "
611
+ "-connect <host>:443 -tls1_1` (must fail). Automate certificate "
612
+ "renewal and alert before expiry."
613
+ ),
614
+ cis="CIS 2.2.x Web Server",
615
+ cwe="CWE-326",
616
+ location="web server TLS configuration",
617
+ severity="low",
618
+ ),
619
+
620
+ "SSH-7408": HostControl(
621
+ control_id="NARVY-HOST-SSH-001",
622
+ title="Remote shell service is running with unhardened settings",
623
+ description=(
624
+ "The remote shell daemon is at or near stock configuration. That "
625
+ "typically leaves password authentication enabled, which exposes "
626
+ "every account on the host to credential stuffing and brute force "
627
+ "from the internet-wide scanners that hit any open port 22 within "
628
+ "minutes, and allows direct root login, which removes the audit "
629
+ "trail of who did what and turns one guessed password into full "
630
+ "control. This is the most attacked service on a typical Linux "
631
+ "server, so its configuration carries more weight than any other."
632
+ ),
633
+ remediation=(
634
+ "Edit /etc/ssh/sshd_config (or a drop-in in /etc/ssh/sshd_config.d/) "
635
+ "and set:\n"
636
+ " PermitRootLogin no # log in as a user, then sudo\n"
637
+ " PasswordAuthentication no # keys only, kills brute force outright\n"
638
+ " KbdInteractiveAuthentication no # closes the keyboard-interactive password path\n"
639
+ " PermitEmptyPasswords no\n"
640
+ " MaxAuthTries 3\n"
641
+ " MaxSessions 4\n"
642
+ " X11Forwarding no\n"
643
+ " AllowTcpForwarding no # unless a tunnel is genuinely needed\n"
644
+ " ClientAliveInterval 300\n"
645
+ " ClientAliveCountMax 2\n"
646
+ " LoginGraceTime 30\n"
647
+ " AllowUsers <admin> [...] # explicit allow list\n"
648
+ "Before disabling password auth, confirm your key works in a SECOND "
649
+ "session, otherwise you lock yourself out. Validate the file with "
650
+ "`sudo sshd -t`, then `sudo systemctl reload ssh`. Confirm the result "
651
+ "with `sudo sshd -T | grep -Ei "
652
+ "'permitrootlogin|passwordauthentication|kbdinteractive|maxauthtries|x11forwarding'` "
653
+ "since the effective config is what counts, not the file. Restrict port "
654
+ "22 to known source addresses at the firewall as well."
655
+ ),
656
+ cis="CIS 5.2.x SSH Server Configuration",
657
+ cwe="CWE-16",
658
+ location="/etc/ssh/sshd_config",
659
+ severity="medium",
660
+ ),
661
+
662
+ "LOGG-2154": HostControl(
663
+ control_id="NARVY-HOST-LOG-001",
664
+ title="Logs are not shipped to a remote collector",
665
+ description=(
666
+ "All logs live only on this host. An attacker who gains root can "
667
+ "erase or edit them, so the record of the intrusion disappears with "
668
+ "the intrusion, and clearing logs is a standard step in any competent "
669
+ "compromise. Local-only logs are also lost outright if the host dies "
670
+ "or is rebuilt. Without an off-host copy, incident response has "
671
+ "nothing to reconstruct events from and no evidence that stands up."
672
+ ),
673
+ remediation=(
674
+ "Ship logs off the host in near real time. With rsyslog, create "
675
+ "/etc/rsyslog.d/50-remote.conf containing:\n"
676
+ " *.* @@<collector-host>:6514 # @@ = TCP; use TLS for anything crossing a network you do not control\n"
677
+ "then `sudo systemctl restart rsyslog`. Configure the receiver to "
678
+ "make the stored copy append-only, and keep retention on the "
679
+ "collector, not here. If the host uses the journal, forward with "
680
+ "`ForwardToSyslog=yes` in /etc/systemd/journald.conf, and set "
681
+ "`Storage=persistent` plus `SystemMaxUse=` so the local journal "
682
+ "survives reboots without filling the disk. Verify end to end: "
683
+ "`logger -t audit-test remote-log-check`, then confirm the line "
684
+ "arrives on the collector. Alert if a host stops sending, because silence "
685
+ "is a signal."
686
+ ),
687
+ cis="CIS 4.2.x Remote Logging",
688
+ cwe="CWE-778",
689
+ location="/etc/rsyslog.d/",
690
+ severity="low",
691
+ ),
692
+
693
+ "BANN-7126": HostControl(
694
+ control_id="NARVY-HOST-BANN-001",
695
+ title="Local login banner contains no legal notice",
696
+ description=(
697
+ "The console login banner has no authorized-use notice. This has no "
698
+ "technical effect (it stops nobody), but it has a legal one: in "
699
+ "several jurisdictions prosecuting unauthorized access is harder when "
700
+ "the system never stated that access was restricted. Default banners "
701
+ "also advertise the exact distribution and kernel version to anyone "
702
+ "at the login prompt. Compliance frameworks check for this."
703
+ ),
704
+ remediation=(
705
+ "Replace /etc/issue with a notice approved by whoever owns legal "
706
+ "wording for your company, along the lines of: 'Authorized users "
707
+ "only. All activity may be monitored and reported.' Do NOT include "
708
+ "the word 'welcome', the hostname, the OS version or the "
709
+ "organization's identity, since those help an attacker and weaken the "
710
+ "notice. Remove the escape sequences (\\S, \\r, \\m) that print "
711
+ "system version details. Set ownership: `sudo chown root:root "
712
+ "/etc/issue && sudo chmod 644 /etc/issue`. Note that some packages "
713
+ "regenerate this file on upgrade, so re-check after distribution "
714
+ "upgrades."
715
+ ),
716
+ cis="CIS 1.7.1 Command Line Warning Banners",
717
+ cwe="",
718
+ location="/etc/issue",
719
+ severity="low",
720
+ ),
721
+ "BANN-7130": HostControl(
722
+ control_id="NARVY-HOST-BANN-002",
723
+ title="Remote login banner contains no legal notice",
724
+ description=(
725
+ "The banner presented to remote connections has no authorized-use "
726
+ "notice. This is the one that faces the internet, so it is the one "
727
+ "that matters for stating that access is restricted before a "
728
+ "connection is authenticated. As shipped it also discloses the "
729
+ "distribution and version to any unauthenticated client that opens a "
730
+ "connection."
731
+ ),
732
+ remediation=(
733
+ "Put the same approved authorized-use notice in /etc/issue.net, with "
734
+ "no hostname, OS version or company identity, and no escape "
735
+ "sequences. Make the remote shell daemon actually serve it: set "
736
+ "`Banner /etc/issue.net` in /etc/ssh/sshd_config, validate with `sudo "
737
+ "sshd -t`, then `sudo systemctl reload ssh`. Set `sudo chown "
738
+ "root:root /etc/issue.net && sudo chmod 644 /etc/issue.net`. Verify "
739
+ "from another machine: `ssh -o PreferredAuthentications=none "
740
+ "<host>` must print the notice before any prompt."
741
+ ),
742
+ cis="CIS 1.7.2 Remote Login Warning Banner",
743
+ cwe="",
744
+ location="/etc/issue.net",
745
+ severity="low",
746
+ ),
747
+
748
+ "ACCT-9622": HostControl(
749
+ control_id="NARVY-HOST-ACCT-001",
750
+ title="Process accounting is disabled",
751
+ description=(
752
+ "The host does not record which commands were executed, by which "
753
+ "user, and when. During an investigation there is then no way to "
754
+ "reconstruct what an attacker ran after gaining access, or to prove "
755
+ "what an administrator did. Shell history is not a substitute: it is "
756
+ "user-writable, trivially cleared, and absent entirely for "
757
+ "non-interactive execution."
758
+ ),
759
+ remediation=(
760
+ "Install and enable process accounting: `sudo apt install acct && "
761
+ "sudo systemctl enable --now acct`. Read the records with `lastcomm` "
762
+ "(commands executed), `sa` (summaries) and `ac` (connect time). "
763
+ "Accounting files grow, so confirm rotation is in place for "
764
+ "/var/log/account/pacct and size the filesystem accordingly. On a "
765
+ "host that already has a full audit subsystem configured with "
766
+ "execve rules, that provides richer coverage and this is largely "
767
+ "redundant, so pick one and know which."
768
+ ),
769
+ cis="CIS 4.1.x Process Accounting",
770
+ cwe="CWE-778",
771
+ location="process accounting service",
772
+ severity="low",
773
+ ),
774
+ "ACCT-9628": HostControl(
775
+ control_id="NARVY-HOST-ACCT-002",
776
+ title="Audit subsystem is not collecting security events",
777
+ description=(
778
+ "The kernel audit subsystem is not running or has no rules loaded, so "
779
+ "security-relevant events are not recorded: privilege escalation, "
780
+ "changes to /etc/passwd and /etc/shadow, loaded kernel modules, "
781
+ "authentication file edits. These are exactly the events that reveal "
782
+ "a compromise, and they are the ones no other log source captures. "
783
+ "Their absence does not let an attacker in, but it means you will not "
784
+ "be able to tell what happened, or prove it did not."
785
+ ),
786
+ remediation=(
787
+ "Install and enable the audit daemon: `sudo apt install auditd "
788
+ "audispd-plugins && sudo systemctl enable --now auditd`. Add rules in "
789
+ "/etc/audit/rules.d/hardening.rules covering at minimum:\n"
790
+ " -w /etc/passwd -p wa -k identity\n"
791
+ " -w /etc/shadow -p wa -k identity\n"
792
+ " -w /etc/sudoers -p wa -k scope\n"
793
+ " -w /etc/sudoers.d/ -p wa -k scope\n"
794
+ " -w /var/log/sudo.log -p wa -k actions\n"
795
+ " -a always,exit -F arch=b64 -S execve -F euid=0 -k rootcmd\n"
796
+ " -e 2 # make the rules immutable until reboot\n"
797
+ "Load them with `sudo augenrules --load` and check with `sudo auditctl "
798
+ "-l`. Set `max_log_file_action = keep_logs` and size "
799
+ "/var/log/audit appropriately; the `-e 2` line must be last. Ship the "
800
+ "audit log off-host, since local-only audit records are erasable by the "
801
+ "attacker they are meant to catch. Query with `ausearch -k rootcmd`."
802
+ ),
803
+ cis="CIS 4.1.x System Auditing",
804
+ cwe="CWE-778",
805
+ location="/etc/audit/rules.d/",
806
+ severity="low",
807
+ ),
808
+
809
+ "FINT-4350": HostControl(
810
+ control_id="NARVY-HOST-FINT-001",
811
+ title="No file integrity monitoring in place",
812
+ description=(
813
+ "Nothing on this host detects unauthorized changes to system "
814
+ "binaries, configuration or startup files. Persistence after a "
815
+ "compromise almost always means writing to disk: a modified binary, "
816
+ "a new service unit, an added cron job, an extra authorized key. "
817
+ "Without a baseline to compare against, those changes stay invisible "
818
+ "indefinitely. This is a detection gap, not an open door: it does not "
819
+ "let an attacker in, it lets one stay."
820
+ ),
821
+ remediation=(
822
+ "Install an integrity checker and baseline it: `sudo apt install "
823
+ "aide aide-common`, then `sudo aideinit` (slow on first run), and "
824
+ "move the database into place with `sudo mv "
825
+ "/var/lib/aide/aide.db.new /var/lib/aide/aide.db`. Run checks on a "
826
+ "schedule (`sudo aide --check`) via a systemd timer and alert on "
827
+ "output. Two things decide whether this is useful: cover the paths "
828
+ "that matter in /etc/aide/aide.conf (/bin, /sbin, /usr/bin, /usr/sbin, "
829
+ "/etc, /boot, systemd unit directories, root's authorized_keys), and "
830
+ "keep the baseline database off-host or read-only, because an "
831
+ "attacker with root will otherwise just re-baseline it. Update the "
832
+ "baseline deliberately after every legitimate change, or the alerts "
833
+ "become noise and get ignored."
834
+ ),
835
+ cis="CIS 1.3.x Filesystem Integrity Checking",
836
+ cwe="CWE-354",
837
+ location="file integrity monitoring",
838
+ severity="low",
839
+ ),
840
+
841
+ "TOOL-5002": HostControl(
842
+ control_id="NARVY-HOST-TOOL-001",
843
+ title="Host is configured manually with no automation",
844
+ description=(
845
+ "No configuration management tooling is present, so this host's state "
846
+ "exists only as the accumulated result of manual changes. Nothing "
847
+ "declares what the configuration should be, so drift is undetectable, "
848
+ "a rebuild cannot reproduce it, and a hardening fix applied here does "
849
+ "not reach the next server. Security-wise the consequence is "
850
+ "practical: manually maintained hosts diverge, and the divergence is "
851
+ "where the unpatched, misconfigured one hides."
852
+ ),
853
+ remediation=(
854
+ "Put this host's configuration in version control and apply it with a "
855
+ "configuration management tool. Ansible is the lightest starting "
856
+ "point for a small estate since it needs nothing installed on the "
857
+ "target beyond a remote shell and Python. Start narrow rather than "
858
+ "boiling the ocean: codify the items in this report (firewall rules, "
859
+ "remote shell config, package baseline, audit rules) as a playbook, "
860
+ "run it in check mode (`ansible-playbook --check --diff`) against "
861
+ "this host to surface drift, then run it for real. Once it is "
862
+ "reproducible, run it on a schedule so drift is corrected rather than "
863
+ "merely reported. Treat the repository as production: review changes, "
864
+ "and keep secrets in a vault rather than in the playbook."
865
+ ),
866
+ cis="",
867
+ cwe="",
868
+ location="configuration management",
869
+ severity="low",
870
+ ),
871
+
872
+ "KRNL-6000": HostControl(
873
+ control_id="NARVY-HOST-KRNL-001",
874
+ title="Kernel parameters differ from a hardened baseline",
875
+ description=(
876
+ "Several kernel tunables are at defaults chosen for compatibility "
877
+ "rather than security. The gaps that matter in practice: kernel "
878
+ "pointers and dmesg readable by unprivileged users, which hands an "
879
+ "attacker the addresses needed to turn a memory bug into a working "
880
+ "local privilege escalation; ptrace unrestricted, letting any process "
881
+ "read another process of the same user, including its in-memory "
882
+ "secrets; core dumps from setuid binaries, which can write "
883
+ "credentials to disk; and IP redirects/source routing accepted, which "
884
+ "allows route manipulation."
885
+ ),
886
+ remediation=(
887
+ "Create /etc/sysctl.d/60-hardening.conf with:\n"
888
+ " kernel.kptr_restrict = 2\n"
889
+ " kernel.dmesg_restrict = 1\n"
890
+ " kernel.yama.ptrace_scope = 1\n"
891
+ " kernel.sysrq = 0\n"
892
+ " fs.suid_dumpable = 0\n"
893
+ " fs.protected_hardlinks = 1\n"
894
+ " fs.protected_symlinks = 1\n"
895
+ " net.ipv4.conf.all.accept_redirects = 0\n"
896
+ " net.ipv4.conf.all.send_redirects = 0\n"
897
+ " net.ipv4.conf.all.accept_source_route = 0\n"
898
+ " net.ipv4.conf.all.rp_filter = 1\n"
899
+ " net.ipv4.conf.all.log_martians = 1\n"
900
+ " net.ipv4.tcp_syncookies = 1\n"
901
+ " net.ipv6.conf.all.accept_redirects = 0\n"
902
+ " net.ipv6.conf.all.accept_ra = 0\n"
903
+ "Apply with `sudo sysctl --system` and verify each with `sysctl "
904
+ "<key>`. Two cautions: `ptrace_scope = 1` breaks debuggers and "
905
+ "profilers attaching to already-running processes, and "
906
+ "`accept_ra = 0` breaks IPv6 autoconfiguration, so skip those two if "
907
+ "the host needs them. Settings applied only with `sysctl -w` are lost "
908
+ "on reboot; the file is what makes them stick."
909
+ ),
910
+ cis="CIS 3.x Network and Kernel Parameters",
911
+ cwe="CWE-1188",
912
+ location="/etc/sysctl.d/",
913
+ severity="low",
914
+ ),
915
+
916
+ "HRDN-7222": HostControl(
917
+ control_id="NARVY-HOST-HRDN-001",
918
+ title="Compilers are installed and usable by any local user",
919
+ description=(
920
+ "Compilers are present on this production host and executable by "
921
+ "every local user. After gaining a foothold, a common next step is "
922
+ "compiling a local privilege-escalation exploit on the target, and "
923
+ "having a compiler already there removes the need to transfer a "
924
+ "binary in, which is the step most likely to be noticed or blocked. "
925
+ "A production server that only runs software built elsewhere has no "
926
+ "reason to be able to build anything."
927
+ ),
928
+ remediation=(
929
+ "Best option: remove the build toolchain from production entirely with "
930
+ "`sudo apt purge gcc g++ make build-essential cc` (verify nothing "
931
+ "depends on it first: some packages compile modules at install or "
932
+ "kernel-update time, so check with `apt -s purge` before committing). "
933
+ "Build artifacts belong in CI or a build host, not here. If a "
934
+ "compiler must stay, restrict it to root: `sudo chmod 750 "
935
+ "/usr/bin/gcc /usr/bin/cc /usr/bin/g++ /usr/bin/make` and `sudo chown "
936
+ "root:root` the same paths. Sweep for what is actually installed "
937
+ "first: `ls -l /usr/bin/{gcc*,cc,g++*,make,as,ld} 2>/dev/null`. Note "
938
+ "this raises the bar rather than closing the door, since an interpreter, "
939
+ "or a binary uploaded ready-made, achieves the same thing."
940
+ ),
941
+ cis="CIS 1.x Compiler Restriction",
942
+ cwe="CWE-250",
943
+ location="/usr/bin/gcc",
944
+ severity="low",
945
+ ),
946
+ "HRDN-7230": HostControl(
947
+ control_id="NARVY-HOST-HRDN-002",
948
+ title="No malware or rootkit detection installed",
949
+ description=(
950
+ "Nothing on this host scans for known malicious binaries, rootkits, "
951
+ "or the local artefacts they leave behind: hidden processes, "
952
+ "modified system binaries, suspicious startup entries. Detection here "
953
+ "is signature-based and will miss anything targeted, so it is not a "
954
+ "defence. It is a cheap catch for commodity, opportunistic "
955
+ "compromises, which is what actually hits an exposed Linux server."
956
+ ),
957
+ remediation=(
958
+ "Install a rootkit checker and run it on a schedule: `sudo apt "
959
+ "install rkhunter && sudo rkhunter --propupd && sudo rkhunter "
960
+ "--check --skip-keypress`, wired into a weekly systemd timer with "
961
+ "output alerted, not just written to a file nobody reads. Baseline it "
962
+ "(`--propupd`) only on a host you believe is clean, and re-baseline "
963
+ "after legitimate package updates or the report fills with false "
964
+ "positives and stops being read. Add `chkrootkit` for a second "
965
+ "opinion, and `clamav` if this host stores or relays user-supplied "
966
+ "files. Do not treat any of this as your primary control: patching, "
967
+ "the firewall and key-only remote access do far more."
968
+ ),
969
+ cis="",
970
+ cwe="",
971
+ location="malware detection tooling",
972
+ severity="low",
973
+ ),
974
+ }
975
+
976
+
977
+ def lookup(test_id: str) -> Optional[HostControl]:
978
+ """Control for an engine test id, or None. Callers must keep emitting the
979
+ raw finding for an unknown id rather than dropping it."""
980
+ if not test_id:
981
+ return None
982
+ return HOST_CONTROLS.get(test_id.strip().upper())