umbermark-cli 1.0.0rc1__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. umbermark/__init__.py +3 -0
  2. umbermark/checks/__init__.py +88 -0
  3. umbermark/checks/auto_updates_configured.py +149 -0
  4. umbermark/checks/dns_resolver_sanity.py +47 -0
  5. umbermark/checks/fail2ban_enabled.py +67 -0
  6. umbermark/checks/failed_login_anomaly.py +48 -0
  7. umbermark/checks/failed_systemd_units.py +41 -0
  8. umbermark/checks/firewall_protective.py +247 -0
  9. umbermark/checks/journald_persistence.py +55 -0
  10. umbermark/checks/kernel_age.py +74 -0
  11. umbermark/checks/listen_vs_firewall_delta.py +136 -0
  12. umbermark/checks/mac_framework_enforcing.py +97 -0
  13. umbermark/checks/pending_security_updates.py +188 -0
  14. umbermark/checks/public_listen_anomalies.py +200 -0
  15. umbermark/checks/reboot_pending.py +71 -0
  16. umbermark/checks/repo_gpg_trust.py +97 -0
  17. umbermark/checks/root_disk_free.py +46 -0
  18. umbermark/checks/ssh_non_default_port.py +62 -0
  19. umbermark/checks/ssh_password_auth.py +71 -0
  20. umbermark/checks/ssh_permit_root.py +59 -0
  21. umbermark/checks/ssh_weak_ciphers.py +85 -0
  22. umbermark/checks/sudo_requires_authentication.py +41 -0
  23. umbermark/checks/sudoers_nopasswd.py +111 -0
  24. umbermark/checks/swap_pressure.py +51 -0
  25. umbermark/checks/time_sync.py +55 -0
  26. umbermark/checks/tmp_hardening.py +45 -0
  27. umbermark/checks/user_password_aging.py +95 -0
  28. umbermark/checks/world_writable_suid_audit.py +171 -0
  29. umbermark/cli/__init__.py +4 -0
  30. umbermark/cli/app.py +23 -0
  31. umbermark/cli/auth.py +44 -0
  32. umbermark/cli/commands/__init__.py +0 -0
  33. umbermark/cli/commands/doctor.py +16 -0
  34. umbermark/cli/commands/login.py +80 -0
  35. umbermark/cli/commands/push.py +54 -0
  36. umbermark/cli/commands/report.py +26 -0
  37. umbermark/cli/commands/scan.py +77 -0
  38. umbermark/cli/commands/scan_local.py +72 -0
  39. umbermark/cli/commands/scan_wsl.py +35 -0
  40. umbermark/cli/commands/version.py +11 -0
  41. umbermark/cli/config.py +13 -0
  42. umbermark/core/__init__.py +0 -0
  43. umbermark/core/accepted.py +61 -0
  44. umbermark/core/access.py +896 -0
  45. umbermark/core/capture.py +228 -0
  46. umbermark/core/check.py +82 -0
  47. umbermark/core/checkin.py +89 -0
  48. umbermark/core/crypto.py +15 -0
  49. umbermark/core/duration.py +45 -0
  50. umbermark/core/engine.py +74 -0
  51. umbermark/core/external/__init__.py +6 -0
  52. umbermark/core/external/check.py +54 -0
  53. umbermark/core/external/checks/__init__.py +31 -0
  54. umbermark/core/external/checks/exposed_datastores.py +55 -0
  55. umbermark/core/external/checks/http_security_headers.py +249 -0
  56. umbermark/core/external/checks/legacy_services.py +52 -0
  57. umbermark/core/external/checks/open_management_ports.py +65 -0
  58. umbermark/core/external/checks/ssh_alt_ports.py +77 -0
  59. umbermark/core/external/checks/ssh_weak_algorithms.py +93 -0
  60. umbermark/core/external/checks/tls_hygiene.py +107 -0
  61. umbermark/core/external/engine.py +89 -0
  62. umbermark/core/external/ownership.py +607 -0
  63. umbermark/core/external/probe.py +103 -0
  64. umbermark/core/external/socket_probe.py +400 -0
  65. umbermark/core/firewall.py +713 -0
  66. umbermark/core/fleet.py +92 -0
  67. umbermark/core/history.py +235 -0
  68. umbermark/core/html_report.py +306 -0
  69. umbermark/core/keyenc.py +42 -0
  70. umbermark/core/labels.py +29 -0
  71. umbermark/core/lifecycle.py +100 -0
  72. umbermark/core/logredact.py +44 -0
  73. umbermark/core/partial.py +57 -0
  74. umbermark/core/pdf/__init__.py +0 -0
  75. umbermark/core/pdf/renderer.py +26 -0
  76. umbermark/core/pdf/weasyprint_renderer.py +18 -0
  77. umbermark/core/platform.py +73 -0
  78. umbermark/core/redact.py +75 -0
  79. umbermark/core/report.py +25 -0
  80. umbermark/core/scoring.py +105 -0
  81. umbermark/core/settings.py +238 -0
  82. umbermark/core/short_id.py +27 -0
  83. umbermark/core/signing.py +24 -0
  84. umbermark/core/ssh.py +156 -0
  85. umbermark/core/ssh_fingerprint.py +16 -0
  86. umbermark/core/tiers.py +128 -0
  87. umbermark/core/webhooks.py +177 -0
  88. umbermark/core/wording.py +371 -0
  89. umbermark_cli-1.0.0rc1.dist-info/METADATA +11 -0
  90. umbermark_cli-1.0.0rc1.dist-info/RECORD +93 -0
  91. umbermark_cli-1.0.0rc1.dist-info/WHEEL +5 -0
  92. umbermark_cli-1.0.0rc1.dist-info/entry_points.txt +2 -0
  93. umbermark_cli-1.0.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,61 @@
1
+ """Accepted risks: a finding the owner has decided to live with, with a reason and an expiry (UMBR-66).
2
+
3
+ The stored scan is never rewritten. The raw score stays what the scanner measured; the "score with accepted risks"
4
+ is computed at read time from the scan's own deductions, leaving out the points of every check that has an active
5
+ (unexpired) acceptance on that server. When an acceptance expires it simply stops applying.
6
+
7
+ Only the owner's pages show it. A public /r share link keeps the raw score and says nothing about acceptances
8
+ (their reasons may describe internal decisions).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import datetime as dt
13
+ import re
14
+ from dataclasses import dataclass
15
+
16
+ from umbermark.core.scoring import _band
17
+
18
+ CHECK_ID_RE = re.compile(r"^[a-z0-9_]{1,64}$")
19
+ REASON_MAX = 500
20
+ DURATIONS_DAYS: tuple[int, ...] = (30, 90, 180, 365) # the choices the form offers; no open-ended acceptance
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class Adjusted:
25
+ score: int
26
+ band: str
27
+ points_back: int # points the accepted checks cost in the raw score
28
+ accepted: tuple[str, ...] # check ids that were left out (only those the scan actually deducted for)
29
+
30
+
31
+ def expires_at(days: int, now: dt.datetime | None = None) -> str:
32
+ now = now or dt.datetime.now(dt.UTC)
33
+ return (now + dt.timedelta(days=days)).isoformat()
34
+
35
+
36
+ def is_active(expires_iso: str, now: dt.datetime | None = None) -> bool:
37
+ now = now or dt.datetime.now(dt.UTC)
38
+ try:
39
+ exp = dt.datetime.fromisoformat(expires_iso)
40
+ except ValueError:
41
+ return False # unreadable expiry: treat as expired (fail closed: the raw score applies)
42
+ if exp.tzinfo is None:
43
+ exp = exp.replace(tzinfo=dt.UTC)
44
+ return exp > now
45
+
46
+
47
+ def adjust(payload: dict, accepted_ids: set[str], coverage: float) -> Adjusted | None:
48
+ """The score with `accepted_ids` left out, or None when no accepted check cost points in this scan.
49
+
50
+ Rebuilt from the scan's own deduction list (100 minus every remaining deduction, clamped to 0..100), so it
51
+ matches the scoring engine exactly. The band follows the engine's rule, including 'incomplete' on low coverage.
52
+ """
53
+ deductions = (payload.get("result") or {}).get("deductions") or []
54
+ hit = tuple(sorted({d.get("check_id", "") for d in deductions
55
+ if d.get("check_id") in accepted_ids and int(d.get("points_lost", 0) or 0) > 0}))
56
+ if not hit:
57
+ return None
58
+ kept = sum(int(d.get("points_lost", 0) or 0) for d in deductions if d.get("check_id") not in accepted_ids)
59
+ back = sum(int(d.get("points_lost", 0) or 0) for d in deductions if d.get("check_id") in accepted_ids)
60
+ score = max(0, min(100, 100 - kept))
61
+ return Adjusted(score=score, band=_band(score, coverage), points_back=back, accepted=hit)